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

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 the Viewport X/Y/W/H for split‑screen. - To follow a target, use the “Camera Follow” list to pick an entity. Toggle Follow Rotation if 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 Priority to 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.layers used 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_present controls 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_color and LightingSystem.enabled.

  • Occluder flags per object: receive_light and receive_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 CameraComponent exists, 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.