103 lines
4.0 KiB
GDScript
103 lines
4.0 KiB
GDScript
extends Node3D
|
|
class_name FPSCameraRig
|
|
|
|
## FPS camera rig — handles mouse look, head bob, FOV kick, wall-run tilt.
|
|
## Attach as child of the CharacterBody3D player. Camera3D is a child of this node.
|
|
|
|
@export var sensitivity: float = 0.002
|
|
@export var pitch_limit: float = 89.0
|
|
|
|
var params: MovementParams
|
|
var camera: Camera3D
|
|
var _bob_timer: float = 0.0
|
|
var _target_tilt: float = 0.0
|
|
var _current_tilt: float = 0.0
|
|
var _target_fov: float = 90.0
|
|
var _weapon_fov_override: float = -1.0
|
|
|
|
|
|
func _ready() -> void:
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
camera = get_node_or_null("Camera3D")
|
|
if camera and params:
|
|
camera.fov = params.base_fov
|
|
_target_fov = params.base_fov
|
|
# Explicitly enable callbacks — set_script() at runtime doesn't auto-register them
|
|
set_process_input(true)
|
|
set_process(true)
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
|
var is_ads = camera and camera.fov < params.base_fov - 5.0
|
|
var sens = SettingsManager.ads_sensitivity if is_ads else SettingsManager.mouse_sensitivity
|
|
# Yaw: rotate the player (parent)
|
|
get_parent().rotate_y(-event.relative.x * sens)
|
|
# Pitch: rotate this pivot
|
|
rotation.x = clampf(rotation.x - event.relative.y * sens, deg_to_rad(-pitch_limit), deg_to_rad(pitch_limit))
|
|
|
|
# Toggle mouse capture with Escape
|
|
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
|
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
else:
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not camera or not params:
|
|
return
|
|
|
|
var player: CharacterBody3D = get_parent() as CharacterBody3D
|
|
if not player:
|
|
return
|
|
|
|
# ── FOV kick ──────────────────────────────────────────────────────────
|
|
_target_fov = params.base_fov
|
|
var hspeed := Vector2(player.velocity.x, player.velocity.z).length()
|
|
|
|
# Smoothly scale FOV based on speed above walking speed
|
|
var speed_factor = clampf((hspeed - params.walk_speed) / (params.walk_speed * 0.5), 0.0, 1.0)
|
|
_target_fov = lerpf(params.base_fov, params.dash_fov, speed_factor)
|
|
|
|
if _weapon_fov_override > 0.0:
|
|
_target_fov = _weapon_fov_override
|
|
|
|
camera.fov = lerpf(camera.fov, _target_fov, 1.0 - exp(-params.fov_lerp_speed * delta))
|
|
|
|
# ── Head bob ──────────────────────────────────────────────────────────
|
|
var sm = player.get_node_or_null("MovementStateMachine")
|
|
var is_sliding = sm and sm.current_state == "slide"
|
|
|
|
if player.is_on_floor() and hspeed > 1.0 and not is_sliding:
|
|
_bob_timer += delta * params.head_bob_frequency * (hspeed / params.walk_speed)
|
|
var bob_y := sin(_bob_timer) * params.head_bob_amplitude
|
|
var bob_x := cos(_bob_timer * 0.5) * params.head_bob_amplitude * 0.5
|
|
camera.position.y = lerp(camera.position.y, bob_y, 0.3)
|
|
camera.position.x = lerp(camera.position.x, bob_x, 0.3)
|
|
else:
|
|
_bob_timer = 0.0
|
|
camera.position.y = lerp(camera.position.y, 0.0, 0.15)
|
|
camera.position.x = lerp(camera.position.x, 0.0, 0.15)
|
|
|
|
# ── Wall-run tilt ─────────────────────────────────────────────────────
|
|
_current_tilt = lerpf(_current_tilt, _target_tilt, 1.0 - exp(-params.wall_run_tilt_speed * delta))
|
|
camera.rotation.z = deg_to_rad(_current_tilt)
|
|
|
|
|
|
## Called by the movement system to set wall-run tilt direction.
|
|
## side: -1.0 (left wall), 1.0 (right wall), 0.0 (no tilt)
|
|
func set_wall_tilt(side: float) -> void:
|
|
if params:
|
|
_target_tilt = side * params.wall_run_tilt_angle
|
|
else:
|
|
_target_tilt = side * 12.0
|
|
|
|
func set_weapon_fov(fov: float) -> void:
|
|
_weapon_fov_override = fov
|
|
|
|
|
|
## Called to reset tilt (e.g., on landing).
|
|
func clear_wall_tilt() -> void:
|
|
_target_tilt = 0.0
|