Cameras

Follow and orbit cameras for third-person games, with smoothing, shoulder views, obstacle clearance and shake.

Commands

Import blitz.cameras As Cam gives your program these, with Cam. in front. Each name leads to its part of the reference.

Functions

Types

Enums

Guide

Cameras gives your game a third-person camera that follows the player about: behind them, over a shoulder, swinging round when the player wants to look about, and moving in close rather than going through a wall. It glides into place instead of jerking, blends smoothly from a close view to a wide one, and shakes when something explodes. You keep your own camera and player. The module works out where the camera should be, each frame.

A yellow barrel among tall blocks, seen from a camera that has moved in close to keep out of a block

Try it

Open yard.bb and run it. Walk with the arrow keys and watch the camera follow. Back up to a block, or swing the camera round with Q and E, and it moves in close rather than going inside. Tab moves it to the other shoulder, and Space shakes it.

; Walk round the yard with the arrow keys and the camera follows. Q and E
; swing it round, Tab moves it to the other shoulder, Space shakes it and
; Escape quits.
Dialect "modern"
Import blitz.cameras As Cam

; A tall block that the camera won't go into. The camera only knows about
; things that have a physics shape, so each block gets one.
Function Block(world:Int, x:Float, z:Float, width:Float, depth:Float)
    Local box:Int = CreateCube()
    ScaleEntity box, width / 2, 3, depth / 2
    PositionEntity box, x, 3, z
    EntityColor box, 190, 115, 75
    Local body:Int = CreatePhysicsBody(world, box, 0) ; 0: it never moves.
    PhysicsBodyBox body, width, 6, depth
End Function

Graphics3D 800, 480, 0, 2
SetBuffer BackBuffer()
Local camera:Int = CreateCamera()
CameraClsColor camera, 18, 52, 79
CameraRange camera, 0.05, 100
Local light:Int = CreateLight()
RotateEntity light, 45, -35, 0
AmbientLight 90, 90, 100
Local ground:Int = CreatePlane()
EntityColor ground, 70, 100, 90

Local world:Int = CreatePhysicsWorld()
Block world, -4, 4, 3, 3
Block world, 4, 5, 4, 2
Block world, 5, -3, 2, 4
Block world, -5, -4, 2, 2
Block world, 0, 11, 10, 2
Block world, -1, -10, 6, 2

; The player: a barrel with a nose, so you can see which way it faces.
Local player:Int = CreatePivot()
PositionEntity player, 0, 1, 0
Local barrel:Int = CreateCylinder(16, True, player)
ScaleEntity barrel, 0.4, 0.9, 0.4
EntityColor barrel, 245, 195, 65
Local nose:Int = CreateCube(player)
ScaleEntity nose, 0.15, 0.15, 0.3
PositionEntity nose, 0, 0.4, 0.45
Local mover:Int = CreatePhysicsQuery(world) ; Slides the player along blocks.

; The rig puts the camera behind the player, a little to the right.
Local view:Cam.RigOptions = New Cam.RigOptions
view.shoulder = 0.8
Local rig:Cam.Rig = Cam.CreateRig(camera, player, view)
Cam.RigObstacles rig, world ; Keep out of the blocks.

Local previous:Int = MilliSecs()
While Not KeyDown(1)
    Local now:Int = MilliSecs()
    Local seconds:Double = Double(now - previous) / 1000D
    previous = now

    ; Move the player first: turn, then walk forward or back.
    TurnEntity player, 0, Float(KeyDown(203) - KeyDown(205)) * 120 * Float(seconds), 0
    TFormVector 0, 0, Float(KeyDown(200) - KeyDown(208)) * 4 * Float(seconds), player, 0
    MovePhysicsCharacter mover, player, 0.8, 1.8, TFormedX(), 0, TFormedZ()

    ; Then the camera. It never reads the keys itself.
    Cam.RigOrbit rig, Float(KeyDown(16) - KeyDown(18)) * 90 * Float(seconds), 0
    If KeyHit(15)
        view.shoulder = -view.shoulder
        Cam.RigView rig, view, 0.4D ; Glide over in 0.4 seconds.
    EndIf
    If KeyHit(57) Then Cam.ShakeRig rig, 0.5, 0.6D
    Cam.UpdateRig rig, seconds

    RenderWorld
    Text 12, 12, "Arrows: walk   Q/E: swing the camera   Tab: shoulder   Space: shake"
    If Cam.RigClearance(rig) = Cam.Clearance.Retracted Then Text 12, 36, "Moved in close to keep out of a block"
    Flip
Wend
Delete rig
Delete view
End

