Skip to content

Camera

The Camera2D node, follow modes and pans, parallax layers, camera zones, smooth zoom and camera views.

Files: camera.odin, camera_tools.odin.

2D camera support for scenes.

A camera is a node: it looks at its own world position, and its world rotation turns the view. Parent it to a player and the view follows the player with no code; or leave it top-level and move it with Camera2D_Follow (smoothed) or Camera2D_MoveTo (timed pans). A scene draws every layer not listed in scene.screenLayers through its current camera (scene.camera); layers in scene.screenLayers (HUD, menus) are drawn in screen space.

screen = zoom * rotate(world - cameraWorldPosition, -cameraWorldRotation) + offset

Rotating the camera clockwise turns the view clockwise, so the world appears to rotate counter-clockwise on screen.

Example

package mygame
import e "engine:."
import sdl "vendor:sdl3"
WORLD_SIZE :: [2]f32{3840, 2160}
HUD_LAYER :: e.RenderLayer.Layer8
Player :: struct {
using node: e.Node,
}
CreateLevelScene :: proc(game: ^e.Game) -> ^e.Scene {
scene := game->createScene("Level")
scene.screenLayers = {HUD_LAYER} // Optional: the HUD ignores the camera
player := e.CreateNode(scene, Player)
player.position = WORLD_SIZE * 0.5
player.update = Player_update
player.draw = Player_draw
// Option 1: a camera that's rigidly attached to the player
// camera := e.Scene_CreateCamera(scene, parent = player)
// Option 2: a top-level camera that eases after the player and stays inside the level
camera := e.Scene_CreateCamera(scene)
camera.follow.mode = .SmoothDamp
camera.follow.smoothTime = 0.3
camera.follow.deadzone = {200, 120}
e.Camera2D_SetBounds(camera, {0, 0}, WORLD_SIZE)
e.Camera2D_SnapTo(camera, player.position)
return scene
}
Player_update :: proc(node: ^e.Node, events: []sdl.Event, deltaTime: f64) {
camera := node.scene.camera
for &event in events {
#partial switch event.type {
case .MOUSE_WHEEL:
camera.zoom = clamp(camera.zoom + event.wheel.y * 0.1, 0.5, 3)
case .MOUSE_BUTTON_DOWN:
// Event positions are in screen space; convert to find the clicked world point
node.position = e.Scene_ScreenToWorld(node.scene, {event.button.x, event.button.y})
case .KEY_DOWN:
// Cinematic pan to the center of the level. Following pauses until it ends
if event.key.scancode == .C do e.Camera2D_MoveTo(camera, WORLD_SIZE * 0.5, 1.5, .InOutCubic)
}
}
e.Camera2D_Follow(camera, e.Node_GetWorldPosition(node), deltaTime)
}
Player_draw :: proc(node: ^e.Node, renderer: ^sdl.Renderer) {
// Local coordinates: the node's transform and the camera are applied for us
e.DrawCircle(renderer, {0, 0}, 24, {120, 200, 255, 255})
}

Camera tools: parallax layers (backgrounds that move slower than the world, for depth), camera zones (rooms the camera stays inside), smooth zoom, and camera views (what another camera sees, in a rectangle on the screen: split screen, a minimap, picture in picture).

  • ParallaxLayer (ParallaxSettings): its image (texture) and children move with the camera by factor of its motion: 1 like the world, 0.5 half as fast (far away), 0 not at all (the sky). repeatX / repeatY tile the image across the screen that way. Its position is where it is when the camera is where the scene starts it (ParallaxLayer.base); scripts move base.
  • CameraZone (size from its position, transition seconds): while the scene camera’s position is inside a zone, the camera’s bounds are the zone, so it can’t show past the room; entering another zone slides the bounds over to it. No zone: the bounds the camera had.
  • Smooth zoom: Camera2D_ZoomTo(camera, zoom, seconds).
  • CameraView (size, camera: a path to a Camera node, zoom, border): draws what that camera sees into its rectangle (put it on a screen layer). Each player’s camera in a split screen, a zoomed-out camera for a minimap. Other cameras than the scene’s follow by being children of what they follow.
