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 上) ## ## 本脚本不实现任何操作,只负责「谁先谁后」和「谁有权跑」。 @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 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) 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。 motor.step(delta) # 蹲伏状态由 Motor 单点判定,再转交给 Look 去升降摄像机 —— # 不让 Look 自己去读蹲键,免得两个组件各判一次、将来不同步。 look.set_crouching(motor.crouching) move_and_slide() animation_player.set_motion(velocity, is_on_floor(), motor.sprinting, motor.crouching) pickup.physics_step()