121 lines
6.0 KiB
GDScript
121 lines
6.0 KiB
GDScript
class_name Player
|
||
extends CharacterBody3D
|
||
## 玩家协调器:只做三件事 —— 多人权限、依赖注入、按固定顺序编排各组件。
|
||
##
|
||
## 具体能力全在子节点组件里:
|
||
## Look 视角 character/player/player_look.gd
|
||
## Motor 移动跳跃 character/player/player_motor.gd
|
||
## Pickup 拾取 character/player/player_pickup.gd
|
||
## Animator 动画表现 character/player/player_animator.gd(挂在 AnimationPlayer 上)
|
||
## HSM 状态机 character/player/hsm/(LimboAI LimboHSM,MANUAL 模式)
|
||
##
|
||
## 本脚本不实现任何操作,只负责「谁先谁后」和「谁有权跑」。
|
||
## 「玩家处于什么状态」(走 / 跑 / 蹲 / 跳 / 落)的判定完全在 HSM 里:
|
||
## 状态在 _enter 时写 motor.target_speed、motor.crouching/sprinting 和动画片段,
|
||
## 这里只按固定顺序推进各组件。
|
||
|
||
@onready var body: Node3D = $Body
|
||
## 本地玩家自己的模型(鼠)。第一人称要藏掉它,见 _ready()。
|
||
@onready var model: Node3D = get_node_or_null("Body/mouse") as Node3D
|
||
@onready var camera_rig: Node3D = $CameraRig
|
||
@onready var camera: Camera3D = $CameraRig/Camera
|
||
## 准心(挂在 CameraRig/HUD 下)。远端玩家的 HUD 会随 camera_rig 一起被释放。
|
||
@onready var crosshair: Crosshair = $CameraRig/HUD/Crosshair
|
||
## 右上角调试面板。同样挂在 HUD 下,所以远端玩家不会显示它。
|
||
@onready var debug_panel: DebugPanel = $CameraRig/HUD/DebugPanel
|
||
@onready var animation_player: PlayerAnimator = $AnimationPlayer
|
||
@onready var look: PlayerLook = $Look
|
||
@onready var motor: PlayerMotor = $Motor
|
||
@onready var pickup: PlayerPickup = $Pickup
|
||
## LimboAI 层级状态机。update_mode = MANUAL,由 _physics_process 按管线顺序显式推进,
|
||
## 保证它永远排在 motor.step 之前、且只在权威端跑。
|
||
@onready var hsm: LimboHSM = $HSM
|
||
|
||
|
||
func _ready() -> void:
|
||
# 远端玩家只是一具「回放身体」:
|
||
# 拆掉相机 —— 否则每多一个玩家就多一台 Camera3D 抢渲染;
|
||
# 关掉输入与物理 —— 否则与权威端双重模拟,表现为抖动、漂移、穿地。
|
||
# 它的 position 与 Body.rotation 由 Sync 子节点同步;血条由 health 同步。
|
||
#
|
||
# 注意:输入已经分散到各组件里,所以这里必须把组件也一起关掉 ——
|
||
# 只关自己是不够的,否则远端玩家照样读本机输入。
|
||
if not is_multiplayer_authority():
|
||
camera_rig.queue_free()
|
||
set_physics_process(false)
|
||
set_process_unhandled_input(false)
|
||
look.set_process_unhandled_input(false)
|
||
motor.set_process_unhandled_input(false)
|
||
pickup.set_process_unhandled_input(false)
|
||
return
|
||
|
||
# 第一人称:藏掉自己的模型。
|
||
# 摄像机的近裁剪面被调小了(见 player.tscn 的 near = 0.01),已经盖不住自己的身体 ——
|
||
# 不藏就会从内部看到模型的内壁。远端玩家的模型是各自独立的实例,不受影响。
|
||
if model != null:
|
||
model.visible = false
|
||
|
||
# 依赖注入:组件不自己去 get_node,由这里接线。
|
||
# 好处是组件不认识玩家的场景结构,换场景 / 单测时注入别的节点即可。
|
||
look.setup(camera_rig, camera, body)
|
||
motor.setup(self, camera_rig)
|
||
pickup.setup(
|
||
camera.get_node("InteractionRay") as RayCast3D,
|
||
camera.get_node("InteractionPos") as Marker3D,
|
||
self
|
||
)
|
||
# 准心接线:拾取组件一发现交互状态变化,就通知准心换外观(点 / 手 / 抓)。
|
||
pickup.interact_state_changed.connect(_on_pickup_interact_state_changed)
|
||
# 调试面板接线:把要读的组件注入进去,面板自己不 get_node。
|
||
debug_panel.setup(self, motor, pickup, camera)
|
||
# 状态机接线:initialize 递归调所有状态的 _setup(注册各自的转换表),
|
||
# set_active 进入 initial_state(Grounded 会按当下输入挑子状态)。
|
||
# 只在权威端跑 —— 远端玩家由 Sync 复制 position / anim_state / anim_speed,
|
||
# 不需要也不允许本地再跑一套状态机;HSM 是 MANUAL 模式,不初始化就完全惰性。
|
||
hsm.initialize(self)
|
||
hsm.set_active(true)
|
||
set_mouse_captured(true)
|
||
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
# 这里只留「跨组件的全局开关」,具体操作归各组件自己处理。
|
||
if event.is_action_pressed("ui_cancel"):
|
||
set_mouse_captured(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED)
|
||
elif event is InputEventMouseButton and event.pressed \
|
||
and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
|
||
set_mouse_captured(true)
|
||
# 吃掉这一下点击:它的意义只是「把鼠标锁回游戏」,
|
||
# 不该顺带触发拾取 —— 否则重新聚焦时会凭空抓起一个物体。
|
||
get_viewport().set_input_as_handled()
|
||
|
||
|
||
func set_mouse_captured(captured: bool) -> void:
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE
|
||
|
||
|
||
## 把拾取组件的两个布尔量收敛成准心的三态。优先级:抓着 > 指着 > 没有。
|
||
## 系统鼠标指针由准心自己同步(Crosshair.sync_os_cursor),这里不用管。
|
||
func _on_pickup_interact_state_changed(hovering: bool, holding: bool) -> void:
|
||
var state := Crosshair.GrabState.NONE
|
||
if holding:
|
||
state = Crosshair.GrabState.HOLDING
|
||
elif hovering:
|
||
state = Crosshair.GrabState.HOVER
|
||
crosshair.set_grab_state(state)
|
||
|
||
|
||
func _physics_process(delta: float) -> void:
|
||
# 顺序固定:状态机(判定状态,写目标速度与动画)→ 算速度 → 同步蹲伏高度 →
|
||
# 推进 → 刷步频 → 搬物体。由根节点统一编排,各组件自己不许写 _physics_process ——
|
||
# 否则顺序取决于节点树顺序,改个节点位置就会出现「先滑再算速度」这类隐形 bug。
|
||
# HSM 排最前:状态写入的 target_speed 当帧就被 motor 消费;
|
||
# 起跳在 Grounded 状态里就地执行(motor.try_jump),同一帧转进 Jump。
|
||
hsm.update(delta)
|
||
motor.step(delta)
|
||
# 蹲伏事实由 HSM 的 Crouch/Sneak 状态写进 motor.crouching ——
|
||
# 不让 Look 自己去读蹲键,免得两边各判一次、将来不同步。
|
||
look.set_crouching(motor.crouching)
|
||
move_and_slide()
|
||
animation_player.set_gait(velocity)
|
||
pickup.physics_step()
|