CameraFollowMode :: enum {
// Jump straight to the position every frame. No easing
Snap,
// Close a fraction of the remaining distance every frame: fast at first, slowing as it arrives.
// Controlled by `CameraFollow.smoothing`
Exponential,
// Critically damped spring: accelerates and decelerates smoothly with no overshoot.
// Feels the most natural for player cameras. Controlled by `CameraFollow.smoothTime`
SmoothDamp,
// Move toward the position at a constant speed. Controlled by `CameraFollow.speed`
Linear,
}

How a camera tracks a moving position in Camera2D_Follow.

enum. Source: camera.odin:81

CameraFollow :: struct {
// How it eases toward the position
mode: CameraFollowMode,
// .Exponential: how tightly to follow. Higher is tighter; around 4-10 feels natural
smoothing: f32,
// .SmoothDamp: roughly how many seconds the camera takes to catch up
smoothTime: f32,
// .Linear: world units per second
speed: f32,
// Caps how fast the camera can move, in world units per second (not in .Snap). 0 = no limit
maxSpeed: f32,
// Size of a box, in world units and centered on the camera, that the followed position can
// move inside without moving the camera. {0, 0} disables it
deadzone: [2]f32,
}

Settings for Camera2D_Follow. Each mode only reads its own strength setting; deadzone applies to every mode, maxSpeed to every mode but .Snap (which always jumps).

struct. Source: camera.odin:98

Camera2D :: struct {
// The node part: transform, hierarchy, hooks
using node: Node,
// Screen position the camera's target appears at, usually the center of the screen
offset: [2]f32,
// Scale factor. 1 is unscaled, 2 shows everything twice as big, 0.5 half as big
zoom: f32,
// When true, the camera's target is clamped every frame so the view stays inside `min..max`.
// Set with `Camera2D_SetBounds`. Rotation is not taken into account, so a rotated view can
// show slightly past the bounds at its corners
bounded: bool,
// Top-left corner of the world boundary, used when `bounded` is true
min: [2]f32,
// Bottom-right corner of the world boundary, used when `bounded` is true
max: [2]f32,
// How `Camera2D_Follow` eases toward the followed position
follow: CameraFollow,
// Internal state; read it if useful but don't modify it
// Current follow velocity in world units per second (.SmoothDamp)
velocity: [2]f32,
// Active `Camera2D_MoveTo` pan
move: CameraMove,
// Active `Camera2D_Shake`
shake: CameraShake,
// Active `Camera2D_ZoomTo` (the zoom in `from.x` / `to.x`: camera_tools.odin)
zoomMove: CameraMove,
// A `Camera2D_MoveTo` pan reached its end (not when replaced or cancelled)
onPanFinished: EventSubject `event:"A timed pan (Camera2D_MoveTo) reached its end"`,
// A `Camera2D_Shake` settled
onShakeFinished: EventSubject `event:"A shake (Camera2D_Shake) settled"`,
}

A camera node. It looks at its world position (Camera2D_GetTarget), and its world rotation turns the view. Create one with Scene_CreateCamera.

A zero-initialized Camera2D also works as a plain value outside a scene: a zoom of 0 is treated as 1 and following snaps.

struct. Source: camera.odin:129

CameraShake :: struct {
// Screen pixels at the start, seconds in all, seconds left
strength, duration, left: f32,
// The view's offset this frame, in screen pixels
offset: [2]f32,
}

State of a Camera2D_Shake

struct. Source: camera.odin:173

CameraMove :: struct {
// A pan is going
active: bool,
// Start and end of the pan, in world pixels
from, to: [2]f32,
// Seconds the pan takes, and seconds into it
duration, elapsed: f32,
// The pan's curve (easing.odin)
ease: EaseType,
}

State of a Camera2D_MoveTo pan

struct. Source: camera.odin:181

CameraToolSettings :: struct {
// ParallaxLayer: how much it moves with the camera (1: like the world, 0: fixed on screen)
factor: [2]f32,
// ParallaxLayer: its image tiles across the screen this way
repeatX, repeatY: bool,
// CameraZone: seconds the camera's bounds slide to it
transition: f32,
// CameraView: zoom (times its camera's)
zoom: f32,
// CameraView: its frame's width in pixels (0: none)
border: f32,
}

