Camera
Camera
Section titled “Camera”The camera controls the viewport into your game world. Spawn a CameraEntity
in your scene to create it:
import { Vec2 } from "@yagejs/core";import { CameraEntity } from "@yagejs/renderer";
const camera = this.spawn(CameraEntity, { position: new Vec2(400, 300) });// All camera operations are available directly on camera:// camera.follow(), camera.shake(), camera.zoomTo(), camera.bounds, etc.Coordinate Convention
Section titled “Coordinate Convention”Camera position (0, 0) places the world origin at the center of the
viewport, not the top-left. An entity drawn at world position (0, 0)
appears in the middle of the screen; positive X goes right, positive Y goes
down.
This is the convention most camera-driven 2D games expect. A scrolling shooter or platformer naturally wants the camera to follow the player, and centring the follow target on screen is the intuitive default.
If your game has a fixed, non-scrolling layout (a puzzle grid, an arcade-style
single-screen game, a tile editor), you probably want world (0, 0) to align
with the top-left of the screen instead — that way tile coordinates, UI
anchors, and typical 2D art tools line up the way you’d expect. Offset the
camera by half the viewport in onEnter:
class GameScene extends Scene { readonly name = "game";
onEnter() { // Top-left origin: world (0,0) → screen (0,0) this.spawn(CameraEntity, { position: new Vec2(400, 300) }); // viewport is 800×600 }}The camera never changes; you’re just choosing which world point sits under the viewport’s top-left corner. Follow-a-target cameras work identically with either convention — they just move to frame the target.
Frame a world rectangle (fitTo)
Section titled “Frame a world rectangle (fitTo)”For fixed-camera scenes — puzzle boards, arcade-style single-screen
games, dialog-scene insets — the ergonomic gap is “frame this
rectangle”: position and zoom together so a known world area fills
the viewport. fitTo: { x, y, width, height } does both in one call:
this.spawn(CameraEntity, { fitTo: { x: 0, y: 0, width: 800, height: 600 },});The camera centres on the rect’s midpoint and sets zoom = min(viewportW / rect.w, viewportH / rect.h) — contain semantics, so the entire rect is
always visible (any spare viewport space along the off-aspect axis stays
empty, exactly like CSS object-fit: contain). Overrides any explicit
position and zoom you pass alongside.
fitTo is applied once at setup against the renderer’s current
viewport. For runtime re-framing (entering a sub-area, transition
between rooms), set position and call zoomTo() directly on the
camera — fitTo isn’t a responsive binding.
Want a fixed camera that doesn’t auto-track? Just don’t pass follow —
that’s already the “don’t move” state. The previous fit: "static"
mode was redundant with that and has been dropped.
Following a Target
Section titled “Following a Target”const cam = this.spawn(CameraEntity, { follow: player.get(Transform), smoothing: 0.1, offset: { x: 0, y: -50 }, deadzone: { halfWidth: 50, halfHeight: 30 }, snap: true,});smoothing controls how quickly the camera catches up (1 = instant, 0 = never
moves). The deadzone defines a rectangle in the center of the screen where
the target can move without the camera responding.
snap: true starts the camera on the target. Leave it out and a smoothed
camera begins wherever it spawned — the world origin, unless position or
fitTo places it elsewhere — so the scene opens with a visible glide toward
the player. The offset applies to the snapped position, and any deadzone takes
effect from the next frame.
snapToTarget() does the same cut mid-game, for a room change or a respawn
where the target jumps across the map:
player.get(Transform).setPosition(1600, 900);cam.snapToTarget();Zoom and Rotation
Section titled “Zoom and Rotation”camera.zoomTo(2.0, 0.5, easeOutQuad); // zoom to 2x over 0.5scamera.rotation = Math.PI / 12; // tilt the cameraScreen Shake
Section titled “Screen Shake”camera.shake(8, 0.4, { decay: 1 });intensity is the maximum pixel displacement per frame. decay sets how much
the shake fades across its duration: 0 (the default) shakes at full strength
until it ends, 1 fades linearly to zero over the duration, and values above
1 reach zero earlier — at 2 the camera stops moving halfway through.
Coordinate Conversion
Section titled “Coordinate Conversion”Convert between screen (pixel) space and world space:
const worldPos = camera.screenToWorld(screenPos);const screenPos = camera.worldToScreen(worldPos);Bounds
Section titled “Bounds”Constrain the camera to a region so it never shows areas outside the level:
camera.bounds = { minX: 0, minY: 0, maxX: 4000, maxY: 2000 };Camera bindings
Section titled “Camera bindings”A CameraEntity spawned without bindings auto-binds every
space: "world" layer at full strength. For finer control — parallax,
minimaps, decoupled HUDs — pass an explicit bindings array. Each
binding has three independent ratios:
interface CameraBinding { layer: string; translateRatio?: number; // default 1 — follows the camera position rotateRatio?: number; // default 1 — rotates with the camera scaleRatio?: number; // default 1 — zooms with the camera}Each ratio is a linear blend from identity (0, ignores that axis of the
camera) to full effect (1, fully follows that axis).
this.spawn(CameraEntity, { bindings: [ { layer: "sky", translateRatio: 0.1 }, // slow parallax { layer: "mid", translateRatio: 0.6 }, // medium parallax { layer: "world" }, // full transform (default) { layer: "minimap", // camera-agnostic overlay painted on a world layer translateRatio: 0, rotateRatio: 0, scaleRatio: 0 }, ],});These ratios are layer-level decoupling primitives: they’re the
right answer for parallax, minimaps, and other content whose position
already lives in the coord space the layer provides. They are not
the right answer for entity-anchored UI like nameplates or health bars
— mixing a partial camera transform with the main camera’s full
transform separates the UI from its target under zoom. For that use
case, see ScreenFollow below.
ScreenFollow (entity-anchored UI)
Section titled “ScreenFollow (entity-anchored UI)”ScreenFollow projects a world source through a camera and writes the
resulting screen coord to its entity’s Transform each frame. Paired
with a UISurface (or UIRoot) on a screen-space layer using
positioning: "transform", it produces UI that tracks a target entity
but stays axis-aligned and constant-size regardless of camera zoom or
rotation — the canonical “billboard” primitive for nameplates, health
bars, damage numbers, and interaction prompts.
import { ScreenFollow } from "@yagejs/renderer";import { UISurface, Anchor } from "@yagejs/ui";
class EnemyNameplate extends Entity { setup(params: { target: Entity; camera: CameraEntity; label: string }) { this.add(new Transform()); this.add(new ScreenFollow({ target: params.target, camera: params.camera, offset: new Vec2(0, -40), // 40 screen pixels above the target, at any zoom })); const panel = this.add(new UISurface({ positioning: "transform", // read Transform.worldPosition anchor: Anchor.BottomCenter, // pivot on the panel padding: 4, background: { color: 0x000000, alpha: 0.6, radius: 4 }, })); panel.text(params.label, { fontSize: 11, fill: 0xffffff }); }}offset is applied in screen pixels, after projection —
concretely cam.worldToScreen(target) + offset. That keeps the visual
gap between UI and target fixed under any camera transform: a 40px
offset above is 40 screen pixels above at any zoom, any rotation. Adding
the offset in world coords before projection (the intuitive-seeming
shape) would let the camera transform warp it — the gap would double at
zoom 2 and rotate off-axis as the camera rotates.
target accepts an Entity, a static Vec2Like, or a function
returning a Vec2Like — you can track anything whose world position
you can name, including animated paths or the midpoint of two entities.
See the UI guide for
the full logical-root + siblings pattern and the world-ui example for
a runnable demo.
Render layers and draw order
Section titled “Render layers and draw order”Layers control which entities paint on top of which, and within a layer
you can depth-sort entities so a top-down game’s “in front of” relationship
looks right (a character standing below a sign paints over it, and one
standing above it paints under it). See Layers & Draw Order for
declaring layers, depth sorting with sort/ySort, and keeping a
multi-part entity from splitting apart under a sort with
SortGroupComponent.
Display System
Section titled “Display System”The built-in display system automatically synchronizes each entity’s Transform
component with the underlying PixiJS display object. When you update position,
rotation, or scale on a Transform, the corresponding Pixi sprite or graphic
moves to match — no manual syncing required.
// Moving the transform moves the sprite on screenentity.transform.setPosition(200, 300);entity.transform.rotate(0.5);entity.transform.setScale(2, 2);This one-way sync (ECS to Pixi) runs once per frame after all component updates have completed, keeping rendering deterministic and free of mid-frame visual glitches.