Skip to content

Cameras and controls

A camera decides where the scene is seen from. A camera rig moves it with the mouse and follows a target; movement control turns keys into player motion.

components: camera · camera_rig · movement_control · spin_control Desktop · Android · iOS · Web

A scene document and the scene DSL are two ways to write the same ECS components. Both tabs below build a player and a third-person camera that follows it. camera is in awake:scene:scene3d; camera_rig and movement_control, and the systems that drive them, are in com.awakekt.awake.scene:controls, which com.awakekt.awake.scene:authoring brings with it.

Add a follow camera

Give the camera entity a camera for the lens and a camera_rig for how it moves. The rig's target names the node it follows; the player's movement_control marks it as the entity the keys move.

follow-camera.scene.json
{
  "version": 1,
  "nodes": [
    {
      "name": "player",
      "transform": {
        "position": { "x": 0.0, "y": 0.5, "z": 0.0 }
      },
      "components": [
        { "component": "movement_control", "speed": 6.0 }
      ]
    },
    {
      "name": "camera",
      "components": [
        {
          "component": "camera",
          "eye": { "x": 0.0, "y": 4.0, "z": 8.0 },
          "center": { "x": 0.0, "y": 1.0, "z": 0.0 },
          "fovYDegrees": 60.0,
          "near": 0.1,
          "far": 500.0
        },
        {
          "component": "camera_rig",
          "mode": "ThirdPerson",
          "target": "player",
          "distance": 8.0,
          "pitch": -0.3,
          "offset": { "x": 0.0, "y": 1.5, "z": 0.0 }
        }
      ]
    }
  ]
}
Kotlin
world.scene {
    val player = entity("player") {
        transform(y = 0.5f)
        with(MovementControl().apply { speed = 6f })
    }
    entity("camera") {
        transform()
        camera(
            mode = CameraMode.ThirdPerson,
            target = player,
            lens = Lens.perspective(
                eye = Vec3f(0f, 4f, 8f),
                center = Vec3f(0f, 1f, 0f),
                fovYDegrees = 60f,
                far = 500f,
            ),
        ) {
            distance = 8f
            pitch = -0.3f
            offsetPosition.set(0f, 1.5f, 0f)
            needsReset = false // keep the pitch above; see the warning below
        }
    }
}

camera() adds both Camera and CameraRig. MovementControl has no helper, so with adds it.

  1. Select the camera entity in the Hierarchy.
  2. In the Inspector, the Camera section sets Field of View, Near and Far. Viewport Preview turns on a preview from this camera.

Studio has no Add component entry for camera_rig or movement_control. Add them in the scene document.

Load it

camera_rig and movement_control are not among the built-in components, so register them before decoding. registerControls() adds both to a registry you pass to instantiate:

Kotlin
DefaultSceneComponentResolvers.install()
val registry = SceneComponentRegistry().registerControls()
val document = SceneLoader.decode(json)
val loaded = SceneLoader.instantiate(document, World(), registry).world

Make it move

Components hold settings; systems move things. Register the control systems in the scene, and tag the camera that takes input with ActiveCamera:

Kotlin
DefaultSceneComponentResolvers.install()
SceneComponentRegistry.registerGlobal(MovementControlBinding)
SceneComponentRegistry.registerGlobal(CameraRigBinding)

val game = app {
    ecs {
        scene(SceneLoader.decode(json))
        playerInputSystem() // keys to MovementControl
        matrixRelativeMovementSystem() // MovementControl to Transform, camera-relative
        cameraSystem() // CameraRig to Camera, on the entity tagged ActiveCamera
        onReady { world.add(requireEntity("camera"), ActiveCamera()) }
    }
}

scene(document) inside an app loads with the global registry, which is why this sample uses registerGlobal instead of registerControls().

Helper in scene { } System Reads Writes
playerInputSystem() PlayerInputSystem The scene's move, jump and run input actions: by default W A S D or the arrow keys, Space, and Shift held MovementControl.moveX, moveZ, jump, run on every entity whose driver is Player
matrixRelativeMovementSystem(speed = 5f) MatrixRelativeMovementSystem MovementControl, the active camera's view Transform.position, relative to where the camera looks
cameraSystem() CameraSystem mouse drag and scroll, the rig's target the Camera lens of the entity tagged ActiveCamera
cameraInputSystem() CameraInputSystem F1, F2, F4, F5 the active rig's mode

