组件模型
This commit is contained in:
+38
-124
@@ -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()
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
[ext_resource type="AnimationLibrary" uid="uid://dy8a178vlbsof" path="res://character/player/assets/animations/sneak.glb" id="8_jo6ux"]
|
||||
[ext_resource type="AnimationLibrary" uid="uid://db83440o2cyr7" path="res://character/player/assets/animations/walk.glb" id="9_fkbv7"]
|
||||
[ext_resource type="Script" uid="uid://c6l2fl5jgvr1h" path="res://character/player/face/face_rig.gd" id="12_lqp71"]
|
||||
[ext_resource type="Script" uid="uid://bo1hob2whrlu0" path="res://character/player/player_look.gd" id="13_look"]
|
||||
[ext_resource type="Script" uid="uid://xta8h2snalsd" path="res://character/player/player_motor.gd" id="14_motor"]
|
||||
[ext_resource type="Script" uid="uid://b3ko4cfjf0vgu" path="res://character/player/player_pickup.gd" id="15_pickup"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rkbax"]
|
||||
radius = 0.54541016
|
||||
@@ -50,6 +53,7 @@ grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
script = ExtResource("2_vuhga")
|
||||
|
||||
[node name="Body" type="Node3D" parent="." unique_id=394283421]
|
||||
@@ -57,8 +61,11 @@ script = ExtResource("2_vuhga")
|
||||
[node name="mouse" parent="Body" unique_id=1995806863 instance=ExtResource("2_wu8e1")]
|
||||
transform = Transform3D(-0.18240134, 0, -1.5946032e-08, 0, 0.18240134, 0, 1.5946032e-08, 0, -0.18240134, 0, 0.8705118, 0)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="Body" unique_id=1656879850]
|
||||
root_node = NodePath("../mouse")
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=1979575210]
|
||||
replication_config = ExtResource("3_wu8e1")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1656879850]
|
||||
root_node = NodePath("../Body/mouse")
|
||||
libraries/crouch = ExtResource("4_g20gq")
|
||||
libraries/fall = ExtResource("4_hr31g")
|
||||
libraries/idle = ExtResource("5_lqp71")
|
||||
@@ -68,8 +75,14 @@ libraries/sneak = ExtResource("8_jo6ux")
|
||||
libraries/walk = ExtResource("9_fkbv7")
|
||||
script = ExtResource("3_g20gq")
|
||||
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=1979575210]
|
||||
replication_config = ExtResource("3_wu8e1")
|
||||
|
||||
[node name="FaceRig" type="Node" parent="." unique_id=792293233]
|
||||
script = ExtResource("12_lqp71")
|
||||
|
||||
[node name="Look" type="Node" parent="." unique_id=1738452096]
|
||||
script = ExtResource("13_look")
|
||||
|
||||
[node name="Motor" type="Node" parent="." unique_id=1043927815]
|
||||
script = ExtResource("14_motor")
|
||||
|
||||
[node name="Pickup" type="Node" parent="." unique_id=1587230140]
|
||||
script = ExtResource("15_pickup")
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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
|
||||
|
||||
## 眼睛分两层转: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
|
||||
|
||||
|
||||
## 由 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))
|
||||
_apply()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角跟手、不等物理帧。
|
||||
## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。
|
||||
func _apply() -> void:
|
||||
if _rig == null:
|
||||
return
|
||||
_rig.rotation.y = _yaw
|
||||
_camera.rotation.x = _pitch
|
||||
_body.rotation.y = _yaw
|
||||
@@ -0,0 +1 @@
|
||||
uid://bo1hob2whrlu0
|
||||
@@ -0,0 +1,83 @@
|
||||
class_name PlayerMotor
|
||||
extends Node
|
||||
## 移动组件:重力、跳跃缓冲、水平加减速。
|
||||
##
|
||||
## 只算速度写回 body.velocity,**不调 move_and_slide()** —— 推进由 Player 统一做。
|
||||
## 这样「算速度 → 推进 → 播动画」的顺序永远固定,不受节点树顺序影响。
|
||||
|
||||
## 跳跃输入缓冲(秒):按下跳跃后这段时间内若落地,自动补上一次起跳
|
||||
const JUMP_BUFFER_TIME: float = 0.1
|
||||
|
||||
@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
|
||||
|
||||
## 本帧是否在疾跑 / 是否按住蹲键。Player 读这两个值转交给 PlayerAnimator。
|
||||
var sprinting: bool = false
|
||||
var crouching: bool = false
|
||||
|
||||
var _body: CharacterBody3D
|
||||
var _look_rig: Node3D
|
||||
var _gravity: float = 9.8
|
||||
var _jump_buffer: float = 0.0
|
||||
|
||||
|
||||
## 由 Player 在 _ready() 注入依赖。look_rig 用来把输入转成「相对视线」的方向。
|
||||
func setup(body: CharacterBody3D, look_rig: Node3D) -> void:
|
||||
_body = body
|
||||
_look_rig = look_rig
|
||||
_gravity = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8))
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("jump"):
|
||||
_jump_buffer = JUMP_BUFFER_TIME
|
||||
|
||||
|
||||
## 每物理帧由 Player 调用一次,必须排在 move_and_slide() 之前。
|
||||
## 只写 _body.velocity,不做位移推进。
|
||||
func step(delta: float) -> void:
|
||||
if _body == null or _look_rig == null:
|
||||
return
|
||||
|
||||
# --- 重力 ---
|
||||
if _body.is_on_floor():
|
||||
if _body.velocity.y < 0.0:
|
||||
_body.velocity.y = 0.0
|
||||
else:
|
||||
_body.velocity.y -= _gravity * delta
|
||||
|
||||
# --- 跳跃(读取输入缓冲)---
|
||||
if _jump_buffer > 0.0:
|
||||
_jump_buffer -= delta
|
||||
if _body.is_on_floor():
|
||||
_body.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 := _look_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()
|
||||
|
||||
# 只有真正按了方向键才允许疾跑(避免原地"疾跑"触发跑动动画)
|
||||
sprinting = Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001
|
||||
crouching = Input.is_action_pressed("crouch")
|
||||
var target_speed := sprint_speed if sprinting else walk_speed
|
||||
var accel := acceleration if _body.is_on_floor() else air_acceleration
|
||||
|
||||
var horizontal := Vector2(_body.velocity.x, _body.velocity.z)
|
||||
horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * target_speed, accel * delta)
|
||||
_body.velocity.x = horizontal.x
|
||||
_body.velocity.z = horizontal.y
|
||||
@@ -0,0 +1 @@
|
||||
uid://xta8h2snalsd
|
||||
@@ -0,0 +1,54 @@
|
||||
class_name PlayerPickup
|
||||
extends Node
|
||||
## 拾取组件:射线看中的 RigidBody3D 会被吸附到 InteractionPos 前。
|
||||
## 只管「拿 / 放 / 拖」,不碰移动与视角。
|
||||
##
|
||||
## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 注入射线与持握点。
|
||||
|
||||
## 拖拽力度:物体被拉向持握点的速度系数
|
||||
@export var pick_power: float = 10.0
|
||||
|
||||
var _current: RigidBody3D = null
|
||||
var _ray: RayCast3D
|
||||
var _hold_pos: Marker3D
|
||||
|
||||
|
||||
## 由 Player 在 _ready() 注入依赖。
|
||||
func setup(ray: RayCast3D, hold_pos: Marker3D) -> void:
|
||||
_ray = ray
|
||||
_hold_pos = hold_pos
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("carry"):
|
||||
if _current == null:
|
||||
_pick()
|
||||
else:
|
||||
_release()
|
||||
|
||||
|
||||
## 每物理帧由 Player 调用一次,排在 move_and_slide() 之后。
|
||||
## 用速度把物体拽向持握点,而不是直接改 global_position ——
|
||||
## 这样它仍会和场景里其他刚体正常碰撞,不会被硬塞进墙里。
|
||||
func physics_step() -> void:
|
||||
if _current == null or _hold_pos == null:
|
||||
return
|
||||
var to_hold := _hold_pos.global_position - _current.global_position
|
||||
_current.set_linear_velocity(to_hold * pick_power)
|
||||
|
||||
|
||||
func _pick() -> void:
|
||||
if _ray == null:
|
||||
return
|
||||
var collider := _ray.get_collider()
|
||||
if collider is RigidBody3D:
|
||||
_current = collider as RigidBody3D
|
||||
|
||||
|
||||
func _release() -> void:
|
||||
_current = null
|
||||
|
||||
|
||||
## 当前是否抓着东西。调试用,也方便以后加「搬运时不能开枪」这类规则。
|
||||
func has_object() -> bool:
|
||||
return _current != null
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3ko4cfjf0vgu
|
||||
Reference in New Issue
Block a user