class_name PlayerLook extends Node ## 视角组件:把鼠标位移转成 CameraRig 的 yaw 与 Camera 的 pitch, ## 并按蹲伏状态平滑升降摄像机。只管摄像机 —— 不碰速度、不碰动画。 ## ## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 调 setup() 注入依赖。 @export_group("视角") ## 鼠标灵敏度(弧度/像素) @export var mouse_sensitivity: float = 0.0025 ## 最低俯角(向下看) @export var pitch_min_deg: float = -89.0 ## 最高仰角(向上看) @export var pitch_max_deg: float = 89.0 @export_group("蹲伏") ## 蹲下时摄像机高度 = 站立高度 × 这个比例 @export_range(0.3, 1.0, 0.05) var crouch_height_ratio: float = 0.6 ## 蹲起过渡速度。这是「每秒收敛掉的比例」,不是米/秒 —— 越大越干脆。 @export var crouch_transition_speed: float = 10.0 ## 眼睛分两层转:CameraRig 绕竖直轴转(yaw),Camera 原地抬头低头(pitch)。 ## 分开是为了让 pitch 不掺进移动方向 —— 移动只用 rig 的 basis,低头看地也不会走歪。 var _yaw: float = 0.0 var _pitch: float = 0.0 var _rig: Node3D var _camera: Camera3D var _body: Node3D ## 站立时 CameraRig 的本地高度(从场景读,不写死),以及当前要收敛到的高度。 var _stand_height: float = 0.0 var _target_height: float = 0.0 func _ready() -> void: # 依赖是 setup() 注入的 —— 没接线(远端玩家不会调 setup)就不该跑 _process。 set_process(false) ## 由 Player 在 _ready() 注入依赖。组件不自己 get_node,才能脱离具体场景复用。 func setup(rig: Node3D, camera: Camera3D, body: Node3D) -> void: _rig = rig _camera = camera _body = body _yaw = rig.rotation.y _pitch = clampf(camera.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg)) # 站立高度直接读场景里的 CameraRig —— 以后在编辑器里把它挪高挪低,这里自动跟上。 _stand_height = rig.position.y _target_height = _stand_height _apply() set_process(true) func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: _yaw -= event.relative.x * mouse_sensitivity _pitch = clampf( _pitch - event.relative.y * mouse_sensitivity, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg) ) _apply() ## 由 Player 每物理帧调用,把「是否蹲着」这个事实转成目标高度。 ## 只在目标真的变了才写入,避免每帧无谓赋值。 func set_crouching(crouching: bool) -> void: if _rig == null: return var target: float = _stand_height * crouch_height_ratio if crouching else _stand_height if is_equal_approx(target, _target_height): return _target_height = target ## 蹲起过渡放在渲染帧而不是物理帧 —— 物理帧只有 60Hz,看起来会一顿一顿。 ## 用指数收敛(1 - e^(-k·delta))而不是定量 lerp:过渡时间不受帧率影响。 func _process(delta: float) -> void: if _rig == null: return var t := 1.0 - exp(-crouch_transition_speed * delta) _rig.position.y = lerpf(_rig.position.y, _target_height, t) ## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角跟手、不等物理帧。 ## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。 func _apply() -> void: if _rig == null: return _rig.rotation.y = _yaw _camera.rotation.x = _pitch _body.rotation.y = _yaw