All four are frame systems. The ones that read input get it with whatever the UI claimed removed, so typing in a text field does not walk the player. With no ActiveCamera, movement treats -Z as forward.

Properties

camera

Property Type Default What it does
eye vector (0, 0, 5) Where the camera is.
center vector (0, 0, 0) The point it looks at.
up vector (0, 1, 0) Which way is up.
fovYDegrees number 60 in a scene document; 45 from Lens.perspective Vertical field of view, between 0 and 180.
near number 0.1 Near clip distance, greater than 0.
far number 100 Far clip distance, greater than near.
primary boolean true Whether this camera renders the scene.
projection perspective · orthographic perspective orthographic keeps things the same size at any distance and ignores fovYDegrees; use it for 2D.
orthoHalfHeight number about 2.07 Half the vertical world extent an orthographic view covers; the width follows from the aspect ratio. Used only when projection is orthographic.

camera_rig

Property Type Default What it does
mode FirstPerson · ThirdPerson · FreeFly · Cinematic · TopDown ThirdPerson in a scene document; FirstPerson from camera() How the camera moves; see below.
target node name none The node the camera follows.
distance number 5 How far a third-person or top-down camera sits from its pivot.
minDistance, maxDistance number 2, 20 The range scrolling zooms within. distance must lie inside it.
pitch, yaw radians 0 Starting angles. Negative pitch looks down.
offset vector (0, 1.8, 0) Added to the target's position to get the pivot, or the pivot itself with no target.
flySpeed number 10 Units per second for FreeFly. Greater than 0.

In the scene DSL these are CameraRig fields; target is targetEntity and offset is offsetPosition.

Mode Needs a target What it does
FirstPerson yes Eye at the target plus offset; dragging turns yaw and pitch.
ThirdPerson no Orbits the pivot at distance; dragging orbits, scrolling zooms. The eye eases toward its pose.
FreeFly no W A S D move along the view, Q and E down and up, Shift is faster; dragging looks around. A host rebinds the keys with CameraGesturePolicy(flyActions = ...), named as in CameraFlyActions.
Cinematic yes Looks at the target from a fixed spot; no input.
TopDown no Looks down at 60 degrees from distance; dragging turns yaw, scrolling zooms.

movement_control

Property Type Default What it does
speed number or none none Units per second for this entity. None uses the movement system's speed.

spin_control

Property Type Default What it does
radians number 0 The current angle about Y.
speed number 1 Turn rate, for the system that advances radians. Not negative.

SpinSystem copies radians to Transform.rotation.y; it does not advance it. Advance it in your own system, as Scene DSL shows.

How it works

CameraSystem runs every frame. It reads the pointer drag and scroll, updates the rig's yaw, pitch and distance, then writes the lens eye and center from the rig, the target's Transform, and the mode. Only the entity tagged ActiveCamera takes input.

A rig's target is resolved after the whole document is loaded, so the camera may come before the node it follows. A target name that no node has stops the load with an error.

Tag the active camera

Neither a scene document nor camera() adds ActiveCamera. Without it CameraSystem leaves the camera where it is. Add it in onReady, as above.

A DSL rig that sets mode resets its angles

Changing CameraRig.mode schedules a reset that replaces pitch, yaw and distance with the mode's defaults on the next CameraSystem update. camera(mode = …) changes the mode, so set needsReset = false in its block to keep angles you set. A loaded camera_rig already keeps them.

MatrixRelativeMovementSystem goes through walls

It writes Transform.position directly. For an entity that should collide, use a character controller instead, and don't run both on the same entity.

F2 does two things

With cameraInputSystem() registered, F2 switches the rig to ThirdPerson, and the scene runtime also uses F2 to toggle its frame timing stats. Pass your own modeKeys map to CameraInputSystem to move it.

See also