组件模型

This commit is contained in:
2026-10-09 15:10:44 +08:00
parent 7fd1d97099
commit 66d87d48d6
9 changed files with 260 additions and 129 deletions
+38 -124
View File
@@ -1,45 +1,21 @@
extends CharacterBody3D
## 跳跃输入缓冲(秒):按下跳跃后这段时间内若落地,自动补上一次起跳
const JUMP_BUFFER_TIME: float = 0.1
@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 var walk_speed: float = 5.0
## 疾跑速度(米/秒)
@export var sprint_speed: float = 8.5
## 地面加速度(米/秒²)
@export var acceleration: float = 45.0
## 空中加速度(米/秒²)
@export var air_acceleration: float = 12.0
## 起跳初速度(米/秒)
@export var jump_velocity: float = 5.0
## 玩家协调器:只做三件事 —— 多人权限、依赖注入、按固定顺序编排各组件。
##
## 具体能力全在子节点组件里:
## Look 视角 character/player/player_look.gd
## Motor 移动跳跃 character/player/player_motor.gd
## Pickup 拾取 character/player/player_pickup.gd
## Animator 动画表现 character/player/player_animator.gd(挂在 Body/AnimationPlayer 上)
##
## 本脚本不实现任何操作,只负责「谁先谁后」和「谁有权跑」。
@onready var body: Node3D = $Body
## 眼睛分两层转:CameraRig 绕竖直轴转(yaw),Camera 原地抬头低头(pitch)。
## 分开是为了让 pitch 不掺进移动方向 —— 移动只用 rig 的 basis,低头看地也不会走歪。
@onready var camera_rig: Node3D = $CameraRig
@onready var camera: Camera3D = $CameraRig/Camera
## 动画状态机。控制器只把「运动结果」交给它,它也只管自己播动画。
@onready var animator: PlayerAnimator = $Body/AnimationPlayer
@onready var interaction_pos: Marker3D = $CameraRig/Camera/InteractionPos
@onready var interaction_ray: RayCast3D = $CameraRig/Camera/InteractionRay
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8))
var _yaw: float = 0.0
var _pitch: float = 0.0
var _jump_buffer: float = 0.0
# 拾取
var _pickObj = null
var _pickPower = 10
@onready var animation_player: PlayerAnimator = $AnimationPlayer
@onready var look: PlayerLook = $Look
@onready var motor: PlayerMotor = $Motor
@onready var pickup: PlayerPickup = $Pickup
func _ready() -> void:
@@ -47,109 +23,47 @@ 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
_yaw = camera_rig.rotation.y
_pitch = clampf(camera.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg))
_apply_look()
# 依赖注入:组件不自己去 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
)
set_mouse_captured(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_look()
elif event.is_action_pressed("jump"):
_jump_buffer = JUMP_BUFFER_TIME
elif event.is_action_pressed("ui_cancel"):
# 这里只留「跨组件的全局开关」,具体操作归各组件自己处理。
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:
elif event is InputEventMouseButton and event.pressed \
and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
set_mouse_captured(true)
if event is InputEventMouseButton and event.is_action_pressed("carry"):
if _pickObj == null:
_pickObject()
else:
_removePickObject()
func set_mouse_captured(captured: bool) -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE
## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角跟手、不等物理帧。
## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。
func _apply_look() -> void:
camera_rig.rotation.y = _yaw
camera.rotation.x = _pitch
body.rotation.y = _yaw
func _physics_process(delta: float) -> void:
# --- 重力 ---
if is_on_floor():
if velocity.y < 0.0:
velocity.y = 0.0
else:
velocity.y -= _gravity * delta
# --- 跳跃(读取输入缓冲)---
if _jump_buffer > 0.0:
_jump_buffer -= delta
if is_on_floor():
velocity.y = jump_velocity
_jump_buffer = 0.0
# --- 相对视线方向的水平输入 ---
# 只用 rig 的 basis(只有 yaw),低头看地时前进方向不会跟着往下扎。
var input_dir := Input.get_vector("move_left", "move_right", "move_back", "move_forward")
var look_basis := camera_rig.global_transform.basis
var wish := look_basis.x * input_dir.x - look_basis.z * input_dir.y
wish.y = 0.0
if wish.length_squared() > 0.000001:
wish = wish.normalized()
# 只有真正按了方向键才允许疾跑(避免原地"疾跑"触发跑动动画)
var sprinting := Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001
var target_speed := sprint_speed if sprinting else walk_speed
var accel := acceleration if is_on_floor() else air_acceleration
var horizontal := Vector2(velocity.x, velocity.z)
horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * target_speed, accel * delta)
velocity.x = horizontal.x
velocity.z = horizontal.y
# 顺序固定:算速度 → 推进 → 播动画 → 搬物体。
# 由根节点统一编排,各组件自己不许写 _physics_process ——
# 否则顺序取决于节点树顺序,改个节点位置就会出现「先滑再算速度」这类隐形 bug。
motor.step(delta)
move_and_slide()
# 动画只读运动结果。控制器不认识任何片段名,动画器也不认识输入。
animator.set_motion(
velocity,
is_on_floor(),
sprinting,
Input.is_action_pressed("crouch"),
)
# 物体拖动
if _pickObj != null:
var pickObjPos = _pickObj.global_position
var pickPlacePos = interaction_pos.global_position
_pickObj.set_linear_velocity((pickPlacePos - pickObjPos) * _pickPower)
func _pickObject() -> void:
var pickCollider = interaction_ray.get_collider()
print(pickCollider)
if pickCollider != null and pickCollider is RigidBody3D:
print(pickCollider)
_pickObj = pickCollider
func _removePickObject() -> void:
_pickObj = null
animation_player.set_motion(velocity, is_on_floor(), motor.sprinting, motor.crouching)
pickup.physics_step()