Files
2026-10-15 16:56:45 +08:00

121 lines
6.0 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()