A ParallaxLayer’s and CameraZone’s and CameraView’s settings (SceneNodeData.cameraTools)

struct. Source: camera_tools.odin:26

ParallaxLayer :: struct {
using node: Node,
settings: CameraToolSettings,
// Its image (nil: just its children)
texture: ^sdl.Texture,
// Where it is when the camera is where it started (its scene file position)
base: [2]f32,
// The camera's position when the scene started (the parallax is relative to it)
cameraStart: [2]f32,
started: bool,
}

A background layer moving slower than the world: see the file’s overview

struct. Source: camera_tools.odin:43

CameraZone :: struct {
using node: Node,
// Its size from its position (its top-left corner)
size: [2]f32,
settings: CameraToolSettings,
}

A room the camera stays inside: see the file’s overview

struct. Source: camera_tools.odin:56

CameraView :: struct {
using node: Node,
// Its rectangle's size, from its position (its top-left corner)
size: [2]f32,
settings: CameraToolSettings,
// The camera it shows: a path from it (node arena)
cameraPath: string,
target: ^RenderTarget,
}

What another camera sees, on screen: see the file’s overview

struct. Source: camera_tools.odin:64

CameraZoneState :: struct {
// The zone the camera is in (zero: none)
zone: EntityId,
// The bounds sliding from, to, and how far (0 to 1) over `duration`
fromMin, fromMax, toMin, toMax: [2]f32,
elapsed, duration: f32,
sliding: bool,
}

Bounds sliding from one zone to another (Scene.cameraZones)

struct. Source: camera_tools.odin:75

Scene_CreateCamera :: proc(scene: ^Scene, parent: ^Node = nil) -> ^Camera2D

Creates a camera node in a scene and makes it the scene’s camera.

A top-level camera starts at the center of the game resolution, so world and screen coordinates line up until it moves. A camera with a parent starts at the parent’s origin and moves with it.

Inputs

  • scene: The scene
  • parent: Node to attach the camera to, or nil for a top-level camera

Returns

  • The new camera, also stored in scene.camera

Example

camera := e.Scene_CreateCamera(scene)
camera.zoom = 2

procedure. Source: camera.odin:210

Scene_SetCamera :: proc(scene: ^Scene, camera: ^Camera2D)

Makes a camera the one the scene draws through (scene.camera). Pass nil to draw without a camera.

Inputs

  • scene: The scene
  • camera: The camera (normally one of the scene’s nodes), or nil

procedure. Source: camera.odin:232

Camera2D_GetTarget :: proc(camera: ^Camera2D) -> [2]f32

The world position the camera looks at (its world position).

Inputs

  • camera: The camera

Returns

  • The position, in world pixels

procedure. Source: camera.odin:245

Camera2D_SetTarget :: proc(camera: ^Camera2D, target: [2]f32)

Moves the camera to look at a world position, whatever its parent is. Unlike Camera2D_SnapTo, a pan or follow velocity going on is left alone.

Inputs

  • camera: The camera
  • target: The world position to look at, in world pixels

procedure. Source: camera.odin:257

Camera2D_SetBounds :: proc(camera: ^Camera2D, min, max: [2]f32)

Restricts the camera so the visible area stays inside a world rectangle.

When the camera is the scene’s camera, the bounds are applied automatically each frame after the scene’s nodes update. If the bounds are smaller than the view on an axis, the camera is centered on that axis instead.

Inputs

  • camera: The camera to restrict
  • min: Top-left corner of the allowed world area
  • max: Bottom-right corner of the allowed world area

Example

// Given: camera: ^e.Camera2D
e.Camera2D_SetBounds(camera, {0, 0}, {3840, 2160})

procedure. Source: camera.odin:277

Camera2D_ClearBounds :: proc(camera: ^Camera2D)

Removes the boundaries set with Camera2D_SetBounds, letting the camera move freely.

Inputs

  • camera: The camera

procedure. Source: camera.odin:289

Camera2D_ViewTransform :: proc(camera: ^Camera2D) -> Transform2D