A rig joins a camera to the thing it follows. Each frame, the program moves the player first, passes on what the player asked the camera to do, and then calls UpdateRig with the seconds that have passed, before RenderWorld. RigObstacles tells the rig which physics world to look in, and from then on it keeps the camera out of everything there that has a shape: here, the blocks.

How do I...

Swing the camera round with the mouse?

RigOrbit turns the camera round the player, and up or down, by a number of degrees. Read the mouse every frame, and swing while the right button is down:

Local mouseDX:Int = MouseXSpeed()
Local mouseDY:Int = MouseYSpeed()
If MouseDown(2) Then Cam.RigOrbit rig, -mouseDX * 0.3, mouseDY * 0.3

The camera rises no more than 80 degrees above the player, and drops no more than 70 below, unless you change minimumPitch and maximumPitch.

Change to a wide view, smoothly?

Change the options you keep, and pass them to RigView with the number of seconds the change should take.

view.distance = 12          ; Twice as far back as usual.
Cam.RigView rig, view, 1D   ; Move back over a second.

Stop the camera swinging round when the player turns?

Use Orbit mode. In the usual Follow mode, the camera swings round to stay behind the player. In Orbit mode, it keeps looking from the same side, and only swings round when you swing it. That suits a player who can run in any direction.

view.mode = Cam.RigMode.Orbit
Cam.RigView rig, view

Stop the camera going through walls?

Give each wall a physics shape, and tell the rig which physics world they are in. If the player has a physics body of its own, pass that too, so that the camera doesn't stop at the player.

Cam.RigObstacles rig, world, -1, playerBody ; -1: shapes of every kind.

Shake the camera?

Say how far it may shake, in scene units, and for how many seconds. The shake dies away by the end. A new shake replaces the one before.

Cam.ShakeRig rig, 0.4, 0.7D

Move the player somewhere new without the camera swooping after it?

Snap the rig after the move. The camera jumps straight to its place behind the player.

PositionEntity player, 0, 1, 0 ; Back to the start.
Cam.SnapRig rig

Follow someone else for a while?

Give the rig a new target, and the camera glides over to it. A target of 0 leaves the camera where it is.

Cam.RigTarget rig, boss   ; Watch the boss...
Cam.RigTarget rig, player ; ...then go back to the player.

Blip's tip

The camera goes straight through walls until you call RigObstacles, and even then it only knows about things that have a physics shape. A cube without one is just a picture to it, so give each wall a body and a shape, as Block does in yard.bb.

Good to know

More examples

Four more complete programs come with the module:

The reference below has every function, option and rule.

Reference

Needs
blitz.random
Examples
advanced_views.bb, courtyard.bb, input_actions.bb, quick_start.bb

Unpublished 0.1.0 preview. Add an upright third-person view to an existing camera and target. The rig handles orbit, shoulders, half-life damping, physics clearance, parameter blends and seeded shake. Game code owns input, movement, physics stepping, rendering and camera creation.

Requires Extended object-lifecycle-v1 (runtime 1114 / module ABI 5 or later) and the accepted blitz.random 0.1.0 source package. No input/noise dependency. No files, windows, engine worlds or objects are created by importing the module.

Quick start

Blitz3D+ comes with this module and blitz.random, so any Extended program can Import blitz.cameras.

Dialect "modern"
Import blitz.cameras
Graphics3D 640, 480, 0, 2
Local camera:Int = CreateCamera()
Local target:Int = CreateCube()
Local rig:Cameras.Rig = Cameras.CreateRig(camera, target, Null)
Cameras.RigOrbit rig, 30, 5
Cameras.UpdateRig rig, 1D / 60D
RenderWorld
Flip
Delete rig
FreeEntity camera
FreeEntity target
EndGraphics
End

Null explicitly chooses defaults: optional object-Null defaults are not legal in the current Extended compiler. The first pose snaps into place. Thereafter update once after target movement/physics and before rendering. Supply finite, non-negative seconds, with the game's pause/time scale already applied. UpdateRig(rig, 0D) does not advance timers, but zero half-life changes and obstacle retraction still apply. No hidden input polling, clock, sleep, Flip, UpdateWorld or StepPhysics happens inside the rig.

Collision starts disabled. Render geometry alone does not block queries. Register physics shapes in the game's chosen physics world, then:

Cameras.RigObstacles rig, physicsWorld, -1, targetBody
Cameras.SnapRig rig

Those two handles must already exist and remain alive. The game explicitly identifies an ignored target body; the module does not infer bodies from entity hierarchies, create hidden colliders, or switch the active world. For multi-body characters, exclude their entire category layer using mask instead. Sensors are always excluded; both query/body category masks participate.

API and settings

