94 lines
3.5 KiB
GDScript
94 lines
3.5 KiB
GDScript
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
|