The transform from world coordinates to screen coordinates for this camera, including any shake. Uses the camera’s interpolated (drawn) transform.

Inputs

  • camera: The camera

Returns

  • offset × zoom × rotate(-world rotation) × translate(-world position)

procedure. Source: camera.odin:303

Camera2D_WorldToScreen :: proc(camera: ^Camera2D, world: [2]f32) -> [2]f32

Converts a world position to the screen position it is drawn at.

Inputs

  • camera: The camera
  • world: A position in world pixels

Returns

  • The screen position, in game coordinates

Example

// Given: camera: ^e.Camera2D, enemy: ^e.Node
// Draw a screen-space marker over a world object
markerPos := e.Camera2D_WorldToScreen(camera, e.Node_GetWorldPosition(enemy))

procedure. Source: camera.odin:327

Camera2D_ScreenToWorld :: proc(camera: ^Camera2D, screen: [2]f32) -> [2]f32

Converts a screen position (e.g. from a mouse event) to the world position under it.

Inputs

  • camera: The camera
  • screen: A screen position, in game coordinates

Returns

  • The world position, in world pixels

Example

// Given: camera: ^e.Camera2D
world := e.Camera2D_ScreenToWorld(camera, e.Input_GetMousePosition())
_ = world

procedure. Source: camera.odin:346

Camera2D_Follow :: proc(camera: ^Camera2D, position: [2]f32, deltaTime: f64)

Eases the camera toward a world position using the settings in camera.follow. Call once per frame, e.g. from the update of the node being followed.

Does nothing while a Camera2D_MoveTo pan is running, so pans aren’t interrupted. Frame rate independent in every mode. A camera parented to the followed node doesn’t need this.

Inputs

  • camera: The camera to move
  • position: World position to follow, e.g. Node_GetWorldPosition(player)
  • deltaTime: Seconds since the last frame

Example

// Given: player: ^e.Node
e.Camera2D_Follow(node.scene.camera, e.Node_GetWorldPosition(player), deltaTime)

procedure. Source: camera.odin:366

Camera2D_MoveTo :: proc(camera: ^Camera2D, position: [2]f32, duration: f32, ease := EaseType.InOutCubic)

Pans the camera to a world position over a fixed time, following an easing curve.

The pan is advanced automatically by the scene whose camera it is, after its nodes update. While it runs, Camera2D_Follow does nothing; afterwards following resumes from wherever the pan ended. Starting a new pan replaces the current one.

Inputs

  • camera: The camera to pan
  • position: World position to pan to
  • duration: Seconds the pan takes. 0 or less jumps immediately
  • ease: Easing curve for the pan

Example

// Given: camera: ^e.Camera2D, door: ^e.Node
// Show the exit door, then return to the player
e.Camera2D_MoveTo(camera, e.Node_GetWorldPosition(door), 1.2, .InOutSine)

procedure. Source: camera.odin:429

Camera2D_SnapTo :: proc(camera: ^Camera2D, position: [2]f32)

Moves the camera to look at a world position immediately, cancelling any pan and easing. Use it when a scene starts or the player respawns.

Inputs

  • camera: The camera
  • position: The world position to look at, in world pixels

Example

// Given: camera: ^e.Camera2D, spawn: [2]f32
e.Camera2D_SnapTo(camera, spawn)

procedure. Source: camera.odin:456

Camera2D_IsMoving :: proc(camera: ^Camera2D) -> bool

Tells whether a Camera2D_MoveTo pan is in progress.

Inputs

  • camera: The camera

Returns

  • true while it pans

procedure. Source: camera.odin:471

Camera2D_Shake :: proc(camera: ^Camera2D, strength, duration: f32)

Shakes the view: it jumps around its place by up to strength screen pixels, less and less over duration seconds (a hit, an explosion). A stronger shake replaces a weaker one; a weaker one started during a stronger one is ignored. The shake only moves what’s drawn (the view transform), never the camera node, so following, bounds and scripts reading its position are unaffected.

The game advances it in real time (Camera2D_UpdateShake, from Game_Update), so it keeps going through a hitstop (Game_HitStop).

Inputs

  • camera: The camera
  • strength: Largest offset, in screen pixels (game coordinates)
  • duration: Seconds until it settles

