Camera & Rendering Tutorial
This tutorial explains cameras, world/screen conversion, layer ordering, design resolution and stretching, sprites and UI rendering, plus lights and shadows. It follows the actual RenderSystem and CameraComponent APIs.
Overview
CameraComponent— camera position/zoom/rotation, split viewports, follow, shake.RenderSystem— draws sprites/tilemaps/UI, manages cameras, world↔screen helpers.SpriteRenderer— basic sprite/rect rendering with transform-driven scale/rotation.LightingSystemwithPointLight2D,SpotLight2D,LightOccluder2D— optional light/shadow overlay.
Editor workflow (no code)
Add a camera - Select an entity in the Hierarchy (e.g., “Main Camera” or create a new one). - Inspector → Add Component → Camera Component. - Adjust
Zoom,Rotation, and theViewport X/Y/W/Hfor split‑screen. - To follow a target, use the “Camera Follow” list to pick an entity. ToggleFollow Rotationif desired.Split‑screen - Add a Camera Component to two different entities. - Set viewports, e.g., top: X=0, Y=0, W=1, H=0.5; bottom: X=0, Y=0.5, W=1, H=0.5. - Use
Priorityto control draw order when viewports overlap (lower draws first).Add a sprite - Select an entity, Inspector → Add Component → Sprite Renderer. - Click “Browse Image…” to pick a texture. Width/Height reflect Transform scale; negative scale flips.
Order with layers - Select any entity with a Transform; use the “Layer” dropdown. - Click “Edit Layers” to add/reorder global
world.layersused for render order (first entry draws behind). - You can also manage layers from Project Settings → Layers/Groups.Design resolution & stretching (Project Settings) - Project → Project Settings → Display tab. - Set Game Width/Height (virtual resolution) and Window Width/Height. - Choose Stretch Mode (fit/stretch/crop), Aspect (keep/ignore), and Scale (fractional/integer). - Save settings; the runtime applies them on launch.
Lights and shadows - Select or create an entity, Inspector → Add Component → Lighting → Point Light 2D or Spot Light 2D. - Add occluders: Inspector → Add Component → Lighting → Light Occluder 2D. For box/circle, sizes can derive from SpriteRenderer. - Shadow range is configurable in Project Settings → Lighting → Shadow Extend.
Cameras: Single, Split-screen, Follow, Shake
Create a camera by adding CameraComponent to an entity that has a Transform.
from core.components import Transform, CameraComponent
class CameraSetup:
def on_start(self):
if not self.entity.get_component(Transform):
self.entity.add_component(Transform(x=400, y=300))
cam = self.entity.get_component(CameraComponent) or CameraComponent(zoom=1.0)
if not self.entity.get_component(CameraComponent):
self.entity.add_component(cam)
Fields: -
zoom(float ≥0.01) -rotation(deg) -viewport_x/y/width/height(0..1 fractions of the render surface) for split-screen. -priority(lower renders first behind higher-priority cameras). -follow_target_id(entity ID to follow),follow_rotation.
Split-screen example (two cameras):
# Top camera (upper half)
top = top_entity.add_component(CameraComponent(viewport_x=0, viewport_y=0, viewport_width=1, viewport_height=0.5))
# Bottom camera (lower half)
bot = bot_entity.add_component(CameraComponent(viewport_x=0, viewport_y=0.5, viewport_width=1, viewport_height=0.5))
Follow a target by ID:
# Find player and set follow_target_id
player = self.find("Player")
if player:
cam = self.entity.get_component(CameraComponent)
cam.follow_target_id = player.id
cam.follow_rotation = True # or False to keep camera's own rotation
Camera shake:
cam = self.entity.get_component(CameraComponent)
cam.shake(intensity=6.0, duration=0.25, decay=CameraComponent.DECAY_EXPONENTIAL)
World ↔ Screen Conversion
Use RenderSystem helpers to convert between world and screen coordinates for the current primary camera.
from core.systems.render_system import RenderSystem
rs = self.entity.world.get_system(RenderSystem)
# World to screen (pixels in the camera viewport)
sx, sy = rs.world_to_screen( world_x, world_y, entities=self.entity.world.entities )
# Screen (viewport pixels) to world
wx, wy = rs.screen_to_world( screen_x, screen_y, entities=self.entity.world.entities )
Tip: To convert the game mouse to world, combine with Input.get_game_mouse_position() from the Advanced Input tutorial.
Layer Ordering
Render order follows world.layers (first name is drawn first, i.e., behind). Entities carry a string entity.layer; use entity.set_layer(name) to set the entity and all its children.
# Configure once (e.g., in a manager script)
self.entity.world.layers = ["Background", "Middleground", "Foreground", "UI"]
# Place entities on layers
self.entity.set_layer("Foreground")
Design Resolution, Stretching, and Present Smoothing
The runtime uses a virtual render surface (design resolution) and then presents it to the window according to stretch settings.
Design size comes from the player’s config; also set on
RenderSystem.design_size.Stretch options (from RuntimePlayer):
stretch_mode("fit"|"stretch"|"crop"),stretch_aspect("keep"|"ignore"),stretch_scale("integer"|"fractional").Present smoothing (bilinear vs. nearest):
RenderSystem.smooth_presentcontrols scaling quality.
Toggle crisp pixel-art scaling at runtime:
from core.systems.render_system import RenderSystem
rs = self.entity.world.get_system(RenderSystem)
rs.smooth_present = False # nearest-neighbor when the runtime scales the frame
Sprites and Animation
SpriteRenderer draws either a loaded image or a colored rectangle. Width/height reflect the entity’s scale (negative scale flips).
from core.components import Transform, SpriteRenderer
class SpriteSetup:
def on_start(self):
if not self.entity.get_component(Transform):
self.entity.add_component(Transform(x=200, y=200))
spr = self.entity.get_component(SpriteRenderer)
if not spr:
spr = SpriteRenderer(image_path="assets/hero.png") # or omit for a colored rect
self.entity.add_component(spr)
UI Overlay
UI components (TextRenderer, ButtonComponent, etc.) are rendered in screen space after world cameras. Their Transform coordinates are pixels in the design resolution. When a camera uses a sub-viewport, UI can also be drawn inside that viewport (editor workflows use this).
Lights and Shadows (Optional)
The LightingSystem composites an ambient + light overlay using multiply blending. RuntimePlayer already adds it after the RenderSystem.
from core.components import Transform
from core.components.light import PointLight2D, SpotLight2D, LightOccluder2D
class LightsExample:
def on_start(self):
# Torch
torch = self.find("Torch")
if torch is None:
torch = self.entity.world.create_entity("Torch")
torch.add_component(Transform(x=300, y=200))
torch.add_component(PointLight2D(color=(255, 200, 120), radius=300, intensity=1.0))
# Spotlight
spot = self.entity.world.create_entity("Spot")
spot.add_component(Transform(x=500, y=300))
spot.add_component(SpotLight2D(color=(200, 220, 255), radius=400, intensity=1.0, angle=0.0, cone_angle=40))
# Wall occluder (casts shadows)
wall = self.entity.world.create_entity("Wall")
wall.add_component(Transform(x=400, y=280))
wall.add_component(LightOccluder2D(shape="box", width=200, height=40))
Ambient color and global enable are on the system:
LightingSystem.ambient_colorandLightingSystem.enabled.Occluder flags per object:
receive_lightandreceive_shadow.
Script Editor snippets you may need
Toggle crisp scale filter at present time
from core.systems.render_system import RenderSystem
rs = self.entity.world.get_system(RenderSystem)
if rs:
rs.smooth_present = False # nearest-neighbor scale on present
World ↔ screen mouse conversion
from core.systems.render_system import RenderSystem
from core.input import Input
rs = self.entity.world.get_system(RenderSystem)
if rs:
gx, gy = Input.get_game_mouse_position() # mapped to virtual surface
wx, wy = rs.screen_to_world(gx, gy, entities=self.entity.world.entities)
Trigger a quick camera shake
from core.components import CameraComponent
cam = self.entity.get_component(CameraComponent)
if cam:
cam.shake(intensity=6.0, duration=0.25, decay=CameraComponent.DECAY_EXPONENTIAL)
Notes
If no active
CameraComponentexists, the renderer uses its internal camera fields (camera_x/y/zoom/rotation) to draw a single full-screen view.Render order is also affected by visibility (
entity.hide()/show()) and negative transform scale flips.