Skip to content
Berke edited this page Oct 6, 2026 · 2 revisions

Camera

Inspector name: Camera · Lua: entity.camera · Public field type: Camera

The Camera is the eye the game is rendered through. It decides where the view is, which way it looks and how wide it sees. A level must have at least one camera to play. Without one the level refuses to start, and no scripts run.

The usual setup puts the Camera on the player entity, next to a Player Controller, which turns it with the mouse. A camera can also sit on any other entity: a security camera, a cutscene viewpoint, a spectator.

How it works

Only one camera renders

A level can contain many cameras, but only one is used at a time: the one with Is Active ticked.

  • Ticking a camera's Is Active switches to it and unticks every other camera, in the inspector or from a script (camera.isActive = true). Is Active always shows which camera is in use.
  • Unticking the active camera leaves no camera active, and nothing is drawn until a script ticks one.
  • Adding a Camera in the editor ticks it, so it takes over. A Camera added by a script (entity:AddComponent("Camera")) starts unticked.
  • A copied entity's camera starts unticked. The original stays active.

When the level starts:

  1. If several cameras were saved ticked, the first one keeps Is Active and the rest are unticked.
  2. If no camera has Is Active ticked, the first camera in the level is used.
  3. If the level has no camera at all, the level doesn't start.

Where the camera is

The view is placed at the camera entity's Transform position:

  • If the same entity also has an active Player Controller, the view is raised by the controller's Eye Height. Transform positions are at the feet, so this puts the eye at head height.
  • Otherwise the view is exactly at the Transform position. For a standalone camera, put its Transform at the height you want to see from.

The Transform's rotation is not used. The camera has its own direction.

Which way it looks

The direction comes from two angles, in degrees:

  • yaw turns left and right around the vertical axis. At 0 the camera looks along +z; in general it looks along x = sin(yaw), z = cos(yaw).
  • pitch tilts up and down. 0 is level, positive looks up and negative looks down.

From those the engine computes forward, a unit Vector3 pointing where the camera looks: (sin(yaw)·cos(pitch), sin(pitch), cos(yaw)·cos(pitch)). Use it to aim projectiles. For "what am I looking at", camera:Raycast() already shoots from the eye along this direction (see Scripting).

On screen, right is (-cos(yaw), 0, sin(yaw)). At yaw 0 that is -x, which is the way the player strafes with D.

While a Player Controller is active, the mouse changes its own camera's yaw and pitch every frame, but only while that camera is the active one. When another entity's camera is active (a cutscene or security camera), the mouse does nothing. The mouse adds to the current angles, so a script can still nudge them (recoil, screen shake) and the mouse carries on from there.

The audio listener, the "ears" for every Audio Source, is at the active camera, facing where it looks, so you hear from where you see.

Lens

  • fov is the vertical field of view in degrees. The horizontal view follows from the window's shape. Bigger values see more but make things look smaller and more stretched at the edges. 60 to 100 is the useful range.
  • nearPlane / farPlane are the closest and furthest distances that get drawn. Anything nearer than nearPlane or further than farPlane is cut off. Keep nearPlane small (the default 0.1 is fine) so walls don't vanish when you press your face against them.
  • aspectRatio is width divided by height. The engine sets it from the window size every frame, so changing it has no lasting effect.

Smooth stepping

When a physics body walks up a step, physics lifts it onto the step in a single frame. Without smoothing, the view would jump up. With Smooth Stepping on, the view glides up to the new height instead, and does the same when stepping down.

  • It only applies to a camera whose entity has a Collider with a Step Size above 0 and a grounded Rigidbody, which is how a player is normally set up.
  • Smoothing Strength sets how fast the view catches up. Each second it closes about 1 - e^(-strength) of the remaining gap, so 1 is a slow, floaty glide, 10 is quick and 20 and above is nearly instant.
  • Jumping and falling are never smoothed. Only step changes are.

In the editor

Field Lua Default Notes
Is Active isActive on See Only one camera renders.
FOV fov 90 Vertical field of view, degrees.
Aspect Ratio aspectRatio 1.75 Overwritten from the window size while playing.
Near Plane nearPlane 0.1
Far Plane farPlane 10000
Smooth Stepping on Editor only.
Smoothing Strength 1 Editor only.

yaw and pitch aren't shown in the inspector. They start at 0 (looking along +z, level) unless a script sets them.

Scripting

Property Type Description
isValid boolean read-only false if the entity or its Camera is gone.
isActive boolean read/write Whether this is the camera in use. Setting it to true switches to this camera and unticks the others.
yaw number read/write Degrees, left/right.
pitch number read/write Degrees, up/down.
fov number read/write Vertical field of view, degrees.
aspectRatio number read/write Overwritten every frame. Treat it as read-only.
nearPlane number read/write
farPlane number read/write
forward Vector3 read-only Unit look direction, updated once per rendered frame.
target Vector3 read-only Not kept up to date. Use forward.

forward is updated when the frame is drawn, which happens after scripts run. If you change yaw or pitch in Update and read forward straight away, you get last frame's direction. Compute it yourself if you need it at once:

local function Forward(yaw, pitch)
    local y, p = mathT.DegToRad(yaw), mathT.DegToRad(pitch)
    return Vector3(mathT.Sin(y) * mathT.Cos(p), mathT.Sin(p), mathT.Cos(y) * mathT.Cos(p))
end

The methods below don't have that delay: they read yaw and pitch when you call them.

Method Returns Description
Raycast([length], [requireCollider]) table or nil Shoots a ray from the eye through the middle of the screen (the crosshair), ignoring the camera's own entity. length defaults to farPlane and requireCollider to false. Returns the same hit table as Game.Raycast, or nil on a miss.
GetEyePosition() Vector3 Where the camera sees from: the Transform position, raised by Eye Height when the entity has an active Player Controller. See Where the camera is.
WorldToScreen(point) Vector2 or nil Where a world point shows up on screen, from (0, 0) top-left to (1, 1) bottom-right, the same space as UI anchors. Values outside 0–1 are off screen. nil when the point is behind the camera.
ScreenToRay(x, y) Vector3, Vector3 The ray through a screen point (same 0–1 space): returns origin, direction, ready for Game.Raycast. ScreenToRay(0.5, 0.5) is the ray Raycast uses.

Two details:

  • The eye is the body's eye. When Smooth stepping is gliding the view up a step, the drawn view is briefly a little lower than GetEyePosition().
  • Raycast skips the camera's own entity. That matters most with requireCollider = false, where every entity counts as a box the size of its Transform scale, and the eye is inside your own box.

Examples

Zoom while holding the right mouse button

-- Scripts/Camera/Zoom.lua (on the player)
public number zoomFov = 40
public number speed = 10

local normalFov

function Start()
    normalFov = entity.camera.fov
end

function Update()
    local camera = entity.camera
    local wanted = Input.GetMouseButton(Input.MouseRight) and zoomFov or normalFov
    camera.fov = mathT.Lerp(camera.fov, wanted, 1 - mathT.Exp(-speed * GameTime.deltaTime))
end

Screen shake

-- Scripts/Camera/Shake.lua (on the player)
-- Other scripts call entity:GetScript("Shake"):Shake(strength, seconds).
local strength = 0
local timeLeft = 0
local lastYaw, lastPitch = 0, 0

function Shake(self, amount, seconds)
    strength = amount
    timeLeft = seconds
end

function Update()
    local camera = entity.camera

    -- Undo last frame's offset so the shake never drifts the view.
    camera.yaw = camera.yaw - lastYaw
    camera.pitch = camera.pitch - lastPitch
    lastYaw, lastPitch = 0, 0

    if timeLeft <= 0 then return end
    timeLeft = timeLeft - GameTime.deltaTime

    lastYaw = mathT.RandomF(-strength, strength)
    lastPitch = mathT.RandomF(-strength, strength)
    camera.yaw = camera.yaw + lastYaw
    camera.pitch = camera.pitch + lastPitch
end

Shoot whatever is under the crosshair

-- Scripts/Camera/Shoot.lua (on the player)
public number range = 2000
public number damage = 25

function Update()
    if not Input.GetMouseButtonDown(Input.MouseLeft) then return end

    local hit = entity.camera:Raycast(range, true)
    if hit == nil or hit.entity == nil then return end

    local health = hit.entity:GetScript("Health")
    if health.isValid then health:TakeDamage(damage) end
end

A marker that follows an entity on screen

-- Scripts/Camera/Marker.lua (on a UI entity with a UITransform and a UISprite)
public Entity player
public Entity target

function Update()
    if player == nil or target == nil or not target.isValid then
        entity.uiSprite.isActive = false
        return
    end

    -- Aim at the target's head, not its feet.
    local point = target.transform.position + Vector3(0, target.transform.scale.y, 0)
    local screen = player.camera:WorldToScreen(point)

    entity.uiSprite.isActive = screen ~= nil
    if screen ~= nil then
        entity.uiTransform.anchorMin = screen
        entity.uiTransform.anchorMax = screen
    end
end

Switching cameras

-- Scripts/Camera/CameraSwitcher.lua
-- Press C to cycle through every entity tagged "camera".
local cameras = {}
local current = 1

function Start()
    for _, e in ipairs(Game.FindEntitiesWithTag("camera")) do
        if e.hasCamera then cameras[#cameras + 1] = e.camera end
    end
end

local function Use(index)
    cameras[index].isActive = true -- unticks every other camera
    current = index
end

function Update()
    if #cameras > 0 and Input.GetKeyDown(Key.C) then
        Use(current % #cameras + 1)
    end
end

Switching to a camera on another entity doesn't stop the Player Controller: WASD still moves the player along the player's own camera, but the mouse no longer looks around. To freeze the player during a cutscene, also set playerController.isActive = false, and turn it back on when you switch back.

Security camera that sweeps side to side

-- Scripts/Camera/Sweep.lua (on an entity with a Camera, placed high on a wall)
public number centerYaw = 0
public number range = 45
public number period = 6

local t = 0

function Start()
    entity.camera.pitch = -20
end

function Update()
    t = t + GameTime.deltaTime
    entity.camera.yaw = centerYaw + mathT.Sin(t / period * mathT.Tau) * range
end

More camera effects (FOV kick, head bob) are in docs/scripts/16_camera_effects.md.

Clone this wiki locally