DeclarationContract and example
RigMode.Follow, .OrbitFollow adds target world yaw to an orbit offset. Orbit retains absolute world yaw when the target turns. Target pitch/roll never tilt the horizon.
RigOptionsCaller-owned editable options; creation/view operations copy all fields. Local view:Cameras.RigOptions = New Cameras.RigOptions. Delete the record when no longer needed.
RigFactory-only owner with private state. Delete rig releases its resources and leaves borrowed entities, world, parenting and last applied pose intact.
CreateRig(camera:Int, target:Int, options:RigOptions):RigNonzero camera and target; explicit Null defaults. Cameras.CreateRig(camera, target, Null). Caller owns the result. Camera cannot target itself or a descendant of itself.
RigTarget(rig, target:Int)Borrow replacement; current camera pose is retained until update. Cameras.RigTarget rig, nextPlayer. 0 detaches and freezes the unshaken base; blend/shake time continues. Follow adopts a new attached target's heading; resuming a detached rig preserves its current world orbit heading.
RigView(rig, options, blendSeconds:Double=0D)Copy validated non-Null settings; linearly blend numeric fields over seconds. Cameras.RigView rig, view, 0.4D. Mode switches immediately while preserving world orbit heading; pitch clamps to the current interpolated limits. An interruption starts from current parameters. Does not immediately change the camera pose.
RigOrbit(rig, yawDelta:Float, pitchDelta:Float)Degree deltas, independent of input devices. Cameras.RigOrbit rig, -mouseDX * 0.3, mouseDY * 0.3. Initial pitch is 15 degrees clamped to limits. Yaw wraps; positive yaw turns toward -X; positive pitch raises the eye and points downward.
RigDistance(rig, distance:Float)Non-negative scene units. Cameras.RigDistance rig, 4. Overrides distance in an active blend without cancelling its other parameters; normal position damping remains.
RigObstacles(rig, physicsWorld:Int, mask:Int=-1, ignoredBody:Int=0)Borrow world/body, create one owned query, replace/free the previous query. Cameras.RigObstacles rig, world, 1, body. world=0 disables/frees it and requires ignoredBody=0. Ignored body must belong to that world. Disabling or deleting after the world was freed is safe; updating with clearance against a freed world is still an error.
ShakeRig(rig, strength:Float, seconds:Double, seed:Long=1L)Replace shake, bounded Euclidean positional amplitude in scene units, square decay envelope. Cameras.ShakeRig rig, 0.4, 0.7D, 42L. Zero strength or duration explicitly ends shake; next update restores the base. Does not destroy the rig.
SnapRig(rig)Finish an active view blend, clear shake and replace damping history with the desired pose. Cameras.SnapRig rig after a teleport. Detached rigs restore their frozen base. An embedded anchor still reports a clearance failure instead of tunnelling.
UpdateRig(rig, seconds:Double)Advance one rig, preserving elapsed hitches without iteration proportional to delta. Cameras.UpdateRig rig, seconds. Writes the borrowed camera's world pose.
RigClearance(rig):IntLast update/snap result, using Clearance below. No query is run by reading it. If Cameras.RigClearance(rig) = Cameras.Clearance.NoClearance Then ....

Operations without a documented result are procedures; the compiler represents them as default Int-returning functions. Do not interpret that default return as a success/failure result. All resource misuse is a contextual RuntimeError. There are no I/O or recoverable loading operations requiring LastError.

RigOptions fieldDefaultMeaning and accepted range
mode:IntRigMode.FollowExactly Follow or Orbit; enums are Int constants.
distance:Float6Anchor-to-eye boom length before shoulder offset; 0..100000 units.
targetHeight:Float1.5Offset above target position along world Y, -100000..100000 units.
shoulder:Float0Offset along yaw-relative right, -100000..100000 units. Aim stays on the target anchor.
minimumPitch, maximumPitch:Float-70, 80Ordered inclusive limits, each -89..89 degrees; avoids upright pole ambiguity.
positionHalfLife:Double0.12DSeconds to halve fixed-goal positional error; 0..1000000, zero immediate.
rotationHalfLife:Double0.08DSame for upright yaw/pitch aim. Shortest yaw arc, deterministic negative arc at exactly 180 degrees.
clearanceRadius:Float0.25Sphere radius, 0.001..10000 units, even while collision is disabled. Fit this to your camera's near-plane/FOV; the rig does not change projection.

All numbers must be finite. Yaw/pitch input deltas are bounded to ±1000000 degrees. Shake strength is 0..100000. Working world coordinates must stay within ±1000000 units; these generous bounds avoid overflowing Float engine geometry. Time and blend durations accept any finite non-negative Double. Extremely small half-lives saturate promptly; elapsed values are capped at their end before an addition can overflow.

Clearance, parenting and timing

