-- 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