Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

README.md

third_person_follow

Over-the-shoulder camera. The rig hangs off a shoulder pivot beside the follow target and sits back along the heading it looks down, so framing is authored as offsets in space. The offset is measured in the camera's own frame, so the subject holds the same spot on screen whichever way it turns.

The rig owns that heading. Binding a follow target aims it along the subject's facing, and look input turns it from there — the subject's facing is never read again. That is what makes the camera steerable for a character who turns to face where it is walking: hold "forward" and the character walks the line the camera looks down, instead of chasing a heading that moves as it turns.

The facing a bind reads comes from the body's own declaration of which way it faces (Locomotion.forward), so the rig lands behind a +Z-forward Synty body and a -Z-forward glTF body alike. A target that declares nothing falls back to its transform forward.

Public fields: follow, shoulderOffset (x across the frame, y up it, z along the heading), verticalArmLength, cameraSide (0 = left shoulder, 1 = right), cameraDistance, minDistance, maxDistance, aimYaw (the world heading the camera looks along, in radians about +Y from world +Z) and aimPitch (how far it tilts off level), damping (per-axis time constants in seconds of the camera's offset from its subject: they soften a change of framing, a look, a zoom or a pull-in, and the subject's own motion carries the camera at any speed), cameraRadius (radius of the pull-in sweep; 0 turns the pull-in off), cameraIgnore (entities whose colliders the pull-in passes through), sensitivity, invertY (off by default: pushing the mouse forward looks UP; on pitches the other way), zoomSpeed, minPitch, maxPitch.

Writing any framing field re-poses the rig immediately with damping bypassed, so edit mode shows the framing play mode will produce.

follow falls back to the Camera.follow slot when it is empty, so a rig selected through Camera.behavior tracks whatever that camera already follows.

Controls: mouse to look (play mode requests pointer lock, so no button is held), scroll to adjust cameraDistance.

The control scheme

inputMap is the scheme the rig answers to. It reads two controls from it, each on its own:

  • look — turns aimYaw and aimPitch.
  • zoom — steps cameraDistance.

A map declaring only one of the two gives the rig that one and keeps the other where it is; the rig logs which control the map left out, and names the map. A field naming no map leaves the rig answering nothing, and logs that too. Point inputMap at a world's own map to rebind looking and zooming without touching this component.

Repointing it on a live rig takes on the next frame. The map the rig was holding is stood down — its controls leave the live set and its touch buttons leave the screen — and the map named here is taken up in its place:

entity(rigId).component.get("third_person_follow").inputMap = asset.resolve("mygame.input.cinema", "inputMap")

Assigning the map the rig already holds changes nothing, so re-stating the scheme leaves the live set where it is. While gameplay is paused the rig holds no map at all — the editor's camera has the controls — and it takes whatever inputMap names when play resumes.

local cam = entity(id).component.get("Camera")
cam.behavior = asset.ref("@builtin::controller.third_person_follow", "component")
entity(id).component.get("controller.third_person_follow").cameraDistance = 3

The camera pulls in when a collider is in the way

Each frame the rig sweeps a sphere of cameraRadius from the point it aims at out to where its framing puts the camera, and stands the camera where the sphere first meets a collider. Every collider in the world stops that sweep, whether or not anything draws it: an invisible walk bound (a BoxCollider with no Model), a trigger volume and a solid wall all pull the camera in alike. The follow subject is the one entity the sweep passes by default.

So a camera that snaps in close to the player, or collapses onto it, while nothing visible stands behind the player is meeting a collider nobody can see. Two fields settle it:

  • cameraIgnore lists the entities whose colliders the camera sees through. Name the walk bounds and gameplay volumes there and the sweep passes them as it passes the subject, while walls the player can see still pull the camera in. Each entry is an entity (or its id), and it is the collider's own entity that is listed.
  • cameraRadius = 0 turns the pull-in off: the camera stands where its framing puts it, through every collider.
local rig = entity(camId).component.get("controller.third_person_follow")
rig.cameraIgnore = { entity.find("NorthBound"), entity.find("SouthBound") }
-- or drop the pull-in entirely
rig.cameraRadius = 0

Attach through Camera.behavior

The Camera owns the behavior slot, so assigning the rig there is what lets the Camera tell it when follow changes. A rig added straight onto the entity with component.add still poses, but the Camera does not know about it and a follow assigned afterwards will not reach it.

Assigning the slot attaches the rig to the camera's entity, so it is tuned on that instance: component.get("third_person_follow") and set its fields. A component.add("third_person_follow", { ... }) after the assignment, in a scene build.luau or anywhere else, writes its values onto the instance the slot attached and returns it, so the Camera keeps the one rig it knows about. In a build.luau that add is the way to tune the rig: its values are saved with the scene and the loaded rig holds them.

Player cameras: leave follow unset

Entering play drops and respawns PlayerPrototype subtrees, so the body is recreated with a NEW entity id. A follow authored on the rig keeps pointing at the original, which is no longer live, and the camera freezes at its last pose instead of tracking the player.

For a player camera, set the target on the Camera and leave the rig's own follow empty:

entity(rigId).component.get("Camera").follow = body   -- PlayerPrototype rebinds this on every spawn

PlayerPrototype refreshes Camera.follow to the live body each time it clones, and the rig reads that slot whenever its own follow is empty. Set the rig's follow only for a camera that watches a fixed, non-respawning subject.