Clearance.Disabled means no explicit world has been supplied (or a query was just reconfigured, before the next update). Clear means the constrained pose was clear, Retracted means at least one candidate required clipping. AnchorOverlapped means the target anchor is embedded: the last unshaken base is held, with no new shake. NoClearance means that held base is also overlapped, or the frozen base of a detached rig is overlapped. Move the game objects out of invalid geometry, then snap; these states do not pretend to solve penetration. A free destination beyond a wall is not proof of a clear path from an embedded anchor. General depenetration/path planning is intentionally not part of this rig.

For a clear anchor, sweep a sphere toward the desired position; damp the base; sweep that base again; finally add shake and sweep the result. This makes wall retraction immediate and release outward damped. Native queries take diameter and displacement, return 1-based hit records, and deliberately omit starting overlaps. The separate overlap query is essential. A 1% radius skin plus 0.001 units and a small travel margin avoid contact rounding. If an endpoint still overlaps, 16 bisections refine the already swept prefix from its clear anchor. The native query's 1024-overlap result capacity is retained; exceeding it is a runtime error, not silent missed collisions. Unregistered, filtered, sensor and moving geometry not yet synchronized to physics cannot obstruct the camera.

Camera parents are preserved. Global PositionEntity/RotateEntity setters use the runtime's inverse parent transform/rotation. Rotated/transformed parents with uniform positive scale work, including a scaled ancestor chain. Non-uniform, singular, reflected or skewed camera-parent world transforms are rejected on creation and every update. The camera itself should keep its ordinary unit scale. Target position and yaw are read in world space, including parented targets.

The base is stored independently of engine Float transforms and shake. Damping uses 1 - exp(-ln(2) * seconds / halfLife); a static endpoint is invariant under time subdivision to Float output tolerance. Moving targets and changing blend parameters are sampled once per update, so their trajectories converge with sample rate; they are not claimed bit-identical at different rates. Hitches do not add unbounded catch-up steps. Use SnapRig to reset after teleports.

Shake algorithm blitz-camera-shake-v1 draws three phase angles from the accepted PCG stream, then evaluates 11/13/17 Hz sine waves at accumulated Double time. The three axes share strength/sqrt(3) and a squared decay envelope. Equal seed, time and base pose give equal offsets within engine Float tolerance, independent of frame partitioning. A retrigger replaces the old stream/envelope; there is no stacked shake queue or noise dependency. The stream is deleted at completion. Collision can alter the final offset; no positional drift feeds back into base.

Ownership, mutation and failures

OwnerOwnedBorrowed and required lifetime
GameRig, options it creates, camera/target/physics sceneKeep all borrowed native handles alive until rig deletion or explicit replacement/disable.
RigThree copied settings records, four scratch points, at most one physics query and one random streamCamera, attached target, explicit physics world and ignored body. Never deleted or reparented by rig.
ExampleIts generated entities/world and typed ListsDelete each rig before cameras/targets/world. Deleting a List alone does not delete its entries.

Mutation occurs synchronously on the main thread. There are no callbacks, listeners, reentrancy phases, hidden registries or worker support. Changes to a caller's options after copying have no effect; use RigView. Game code should update one rig per camera; two rigs may freely share a target or world. Direct camera edits are overwritten at the next update. Detached rigs retain their query and constrain any remaining shake from the frozen base.

Delete invokes Method Delete once; deleted object aliases compare Null. Delete of typed Null is harmless, but every other rig operation rejects Null/deleted rigs. Leaving scope/assigning Null does not run destructors. Native handle validity is the caller's lifetime contract; no general stale entity-handle check is available. Query world/body validation uses the runtime's actual validity APIs. Destruction never advances, saves, commits or completes gameplay actions; ending/replacing a shake and detaching a target remain explicit operations.

Failure examples (run separately because RuntimeError ends the program):

Cameras.UpdateRig rig, -0.1D     ; seconds must be non-negative
Cameras.RigDistance rig, -3     ; distance out of range
Cameras.RigView rig, Null       ; options are Null or deleted
; New Cameras.Rig(...) and rig.query are compiler privacy errors.

Examples

Open an example and run it. Courtyard and advanced views Include examples/support/ beside them. Each accepts --verify, executes a fixed number of frames through its normal update path (24/36 at 1/12-second steps), releases resources, and prints PASS only after cleanup.

The optional input-actions example uses accepted blitz.inputactions public APIs: one capture shared by gameplay/menu/pointer, WASD/stick movement, Q/E/right-stick/right-mouse orbit, shoulder/shake/reset and pause with held controls suppressed until release. It creates no dependency in the camera core. General depenetration, arbitrary-roll cameras, rails, workers and stacked shake are outside this first release. See provenance and licence.