Follow and orbit cameras for third-person games, with smoothing, shoulder views, obstacle clearance and shake.
Import blitz.cameras As Cam gives your program these, with Cam. in front. Each name leads to its part of the reference.
CreateRig:Rig(camera:Int, target:Int, options:RigOptions)RigTarget(rig:Rig, target:Int)RigView(rig:Rig, options:RigOptions, blendSeconds:Double = 0D)RigOrbit(rig:Rig, yawDelta:Float, pitchDelta:Float)RigDistance(rig:Rig, distance:Float)RigObstacles(rig:Rig, physicsWorld:Int, mask:Int = -1, ignoredBody:Int = 0)ShakeRig(rig:Rig, strength:Float, seconds:Double, seed:Long = 1L)SnapRig(rig:Rig)UpdateRig(rig:Rig, seconds:Double)RigClearance:Int(rig:Rig)RigOptionsmode:Int = RigMode.Follow, distance:Float = 6, targetHeight:Float = 1.5, shoulder:Float = 0, minimumPitch:Float = -70, maximumPitch:Float = 80, positionHalfLife:Double = 0.12D, rotationHalfLife:Double = 0.08D, clearanceRadius:Float = 0.25RigRigModeFollow, OrbitClearanceDisabled, Clear, Retracted, AnchorOverlapped, NoClearanceCameras 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.

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.
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 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.
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
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.
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
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
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, asBlockdoes in yard.bb.
RenderWorld. The rig never reads the keys, the mouse or the clock, and
moves the camera only when you call UpdateRig or SnapRig. Pass 0
seconds while the game is paused.distance, targetHeight and positionHalfLife in the options change
these.UpdateRig puts it
back where the rig wants it.Four more complete programs come with the module:
The reference below has every function, option and rule.
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.
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.
| Declaration | Contract and example |
|---|---|
RigMode.Follow, .Orbit | Follow adds target world yaw to an orbit offset. Orbit retains absolute world yaw when the target turns. Target pitch/roll never tilt the horizon. |
RigOptions | Caller-owned editable options; creation/view operations copy all fields. Local view:Cameras.RigOptions = New Cameras.RigOptions. Delete the record when no longer needed. |
Rig | Factory-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):Rig | Nonzero 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):Int | Last 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 field | Default | Meaning and accepted range |
|---|---|---|
mode:Int | RigMode.Follow | Exactly Follow or Orbit; enums are Int constants. |
distance:Float | 6 | Anchor-to-eye boom length before shoulder offset; 0..100000 units. |
targetHeight:Float | 1.5 | Offset above target position along world Y, -100000..100000 units. |
shoulder:Float | 0 | Offset along yaw-relative right, -100000..100000 units. Aim stays on the target anchor. |
minimumPitch, maximumPitch:Float | -70, 80 | Ordered inclusive limits, each -89..89 degrees; avoids upright pole ambiguity. |
positionHalfLife:Double | 0.12D | Seconds to halve fixed-goal positional error; 0..1000000, zero immediate. |
rotationHalfLife:Double | 0.08D | Same for upright yaw/pitch aim. Shortest yaw arc, deterministic negative arc at exactly 180 degrees. |
clearanceRadius:Float | 0.25 | Sphere 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.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.
| Owner | Owned | Borrowed and required lifetime |
|---|---|---|
| Game | Rig, options it creates, camera/target/physics scene | Keep all borrowed native handles alive until rig deletion or explicit replacement/disable. |
| Rig | Three copied settings records, four scratch points, at most one physics query and one random stream | Camera, attached target, explicit physics world and ignored body. Never deleted or reparented by rig. |
| Example | Its generated entities/world and typed Lists | Delete 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.
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.