Example

e.Camera2D_Shake(node.scene.camera, 4, 0.2)

procedure. Source: camera.odin:519

Camera2D_ClampToBounds :: proc(camera: ^Camera2D, viewSize: [2]f32)

Adjusts the camera so the visible area stays inside its bounds. Does nothing if the camera is not bounded. Called by Camera2D_Update.

Inputs

  • camera: The camera to clamp
  • viewSize: Size of the visible area in screen coordinates, normally game.size

procedure. Source: camera.odin:553

Scene_ScreenToWorld :: proc(scene: ^Scene, screen: [2]f32) -> [2]f32

Converts a screen position to world coordinates using the scene’s camera. Without a camera, screen and world coordinates are the same.

Inputs

  • scene: The scene
  • screen: A screen position, in game coordinates

Returns

  • The world position, in world pixels

procedure. Source: camera.odin:588

Scene_WorldToScreen :: proc(scene: ^Scene, world: [2]f32) -> [2]f32

Converts a world position to screen coordinates using the scene’s camera. Without a camera, screen and world coordinates are the same.

Inputs

  • scene: The scene
  • world: A position in world pixels

Returns

  • The screen position, in game coordinates

procedure. Source: camera.odin:604

Scene_GetMouseWorldPosition :: proc(scene: ^Scene) -> [2]f32

The world position currently under the mouse cursor, for code that polls the mouse instead of handling events.

Inputs

  • scene: The scene (its camera and game)

Returns

  • The world position, in world pixels

Example

target := e.Scene_GetMouseWorldPosition(scene)
e.Debug_Circle(scene, target, 4)

procedure. Source: camera.odin:623

CreateParallaxLayer :: proc(scene: ^Scene, texture: ^sdl.Texture, factor := [2]f32{0.5, 0.5}) -> ^ParallaxLayer

Creates a ParallaxLayer and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • texture: Its image (nil: just its children)
  • factor: How much it moves with the camera (1: like the world, 0: fixed on screen)

Returns

  • The layer

Example

// Given: hills: ^sdl.Texture
far := e.CreateParallaxLayer(scene, hills, {0.3, 0})
far.settings.repeatX = true

procedure. Source: camera_tools.odin:100

CreateCameraZone :: proc(scene: ^Scene, size: [2]f32) -> ^CameraZone

Creates a CameraZone and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • size: Its width and height, from its position (its top-left corner)

Returns

  • The zone

Example

room := e.CreateCameraZone(scene, {640, 360})
room.position = {640, 0}

procedure. Source: camera_tools.odin:130

CreateCameraView :: proc(scene: ^Scene, size: [2]f32, cameraPath: string) -> ^CameraView

Creates a CameraView and adds it to the scene (at the start of the next update).

Inputs

  • scene: The scene
  • size: Its rectangle’s size
  • cameraPath: The Camera node it shows (a path from it)

Returns

  • The view

Example

minimap := e.CreateCameraView(scene, {200, 120}, "../MapCamera")
minimap.settings.zoom = 0.2

procedure. Source: camera_tools.odin:156

Camera2D_ZoomTo :: proc(camera: ^Camera2D, zoom: f32, duration: f32, ease := EaseType.InOutCubic)

Zooms a camera smoothly to a zoom over some seconds.

Inputs

  • camera: The camera
  • zoom: The zoom to reach (1: unscaled)
  • duration: Seconds (0: at once)
  • ease: The curve

Example

e.Camera2D_ZoomTo(scene.camera, 2, 0.5)

procedure. Source: camera_tools.odin:183

DEFAULT_CAMERA_FOLLOW :: CameraFollow{
mode = .Exponential,
smoothing = 6,
smoothTime = 0.25,
speed = 600,
}

The follow settings new cameras get: .Exponential with a smoothing of 6, no deadzone or speed cap

constant. Source: camera.odin:115

DEFAULT_CAMERA_TOOL_SETTINGS :: CameraToolSettings{factor = {0.5, 0.5}, transition = 0.4, zoom = 1, border = 2}

When a scene file sets none

constant. Source: camera_tools.odin:40