Examples
Examples

First-person 3D camera and movement


Setup

This example shows how to build a simple first-person controller for a 3D scene. You can look around with the mouse and move on the XZ plane using the keyboard (WSAD).

What you’ll learn?

  • How to implement a FPP camera with mouse to look around.
  • How to lock/unlock the mouse cursor for immersive camera control.
  • How to move on a simple XZ plane logic with keyboard input.

Controls

| Input | Action | |

Scripts

character_controller.script

-- First-person 3D camera controller

-- Tuning parameters
local look_sensitivity = 0.15 -- degrees of camera rotation per 1 pixel of mouse movement
local move_speed = 0.5        -- world units per second for camera movement on XZ plane
local move_limit = 1.25       -- bounds (half-size) for camera movement on XZ to keep it in a square area

function init(self)
    -- Acquire input focus to receive input events from the engine
    msg.post(".", "acquire_input_focus")

    -- Mouse lock state: when true, mouse deltas rotate the camera
    self.mouse_locked = false

    -- Initialize yaw/pitch from current rotation (stored in degrees in Defold)
    self.yaw = go.get(".", "euler.y")
    self.pitch = go.get(".", "euler.x")

    -- Input state for continuous movement (WASD)
    self.input = {
        forward = false,
        backward = false,
        left = false,
        right = false,
    }
end

function update(self, dt)
    -- Clamp pitch to avoid flipping the camera upside down
    if self.pitch > 89 then self.pitch = 89 end
    if self.pitch < -89 then self.pitch = -89 end

    -- Apply rotation directly via Euler angles (in degrees)
    go.set(".", "euler", vmath.vector3(self.pitch, self.yaw, 0))

    -- Build desired movement direction on XZ plane from input flags
    local x = (self.input.right and 1 or 0) - (self.input.left and 1 or 0)
    local z = (self.input.backward and 1 or 0) - (self.input.forward and 1 or 0)

    -- If there is any movement input, move the camera
    if x ~= 0 or z ~= 0 then
        -- Local space direction (camera space)
        local local_dir = vmath.vector3(x, 0, z)
        local len = math.sqrt(local_dir.x * local_dir.x + local_dir.z * local_dir.z)

        if len > 0 then
            -- Normalize to keep speed consistent diagonally
            local_dir.x = local_dir.x / len
            local_dir.z = local_dir.z / len

            -- Convert the yaw to a quaternion
            local q_yaw = vmath.quat_rotation_y(math.rad(self.yaw))

            -- Convert local movement to world space using current yaw
            local world_dir = vmath.rotate(q_yaw, local_dir)

            -- Get the current position of the character
            local pos = go.get_position()

            -- Integrate the position
            pos.x = pos.x + world_dir.x * move_speed * dt
            pos.z = pos.z + world_dir.z * move_speed * dt

            -- Clamp the position within the square bounds
            if pos.x > move_limit then pos.x = move_limit end
            if pos.x < -move_limit then pos.x = -move_limit end
            if pos.z > move_limit then pos.z = move_limit end
            if pos.z < -move_limit then pos.z = -move_limit end

            -- Set the new position
            go.set_position(pos)
        end
    end
end

-- Pre-hashed input action ids (must match project input bindings)
local KEY_W = hash("key_w")
local KEY_S = hash("key_s")
local KEY_A = hash("key_a")
local KEY_D = hash("key_d")
local KEY_ESC = hash("key_esc")
local TOUCH = hash("touch")
local MOUSE_BUTTON_1 = hash("mouse_button_1")

function on_input(self, action_id, action)
    -- Mouse look when locked: engine provides action.dx/dy even while cursor is locked
    if self.mouse_locked and (action.dx or action.dy) then
        -- Rotate the camera based on the mouse movement
        self.yaw = self.yaw - (action.dx or 0) * look_sensitivity
        self.pitch = self.pitch + (action.dy or 0) * look_sensitivity
    end

    -- Lock on first click (touch or left mouse button)
    if not self.mouse_locked and action.pressed
        and (action_id == TOUCH or action_id == MOUSE_BUTTON_1) then
        -- Lock the mouse
        window.set_mouse_lock(true)
        self.mouse_locked = true
    end

    -- WSAD - Continuous movement input state (pressed/released)
    if action_id == KEY_W then
        -- Set the forward input flag to true if the W key is pressed
        if action.pressed then self.input.forward = true end
        if action.released then self.input.forward = false end
    end
    if action_id == KEY_S then
        -- Set the backward input flag to true if the S key is pressed
        if action.pressed then self.input.backward = true end
        if action.released then self.input.backward = false end
    end
    if action_id == KEY_A then
        -- Set the left input flag to true if the A key is pressed
        if action.pressed then self.input.left = true end
        if action.released then self.input.left = false end
    end
    if action_id == KEY_D then
        -- Set the right input flag to true if the D key is pressed
        if action.pressed then self.input.right = true end
        if action.released then self.input.right = false end
    end

    -- ESC unlocks the mouse so the cursor is free again
    if action_id == KEY_ESC and action.pressed then
        -- Unlock the mouse
        window.set_mouse_lock(false)
        self.mouse_locked = false
    end
end

This example was created by Defold Foundation.

DOWNLOAD

 

Do you want to see more examples? Why not write a few yourself and submit a pull request? We love contributions.

GITHUB