extends RigidBody3D # Initially I used a CharacterBody3D, however, I wanted the player to bounce off # other objects in the environment and that would have required manual handling # of collisions. So that's why we're using a RigidBody3D instead. const MOVE_SPEED := 8.0 const ACCELLERATION := 30.0 const DECELLERATION := 40.0 const JUMP_SPEED := 4.0 const MAX_NUMBER_OF_JUMPS := 2 const MIN_FOV := 10 const MAX_FOV := 180 const ZOOM_FACTOR := 1.1 # Zoom out when >1, in when < 1 var mouse_sensitivity := 0.005 var rotation_x := 0.0 var rotation_y := 0.0 var cameraMoveMode := false var current_number_of_jumps := 0 var _pending_mouse_delta := Vector2.ZERO var _last_move_forward := Vector3(0, 0, 1) var _last_move_right := Vector3(1, 0, 0) var _camera_offset_local := Vector3.ZERO var _camera_yaw := 0.0 var _camera_pitch := 0.0 var _in_vehicle := false var _vehicle_collision_layer := 0 var _vehicle_collision_mask := 0 var _vehicle_original_parent: Node = null var _light_was_on := false @onready var _flashlight: SpotLight3D = $SpotLight3D @export var camera_follow_speed := 10.0 var jump_sound = preload("res://assets/audio/jump.ogg") var audio_player = AudioStreamPlayer.new() @export var camera_path: NodePath @onready var cam: Camera3D = get_node(camera_path) if camera_path != NodePath("") else null @export var phone_path: NodePath @onready var phone: CanvasLayer = get_node(phone_path) if phone_path != NodePath("") else null var phone_visible := false func _ready() -> void: axis_lock_angular_x = true axis_lock_angular_z = true angular_damp = 6.0 contact_monitor = true max_contacts_reported = 4 add_child(audio_player) audio_player.stream = jump_sound audio_player.volume_db = -20 if cam: _camera_offset_local = cam.transform.origin _camera_pitch = cam.rotation.x _camera_yaw = global_transform.basis.get_euler().y cam.set_as_top_level(true) cam.global_position = global_position + (Basis(Vector3.UP, _camera_yaw) * _camera_offset_local) cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0) var move_basis := cam.global_transform.basis if cam else global_transform.basis var forward := move_basis.z var right := move_basis.x forward.y = 0.0 right.y = 0.0 if forward.length() > 0.0001: _last_move_forward = forward.normalized() if right.length() > 0.0001: _last_move_right = right.normalized() _vehicle_collision_layer = collision_layer _vehicle_collision_mask = collision_mask func _integrate_forces(state): if _in_vehicle: linear_velocity = Vector3.ZERO return if cameraMoveMode and _pending_mouse_delta != Vector2.ZERO: rotation_x -= _pending_mouse_delta.y * mouse_sensitivity rotation_y -= _pending_mouse_delta.x * mouse_sensitivity rotation_x = clamp(rotation_x, deg_to_rad(-90), deg_to_rad(90)) # Prevent flipping _camera_pitch = rotation_x rotation.y = rotation_y _pending_mouse_delta = Vector2.ZERO # Input as 2D vector var input2v := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") if Input.is_action_just_pressed("player_phone"): phone_visible = !phone_visible if phone: phone.visible = phone_visible # Camera based movement var forward := Vector3.FORWARD * -1.0 var right := Vector3.RIGHT if cam: forward = cam.global_transform.basis.z right = cam.global_transform.basis.x # Project onto ground plane so looking up/down doesn't kill movement. forward.y = 0.0 right.y = 0.0 if forward.length() > 0.0001: forward = forward.normalized() _last_move_forward = forward else: forward = _last_move_forward if right.length() > 0.0001: right = right.normalized() _last_move_right = right else: right = _last_move_right var dir := (right * input2v.x + forward * input2v.y).normalized() var target_v := dir * MOVE_SPEED var ax := ACCELLERATION if dir != Vector3.ZERO else DECELLERATION linear_velocity.x = move_toward(linear_velocity.x, target_v.x, ax * state.step) linear_velocity.z = move_toward(linear_velocity.z, target_v.z, ax * state.step) # Jump Logic var on_floor = false for i in state.get_contact_count(): var normal = state.get_contact_local_normal(i) if normal.y > 0.5: on_floor = true break if Input.is_action_just_pressed("ui_accept") and (on_floor or current_number_of_jumps == 1): current_number_of_jumps = (current_number_of_jumps + 1) % 2 linear_velocity.y = JUMP_SPEED audio_player.play() if cam: var target_yaw := global_transform.basis.get_euler().y _camera_yaw = lerp_angle(_camera_yaw, target_yaw, camera_follow_speed * state.step) var target_basis := Basis(Vector3.UP, _camera_yaw) var target_pos := global_position + (target_basis * _camera_offset_local) cam.global_position = cam.global_position.lerp(target_pos, camera_follow_speed * state.step) cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0) func _input(event): if _in_vehicle: return if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_MIDDLE: if event.pressed: cameraMoveMode = true Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) else: cameraMoveMode = false Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) if event is InputEventMouseMotion and cameraMoveMode: _pending_mouse_delta += event.relative if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: zoom_camera(1.0 / ZOOM_FACTOR) # Zoom in elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: zoom_camera(ZOOM_FACTOR) # Zoom out if event.is_action_pressed("player_light"): _flashlight.visible = !_flashlight.visible func zoom_camera(factor): var new_fov = cam.fov * factor cam.fov = clamp(new_fov, MIN_FOV, MAX_FOV) func enter_vehicle(_vehicle: Node, seat: Node3D, vehicle_camera: Camera3D) -> void: _in_vehicle = true freeze = true sleeping = true collision_layer = 0 collision_mask = 0 _vehicle_original_parent = get_parent() _light_was_on = _flashlight.visible _flashlight.visible = false if seat: reparent(seat, true) global_transform = seat.global_transform if cam: cam.current = false if vehicle_camera: vehicle_camera.current = true func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void: _in_vehicle = false freeze = false sleeping = false collision_layer = _vehicle_collision_layer collision_mask = _vehicle_collision_mask if _vehicle_original_parent: reparent(_vehicle_original_parent, true) _vehicle_original_parent = null _flashlight.visible = _light_was_on if exit_point: global_transform = exit_point.global_transform if vehicle_camera: vehicle_camera.current = false if cam: cam.current = true