class_name FPVController extends CharacterBody3D @export var move_speed: float = 3.0 @export var mouse_sensitivity: float = 0.0025 @export var gravity: float = 9.8 @onready var camera: Camera3D = $Camera3D @onready var interact_ray: RayCast3D = $Camera3D/InteractRay signal interactable_focused(obj: Node) signal interactable_unfocused signal interact_pressed(obj: Node) var _captured: bool = false var _locked: bool = false var _focused_obj: Node = null func _ready() -> void: _capture_mouse() func _capture_mouse() -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED _captured = true func _release_mouse() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _captured = false func _input(event: InputEvent) -> void: if event is InputEventMouseMotion and _captured and not _locked: rotate_y(-event.relative.x * mouse_sensitivity) camera.rotate_x(-event.relative.y * mouse_sensitivity) camera.rotation.x = clamp(camera.rotation.x, -PI * 0.45, PI * 0.45) elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if not _captured: _capture_mouse() return if _locked: return if _focused_obj != null: interact_pressed.emit(_focused_obj) if _focused_obj.has_method("interact"): _focused_obj.interact(self) elif event is InputEventKey and event.pressed: if event.keycode == KEY_ESCAPE: if _captured: _release_mouse() else: _capture_mouse() elif event.keycode == KEY_Q: get_tree().quit() func _physics_process(delta: float) -> void: if _locked: velocity = Vector3.ZERO move_and_slide() _update_focus() return var input_dir: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_back") var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0.0, input_dir.y)).normalized() velocity.x = direction.x * move_speed velocity.z = direction.z * move_speed if not is_on_floor(): velocity.y -= gravity * delta else: velocity.y = 0.0 move_and_slide() _update_focus() func _update_focus() -> void: interact_ray.force_raycast_update() var new_focus: Node = null if interact_ray.is_colliding(): var col: Object = interact_ray.get_collider() if col != null and col is Node and col.has_method("get_prompt"): new_focus = col if new_focus != _focused_obj: if new_focus != null: interactable_focused.emit(new_focus) else: interactable_unfocused.emit() _focused_obj = new_focus func lock_input() -> void: _locked = true func unlock_input() -> void: _locked = false func get_focused() -> Node: return _focused_obj