limbo hsm
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
extends LimboHSM
|
||||
## 空中子机(挂在 HSM/Airborne 上)。
|
||||
##
|
||||
## Jump(上升段)→ Fall(越过顶点开始下落)。进入时按当前竖直速度挑初始子状态:
|
||||
## 起跳进来 vy > 0 → Jump;踏空掉进来 vy ≈ 0 → Fall(vy ≤ 0 一律算 Fall)。
|
||||
|
||||
@onready var jump: LimboState = $Jump
|
||||
@onready var fall: LimboState = $Fall
|
||||
|
||||
|
||||
func _setup() -> void:
|
||||
add_transition(jump, fall, &"falling")
|
||||
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
set_initial_state(fall if player == null or player.velocity.y <= 0.0 else jump)
|
||||
@@ -0,0 +1 @@
|
||||
uid://43psuo6pb4wq
|
||||
@@ -0,0 +1,18 @@
|
||||
extends PlayerGroundState
|
||||
## 原地蹲:目标速度 0,播 crouch(蹲着不动)。
|
||||
## 蹲着走是 Sneak —— 两者只差「有没有方向输入」,转换事件同为 move_start/stop。
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
player.motor.target_speed = 0.0
|
||||
player.motor.sprinting = false
|
||||
player.motor.crouching = true
|
||||
player.animation_player.play_state(PlayerAnimator.AnimState.CROUCH)
|
||||
|
||||
|
||||
func _think(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if not Input.is_action_pressed("crouch"):
|
||||
dispatch(&"crouch_end")
|
||||
elif player.motor.has_move_input():
|
||||
dispatch(&"move_start")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb8dvx7nwuiwo
|
||||
@@ -0,0 +1,14 @@
|
||||
extends LimboState
|
||||
## 下落段:播 fall。落地(is_on_floor 且不再上升)转回地面,由 Grounded._enter
|
||||
## 按当下输入挑子状态(走/跑/蹲着落地不会闪站立)。
|
||||
|
||||
func _enter() -> void:
|
||||
(get_agent() as Player).animation_player.play_state(PlayerAnimator.AnimState.FALL)
|
||||
|
||||
|
||||
func _update(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if player == null:
|
||||
return
|
||||
if player.is_on_floor() and player.velocity.y <= 0.0:
|
||||
dispatch(&"landed")
|
||||
@@ -0,0 +1 @@
|
||||
uid://yadts2ydgfs6
|
||||
@@ -0,0 +1,32 @@
|
||||
class_name PlayerGroundState
|
||||
extends LimboState
|
||||
## 地面叶子状态的公共基类(Idle / Walk / Sprint / Crouch / Sneak 共用)。
|
||||
##
|
||||
## 每帧先统一判定「环境事实」,之后才轮到 _think() 里各状态自己的输入转换:
|
||||
## 1. 跳跃缓冲成立(try_jump 当场起跳)→ 转空中(vy > 0 → Jump);
|
||||
## 2. 脚下没地板(踏空)→ 转空中(Fall)。
|
||||
## 这两层判定放在**叶子**而不是父机:父机 _update 先于子状态执行,
|
||||
## 若在父机里 dispatch,转换后父机本轮还会继续调(已被退出的)旧子状态,
|
||||
## 存在「旧状态在已退出的机器里再次 dispatch」的顺序陷阱;叶子自己判、
|
||||
## dispatch 后立即 return,则没有这个窗口。
|
||||
##
|
||||
## is_on_floor() 反映的是上一帧 move_and_slide() 的结果 ——
|
||||
## 与推进的更新节奏一致,不引入额外的检测延迟。
|
||||
|
||||
func _update(delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if player == null:
|
||||
return
|
||||
if player.motor.try_jump():
|
||||
dispatch(&"airborne")
|
||||
return
|
||||
if not player.is_on_floor():
|
||||
dispatch(&"airborne")
|
||||
return
|
||||
_think(delta)
|
||||
|
||||
|
||||
## 各状态自己的输入转换。要不要 dispatch 事件、往哪转,由具体状态决定
|
||||
## (转换表注册在 grounded.gd 的 _setup 里)。
|
||||
func _think(_delta: float) -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://dss42q7c4kqui
|
||||
@@ -0,0 +1,45 @@
|
||||
extends LimboHSM
|
||||
## 地面子机(挂在 HSM/Grounded 上)。
|
||||
##
|
||||
## 五个叶子状态:
|
||||
## Idle 站立不动 Walk 步行 Sprint 疾跑 Crouch 原地蹲 Sneak 蹲走
|
||||
## 叶子各自的输入转换见 ground_state.gd 及各状态脚本;本机只注册转换表,
|
||||
## 并在 _enter(首次激活 / 每次落地)时按「当下输入」挑初始子状态 ——
|
||||
## 不然按着方向键落地会先闪一帧站立。
|
||||
|
||||
@onready var idle: LimboState = $Idle
|
||||
@onready var walk: LimboState = $Walk
|
||||
@onready var sprint: LimboState = $Sprint
|
||||
@onready var crouch: LimboState = $Crouch
|
||||
@onready var sneak: LimboState = $Sneak
|
||||
|
||||
|
||||
func _setup() -> void:
|
||||
# 事件口径:move_start/stop 有无方向输入,sprint_start/stop 疾跑键,
|
||||
# crouch_start/end 蹲键。蹲伏优先于疾跑(蹲下时按 Shift 不算疾跑)。
|
||||
add_transition(idle, walk, &"move_start")
|
||||
add_transition(idle, crouch, &"crouch_start")
|
||||
add_transition(walk, idle, &"move_stop")
|
||||
add_transition(walk, sprint, &"sprint_start")
|
||||
add_transition(walk, sneak, &"crouch_start")
|
||||
add_transition(sprint, walk, &"sprint_stop")
|
||||
add_transition(sprint, idle, &"move_stop")
|
||||
add_transition(sprint, sneak, &"crouch_start")
|
||||
add_transition(crouch, sneak, &"move_start")
|
||||
add_transition(crouch, idle, &"crouch_end")
|
||||
add_transition(sneak, crouch, &"move_stop")
|
||||
add_transition(sneak, walk, &"crouch_end")
|
||||
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
if player == null:
|
||||
set_initial_state(idle)
|
||||
return
|
||||
var moving := player.motor.has_move_input()
|
||||
if Input.is_action_pressed("crouch"):
|
||||
set_initial_state(sneak if moving else crouch)
|
||||
elif moving:
|
||||
set_initial_state(sprint if Input.is_action_pressed("sprint") else walk)
|
||||
else:
|
||||
set_initial_state(idle)
|
||||
@@ -0,0 +1 @@
|
||||
uid://2bnkxlycmqqu
|
||||
@@ -0,0 +1,17 @@
|
||||
extends PlayerGroundState
|
||||
## 站立:目标速度 0,播 idle。
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
player.motor.target_speed = 0.0
|
||||
player.motor.sprinting = false
|
||||
player.motor.crouching = false
|
||||
player.animation_player.play_state(PlayerAnimator.AnimState.IDLE)
|
||||
|
||||
|
||||
func _think(_delta: float) -> void:
|
||||
# 蹲键优先 —— 同帧又蹲又走时先蹲下,下一帧 Crouch 再转 Sneak,不会漏蹲。
|
||||
if Input.is_action_pressed("crouch"):
|
||||
dispatch(&"crouch_start")
|
||||
elif (get_agent() as Player).motor.has_move_input():
|
||||
dispatch(&"move_start")
|
||||
@@ -0,0 +1 @@
|
||||
uid://2isemrs6ov3
|
||||
@@ -0,0 +1,20 @@
|
||||
extends LimboState
|
||||
## 上升段:播 jump。越过顶点(vy ≤ 0)转 Fall。
|
||||
## 水平目标速度保持离地前的值 —— 空中加速度本来就小(air_acceleration),
|
||||
## 不由本状态改写。
|
||||
|
||||
func _enter() -> void:
|
||||
(get_agent() as Player).animation_player.play_state(PlayerAnimator.AnimState.JUMP)
|
||||
|
||||
|
||||
func _update(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if player == null:
|
||||
return
|
||||
# 落地判定带 vy ≤ 0 门槛:起跳那一帧 is_on_floor() 还是 true
|
||||
# (要等 move_and_slide() 之后才翻假),没有门槛会当帧弹回地面状态。
|
||||
if player.is_on_floor() and player.velocity.y <= 0.0:
|
||||
dispatch(&"landed")
|
||||
return
|
||||
if player.velocity.y <= 0.0:
|
||||
dispatch(&"falling")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvfcvo7k1i5jr
|
||||
@@ -0,0 +1,16 @@
|
||||
extends LimboHSM
|
||||
## 玩家状态机根(挂在 Player/HSM 节点上,update_mode = MANUAL)。
|
||||
##
|
||||
## 只管最粗的一层:「身体在地上还是空中」。细分状态在两个子机里:
|
||||
## Grounded 地面 grounded.gd —— Idle / Walk / Sprint / Crouch / Sneak
|
||||
## Airborne 空中 airborne.gd —— Jump / Fall
|
||||
## 推进由 Player._physics_process 显式调 update(delta)(管线第一位)。
|
||||
|
||||
@onready var grounded: LimboHSM = $Grounded
|
||||
@onready var airborne: LimboHSM = $Airborne
|
||||
|
||||
|
||||
func _setup() -> void:
|
||||
# 起跳或踏空 → 空中;落地 → 回地面。事件的发起在各叶子状态里。
|
||||
add_transition(grounded, airborne, &"airborne")
|
||||
add_transition(airborne, grounded, &"landed")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgjaw5xbg7gdj
|
||||
@@ -0,0 +1,19 @@
|
||||
extends PlayerGroundState
|
||||
## 蹲走(潜行):目标速度 = crouch_speed,播 sneak。
|
||||
## 蹲伏优先于疾跑:蹲着按 Shift 也只是潜行。
|
||||
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
player.motor.target_speed = player.motor.crouch_speed
|
||||
player.motor.sprinting = false
|
||||
player.motor.crouching = true
|
||||
player.animation_player.play_state(PlayerAnimator.AnimState.SNEAK, player.motor.crouch_speed)
|
||||
|
||||
|
||||
func _think(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if not Input.is_action_pressed("crouch"):
|
||||
dispatch(&"crouch_end")
|
||||
elif not player.motor.has_move_input():
|
||||
dispatch(&"move_stop")
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4okw70vrah42
|
||||
@@ -0,0 +1,19 @@
|
||||
extends PlayerGroundState
|
||||
## 疾跑:目标速度 = sprint_speed,播 run。只在按着方向键时成立(Sprint 由 Walk 进入)。
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
player.motor.target_speed = player.motor.sprint_speed
|
||||
player.motor.sprinting = true
|
||||
player.motor.crouching = false
|
||||
player.animation_player.play_state(PlayerAnimator.AnimState.RUN, player.motor.sprint_speed)
|
||||
|
||||
|
||||
func _think(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if Input.is_action_pressed("crouch"):
|
||||
dispatch(&"crouch_start")
|
||||
elif not player.motor.has_move_input():
|
||||
dispatch(&"move_stop")
|
||||
elif not Input.is_action_pressed("sprint"):
|
||||
dispatch(&"sprint_stop")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cklw8uq6xahe
|
||||
@@ -0,0 +1,19 @@
|
||||
extends PlayerGroundState
|
||||
## 步行:目标速度 = walk_speed,播 walk。
|
||||
|
||||
func _enter() -> void:
|
||||
var player := get_agent() as Player
|
||||
player.motor.target_speed = player.motor.walk_speed
|
||||
player.motor.sprinting = false
|
||||
player.motor.crouching = false
|
||||
player.animation_player.play_state(PlayerAnimator.AnimState.WALK, player.motor.walk_speed)
|
||||
|
||||
|
||||
func _think(_delta: float) -> void:
|
||||
var player := get_agent() as Player
|
||||
if Input.is_action_pressed("crouch"):
|
||||
dispatch(&"crouch_start")
|
||||
elif not player.motor.has_move_input():
|
||||
dispatch(&"move_stop")
|
||||
elif Input.is_action_pressed("sprint"):
|
||||
dispatch(&"sprint_start")
|
||||
@@ -0,0 +1 @@
|
||||
uid://6pa3038100o4
|
||||
@@ -1,3 +1,4 @@
|
||||
class_name Player
|
||||
extends CharacterBody3D
|
||||
## 玩家协调器:只做三件事 —— 多人权限、依赖注入、按固定顺序编排各组件。
|
||||
##
|
||||
@@ -6,8 +7,12 @@ extends CharacterBody3D
|
||||
## 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()。
|
||||
@@ -22,6 +27,9 @@ extends CharacterBody3D
|
||||
@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:
|
||||
@@ -60,6 +68,12 @@ func _ready() -> void:
|
||||
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)
|
||||
|
||||
|
||||
@@ -91,13 +105,16 @@ func _on_pickup_interact_state_changed(hovering: bool, holding: bool) -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# 顺序固定:算速度 → 同步蹲伏高度 → 推进 → 播动画 → 搬物体。
|
||||
# 由根节点统一编排,各组件自己不许写 _physics_process ——
|
||||
# 顺序固定:状态机(判定状态,写目标速度与动画)→ 算速度 → 同步蹲伏高度 →
|
||||
# 推进 → 刷步频 → 搬物体。由根节点统一编排,各组件自己不许写 _physics_process ——
|
||||
# 否则顺序取决于节点树顺序,改个节点位置就会出现「先滑再算速度」这类隐形 bug。
|
||||
# HSM 排最前:状态写入的 target_speed 当帧就被 motor 消费;
|
||||
# 起跳在 Grounded 状态里就地执行(motor.try_jump),同一帧转进 Jump。
|
||||
hsm.update(delta)
|
||||
motor.step(delta)
|
||||
# 蹲伏状态由 Motor 单点判定,再转交给 Look 去升降摄像机 ——
|
||||
# 不让 Look 自己去读蹲键,免得两个组件各判一次、将来不同步。
|
||||
# 蹲伏事实由 HSM 的 Crouch/Sneak 状态写进 motor.crouching ——
|
||||
# 不让 Look 自己去读蹲键,免得两边各判一次、将来不同步。
|
||||
look.set_crouching(motor.crouching)
|
||||
move_and_slide()
|
||||
animation_player.set_motion(velocity, is_on_floor(), motor.sprinting, motor.crouching)
|
||||
animation_player.set_gait(velocity)
|
||||
pickup.physics_step()
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
[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"]
|
||||
[ext_resource type="Script" uid="uid://cgjaw5xbg7gdj" path="res://character/player/hsm/player_hsm.gd" id="20_7gte1"]
|
||||
[ext_resource type="Script" uid="uid://2bnkxlycmqqu" path="res://character/player/hsm/grounded.gd" id="21_k6mlu"]
|
||||
[ext_resource type="Script" uid="uid://2isemrs6ov3" path="res://character/player/hsm/idle.gd" id="22_r212m"]
|
||||
[ext_resource type="Script" uid="uid://6pa3038100o4" path="res://character/player/hsm/walk.gd" id="23_8bmvw"]
|
||||
[ext_resource type="Script" uid="uid://bb8dvx7nwuiwo" path="res://character/player/hsm/crouch.gd" id="24_2pong"]
|
||||
[ext_resource type="Script" uid="uid://b4okw70vrah42" path="res://character/player/hsm/sneak.gd" id="25_tkl2s"]
|
||||
[ext_resource type="Script" uid="uid://cklw8uq6xahe" path="res://character/player/hsm/sprint.gd" id="26_vhygx"]
|
||||
[ext_resource type="Script" uid="uid://43psuo6pb4wq" path="res://character/player/hsm/airborne.gd" id="27_s2r15"]
|
||||
[ext_resource type="Script" uid="uid://cvfcvo7k1i5jr" path="res://character/player/hsm/jump.gd" id="28_ed2qs"]
|
||||
[ext_resource type="Script" uid="uid://yadts2ydgfs6" path="res://character/player/hsm/fall.gd" id="29_u55fa"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rkbax"]
|
||||
radius = 0.54541016
|
||||
@@ -98,8 +108,37 @@ script = ExtResource("13_look")
|
||||
|
||||
[node name="Motor" type="Node" parent="." unique_id=1043927815]
|
||||
script = ExtResource("14_motor")
|
||||
walk_speed = 2.419
|
||||
sprint_speed = 3.624
|
||||
|
||||
[node name="Pickup" type="Node" parent="." unique_id=1587230140]
|
||||
script = ExtResource("15_pickup")
|
||||
|
||||
[node name="HSM" type="LimboHSM" parent="." unique_id=100369043]
|
||||
update_mode = 2
|
||||
script = ExtResource("20_7gte1")
|
||||
|
||||
[node name="Grounded" type="LimboHSM" parent="HSM" unique_id=159625644]
|
||||
script = ExtResource("21_k6mlu")
|
||||
|
||||
[node name="Idle" type="LimboState" parent="HSM/Grounded" unique_id=1876145612]
|
||||
script = ExtResource("22_r212m")
|
||||
|
||||
[node name="Walk" type="LimboState" parent="HSM/Grounded" unique_id=689938768]
|
||||
script = ExtResource("23_8bmvw")
|
||||
|
||||
[node name="Crouch" type="LimboState" parent="HSM/Grounded" unique_id=1470963204]
|
||||
script = ExtResource("24_2pong")
|
||||
|
||||
[node name="Sneak" type="LimboState" parent="HSM/Grounded" unique_id=988596449]
|
||||
script = ExtResource("25_tkl2s")
|
||||
|
||||
[node name="Sprint" type="LimboState" parent="HSM/Grounded" unique_id=674736640]
|
||||
script = ExtResource("26_vhygx")
|
||||
|
||||
[node name="Airborne" type="LimboHSM" parent="HSM" unique_id=1349311867]
|
||||
script = ExtResource("27_s2r15")
|
||||
|
||||
[node name="Jump" type="LimboState" parent="HSM/Airborne" unique_id=2079971118]
|
||||
script = ExtResource("28_ed2qs")
|
||||
|
||||
[node name="Fall" type="LimboState" parent="HSM/Airborne" unique_id=233377038]
|
||||
script = ExtResource("29_u55fa")
|
||||
|
||||
@@ -2,8 +2,10 @@ class_name PlayerAnimator
|
||||
extends Node
|
||||
## 玩家表现层:把「运动结果」翻译成动画片段 —— 只读运动、只写动画,不碰物理。
|
||||
##
|
||||
## 控制器每物理帧调一次 set_motion(),本节点据此决定 AnimState 并切片段。
|
||||
## 片段名集中在 ANIM_CLIPS,换模型 / 加动作只改这张表,控制器不认识任何片段名。
|
||||
## 「现在该播哪个状态、按什么速度为基准」由 hsm/ 的 LimboHSM 状态机决定:
|
||||
## 状态在 _enter 时调 play_state(状态, 基准速度) 切片段,
|
||||
## 控制器每物理帧调 set_gait() 按实际水平速度刷播放倍率。
|
||||
## 片段名集中在 ANIM_CLIPS,换模型 / 加动作只改这张表,状态机不认识任何片段名。
|
||||
##
|
||||
## 节点接法:本脚本直接挂在 Body/AnimationPlayer 上 —— 这时 animation_player 留空,
|
||||
## _ready() 会在父节点的子节点里找 AnimationPlayer,找到的就是它自己。
|
||||
@@ -36,32 +38,21 @@ const ANIM_LOOPED: Array[int] = [
|
||||
@export_group("播放")
|
||||
## 动作切换的交叉淡入时间(秒)。0 为硬切
|
||||
@export var blend_time: float = 0.15
|
||||
## 低于此水平速度就视为站立(米/秒)
|
||||
@export var idle_threshold: float = 0.2
|
||||
## 播放倍率下限:被墙抵住、起步那一瞬,脚步不该慢到像在滑冰
|
||||
@export var speed_scale_min: float = 0.65
|
||||
## 播放倍率上限:防止速度被外力拉高时脚步快到抽搐
|
||||
@export var speed_scale_max: float = 1.6
|
||||
|
||||
@export_group("片段基准速度")
|
||||
## 各片段在多少米/秒下按 1.0 倍播放。这是片段自身的属性,不是控制器的手感参数:
|
||||
## 控制器把跑速调到 6 m/s 时,这段动画自然播成 1.2 倍,脚步自动跟上,两边不用对齐。
|
||||
##
|
||||
## 注意:这几个数和「模型缩放」严格绑定 —— 角色尺寸一变,同一个片段一个循环跨出的
|
||||
## 世界距离也按同比例变,所以基准速度必须跟着乘同一系数,否则脚步会明显打滑。
|
||||
## 当前值对应 0.15 m 身高(基准系数 k ≈ 0.0857,推导见 player_motor.gd 顶部的说明)。
|
||||
##
|
||||
## sneak_ref_speed 要和 player_motor.gd 的 crouch_speed 保持一致 ——
|
||||
## 蹲着走时播的就是 SNEAK 片段,两边不等就会打滑。(当前两者都是 0.2143)
|
||||
@export var walk_ref_speed: float = 0.4286
|
||||
@export var run_ref_speed: float = 0.7286
|
||||
@export var crouch_ref_speed: float = 0.2143
|
||||
@export var sneak_ref_speed: float = 0.2143
|
||||
|
||||
## 状态 → AnimationPlayer 里真正的片段键(形如 "walk/walk")。_ready() 里解析一次。
|
||||
## 不直接拿 ANIM_CLIPS 的值去播,是因为多库时短名查不到 —— 见 _ready() 里的说明。
|
||||
var _clips: Dictionary = {}
|
||||
|
||||
## 当前片段的基准速度(米/秒):play_state() 时由 HSM 状态传入。
|
||||
## 倍率 = 实际水平速度 / 它。状态自己清楚它把 motor 的哪个速度当目标,
|
||||
## 由它传入就不会出现「两处各存一份基准速度、改了一边忘了另一边」的打滑。
|
||||
## 0(或负)= 该片段不参与速度缩放(站立 / 原地蹲 / 跳 / 落一律原速)。
|
||||
var _ref_speed: float = 0.0
|
||||
|
||||
## 当前动作状态(AnimState)。权威端每物理帧写一次,Sync 复制给远端。
|
||||
var anim_state: int = AnimState.IDLE:
|
||||
set(value):
|
||||
@@ -115,17 +106,23 @@ func _ready() -> void:
|
||||
|
||||
# ---------------- 对外接口 ----------------
|
||||
|
||||
## HSM 状态在 _enter 时调用:切到该动作状态(setter 负责切片段),并登记
|
||||
## 该片段的基准速度。「播什么」是状态机的决定,「怎么播」(交叉淡入、循环、倍率)
|
||||
## 才是本节点的职责。
|
||||
func play_state(state: AnimState, ref_speed: float = 0.0) -> void:
|
||||
# 先写基准速度再切片段:万一 setter 里有人读倍率,也不会用上旧的。
|
||||
_ref_speed = maxf(ref_speed, 0.0)
|
||||
anim_state = state
|
||||
|
||||
|
||||
## 控制器每物理帧调用一次(放在 move_and_slide() 之后)。
|
||||
## 只读入运动快照,不回写任何物理量 —— 这就是「单向耦合」的边界。
|
||||
## crouching 就是「按住了蹲键」这个输入事实;蹲着不动算 CROUCH、蹲着走算 SNEAK,
|
||||
## 这个区分在这里做,控制器不需要知道两个状态有什么不一样。不传就是普通站立。
|
||||
func set_motion(
|
||||
velocity: Vector3, on_floor: bool, sprinting: bool = false, crouching: bool = false
|
||||
) -> void:
|
||||
var state := _resolve_state(velocity, on_floor, sprinting, crouching)
|
||||
# 先写倍率再切片段 —— 顺序反了会让新片段用一帧旧倍率。
|
||||
anim_speed = _speed_scale_for(state, velocity)
|
||||
anim_state = state # setter 负责切片段
|
||||
## 只按当前水平速度刷播放倍率 —— 不切片段、不回写任何物理量,单向耦合的边界不变。
|
||||
## play_state() 在状态 _enter(本帧更早)时执行,这里只会更新倍率。
|
||||
func set_gait(velocity: Vector3) -> void:
|
||||
if _ref_speed <= 0.0:
|
||||
anim_speed = 1.0
|
||||
return
|
||||
anim_speed = Vector2(velocity.x, velocity.z).length() / _ref_speed
|
||||
|
||||
|
||||
## 当前正在播的片段名。调试与测试用。
|
||||
@@ -137,46 +134,6 @@ func current_clip() -> String:
|
||||
|
||||
# ---------------- 内部 ----------------
|
||||
|
||||
## 优先级:空中 > 蹲 > 移动。
|
||||
## 空中最优先 —— 跳 / 落时不关心脚下是走还是蹲。
|
||||
## 蹲排在移动前面:只要按着蹲键就播蹲的片段,不会因为没动就弹回站立。
|
||||
## 蹲着不动是 CROUCH(原地蹲),蹲着走是 SNEAK(潜行)—— 两者只差「有没有在移动」。
|
||||
func _resolve_state(
|
||||
velocity: Vector3, on_floor: bool, sprinting: bool, crouching: bool
|
||||
) -> int:
|
||||
if not on_floor:
|
||||
# 上升用 jump、越过顶点用 fall。jump 片段长 0.5s,而上升时间是
|
||||
# jump_velocity / 有效重力(1.5 / 4.9 ≈ 0.31s),片段只播得完约六成 ——
|
||||
# 越过顶点立刻切 fall,观感上是连贯的。
|
||||
return AnimState.JUMP if velocity.y > 0.0 else AnimState.FALL
|
||||
var moving := Vector2(velocity.x, velocity.z).length() >= idle_threshold
|
||||
if crouching:
|
||||
return AnimState.SNEAK if moving else AnimState.CROUCH
|
||||
if not moving:
|
||||
return AnimState.IDLE
|
||||
return AnimState.RUN if sprinting else AnimState.WALK
|
||||
|
||||
|
||||
## 播放倍率 = 实际水平速度 / 该片段的基准速度。
|
||||
## 只有循环的位移动作参与缩放,跳跃 / 下落一律原速(它们不该被速度拉扯)。
|
||||
func _speed_scale_for(state: int, velocity: Vector3) -> float:
|
||||
var ref := 0.0
|
||||
match state:
|
||||
AnimState.WALK:
|
||||
ref = walk_ref_speed
|
||||
AnimState.RUN:
|
||||
ref = run_ref_speed
|
||||
AnimState.CROUCH:
|
||||
ref = crouch_ref_speed
|
||||
AnimState.SNEAK:
|
||||
ref = sneak_ref_speed
|
||||
_:
|
||||
return 1.0
|
||||
if ref <= 0.0:
|
||||
return 1.0
|
||||
return Vector2(velocity.x, velocity.z).length() / ref
|
||||
|
||||
|
||||
func _play(clip: String, blend: float) -> void:
|
||||
if animation_player == null or clip.is_empty():
|
||||
return
|
||||
|
||||
@@ -4,43 +4,41 @@ extends Node
|
||||
##
|
||||
## 只算速度写回 body.velocity,**不调 move_and_slide()** —— 推进由 Player 统一做。
|
||||
## 这样「算速度 → 推进 → 播动画」的顺序永远固定,不受节点树顺序影响。
|
||||
##
|
||||
## 「走多快、蹲不蹲、跑不跑」由 hsm/ 的 LimboHSM(LimboAI)决定:
|
||||
## 状态在 _enter 时写 target_speed / crouching / sprinting,
|
||||
## 本组件只负责把这份「意图」用物理方式变成速度。
|
||||
##
|
||||
## 设计基准:**小人国玩法,角色身高约 0.2 m**。速度不按「人类等比缩放」推导,
|
||||
## 直接按鼠类疾走的手感定标:走 ≈ 12 身长/秒、疾跑 ≈ 18 身长/秒、
|
||||
## 跳高 ≈ 1.15 身高。速度/加速度按手感单独调即可,不必成套换算;
|
||||
## 唯独 jump_velocity 与 gravity_scale 是**配对**的(反解公式见重力一节)。
|
||||
|
||||
## 跳跃输入缓冲(秒):按下跳跃后这段时间内若落地,自动补上一次起跳
|
||||
const JUMP_BUFFER_TIME: float = 0.1
|
||||
|
||||
@export_group("移动")
|
||||
## 【设计基准】以人类为参照做等比缩放:人 1.75 m,游戏惯例 walk 5.0 / sprint 8.5 m/s。
|
||||
## 本角色身高 0.15 m,系数 k = 0.15 / 1.75 ≈ 0.0857,下面所有线性量都是原值 × k。
|
||||
## 这样「每秒走过几个自身体长」和人类一致,动作比例看起来才对。
|
||||
##
|
||||
## 换尺寸时的公式:线性量(速度/加速度)× k';跳跃那两个数(gravity_scale 与 jump_velocity)用下面的反解公式重算。
|
||||
##
|
||||
## 常规移动速度(米/秒)—— 约 2.9 身长/秒,和人类走路同一比例
|
||||
@export var walk_speed: float = 0.4286
|
||||
## 疾跑速度(米/秒)—— 约 4.9 身长/秒
|
||||
@export var sprint_speed: float = 0.7286
|
||||
## 蹲行速度(米/秒)—— 默认是走速的一半。蹲下时按住 Shift 也不会疾跑。
|
||||
## 注意:改这个值要同步改 player_animator.gd 的 sneak_ref_speed,否则蹲走时脚步会打滑。
|
||||
@export var crouch_speed: float = 0.2143
|
||||
## 地面加速度(米/秒²)—— 约 0.11 秒加到全速,起步干脆
|
||||
@export var acceleration: float = 3.8571
|
||||
## 空中加速度(米/秒²)
|
||||
@export var air_acceleration: float = 1.0286
|
||||
## 起跳初速度(米/秒)—— 配下面的 gravity_scale 0.5(g = 4.9)时,
|
||||
## 跳跃高度 ≈ 0.230 m(约 1.53 身高)、滞空 ≈ 0.61 s。
|
||||
## 这两个数是**配对**的:改重力必须按「反解公式」重算它,否则跳跃高度按 1/g 暴涨。
|
||||
## 常规移动速度(米/秒)—— ≈ 12 身长/秒(0.2 m 身高),小鼠疾走的步频感
|
||||
@export var walk_speed: float = 2.419
|
||||
## 疾跑速度(米/秒)—— ≈ 18 身长/秒
|
||||
@export var sprint_speed: float = 3.624
|
||||
## 蹲行速度(米/秒)—— 走速的一半。蹲下时按住 Shift 也不疾跑(蹲伏优先)。
|
||||
@export var crouch_speed: float = 1.21
|
||||
## 地面加速度(米/秒²)—— 约 0.11 秒加到走速全速,起步干脆(鼠类的一窜一动)
|
||||
@export var acceleration: float = 22.0
|
||||
## 空中加速度(米/秒²)—— 约地面的 1/4:空中能修正方向,但不至于完全失控
|
||||
@export var air_acceleration: float = 5.9
|
||||
## 起跳初速度(米/秒)—— 配 gravity_scale 0.5(有效 g = 4.9)时,
|
||||
## 跳高 ≈ 0.23 m(约 1.15 身高)、滞空 ≈ 0.61 s。**与 gravity_scale 配对**,见下。
|
||||
@export var jump_velocity: float = 1.5
|
||||
|
||||
@export_group("重力")
|
||||
## 重力缩放系数。0.5 = 重力减半(有效 g = 4.9 m/s²)。
|
||||
## 重力缩放系数。0.5 = 有效重力 4.9 m/s²。
|
||||
##
|
||||
## 为什么不用 1.0(物理真实的 9.8):重力加速度本身和物体大小无关,但我们的速度是按身材
|
||||
## 等比缩过的,于是落体相对过快。更关键的是跳跃 —— 由 h = v²/2g 和 T = 2v/g 可得
|
||||
## h = g·T²/8:**固定重力下滞空翻倍、高度要涨 4 倍**。所以「想跳得久又不想跳太高」
|
||||
## 只能降重力。取 0.5 时:跳高 0.230 m(1.53 身高)、滞空 0.61 s;
|
||||
## 若维持 1.0,同样 0.23 m 的高度只有 0.43 s 滞空,落地明显仓促。
|
||||
## 小个子角色若用真实 9.8,同样的起跳速度滞空短一半、跳起来像被拍回去 ——
|
||||
## 降重力换滞空,跳跃才有「腾空感」。0.5 时:跳高 0.23 m、滞空 0.61 s。
|
||||
##
|
||||
## ⚠ 改这个值必须同步改 jump_velocity(跳跃高度 = v²/2g)。
|
||||
## ⚠ 与 jump_velocity **配对**:改一个必须按反解公式重算另一个(跳跃高度 = v²/2g)。
|
||||
## 反解公式 —— 定目标高度 h(米)与滞空 T(秒):
|
||||
## g = 8h / T² → gravity_scale = g / 9.8
|
||||
## jump_velocity = 4h / T
|
||||
@@ -49,8 +47,12 @@ const JUMP_BUFFER_TIME: float = 0.1
|
||||
## 那是全局的,会连带把拖拽/投掷的刚体一起变飘。
|
||||
@export_range(0.0, 2.0, 0.01) var gravity_scale: float = 0.5
|
||||
|
||||
## 本帧是否在疾跑 / 是否按住蹲键。
|
||||
## Player 读这两个值转交给 PlayerAnimator 切片段,crouching 还用来压低摄像机。
|
||||
## 当前目标水平速度(米/秒)—— 由 HSM 状态在 _enter 时写入,step() 朝它加减速。
|
||||
## Idle / Crouch 写 0(自然减速到停),Walk / Sprint / Sneak 写各自速度。
|
||||
var target_speed: float = 0.0
|
||||
|
||||
## 是否蹲着 / 是否疾跑 —— 由 HSM 状态写入的**事实镜像**,本组件不再自己读输入推断:
|
||||
## crouching 给 Look 压低摄像机,两者都给 DebugPanel 显示。
|
||||
var sprinting: bool = false
|
||||
var crouching: bool = false
|
||||
|
||||
@@ -73,6 +75,23 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_jump_buffer = JUMP_BUFFER_TIME
|
||||
|
||||
|
||||
## 本帧是否按着方向键。HSM 状态(Idle/Walk 等的转换判定)与 step() 共用这个口径。
|
||||
func has_move_input() -> bool:
|
||||
return Input.get_vector("move_left", "move_right", "move_back", "move_forward") \
|
||||
.length_squared() > 0.000001
|
||||
|
||||
|
||||
## 由 HSM 地面状态每帧调用:跳跃缓冲还有效且脚下有地板,就当场起跳并返回 true。
|
||||
## 起跳「动作」放在状态判定处执行(而不是 step() 里),
|
||||
## 状态机同一帧就能拿到 vy > 0 这个事实、转进 Jump,动画不迟到一帧。
|
||||
func try_jump() -> bool:
|
||||
if _jump_buffer > 0.0 and _body != null and _body.is_on_floor():
|
||||
_body.velocity.y = jump_velocity
|
||||
_jump_buffer = 0.0
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## 每物理帧由 Player 调用一次,必须排在 move_and_slide() 之前。
|
||||
## 只写 _body.velocity,不做位移推进。
|
||||
func step(delta: float) -> void:
|
||||
@@ -87,12 +106,9 @@ func step(delta: float) -> void:
|
||||
# gravity_scale 每帧现读,方便运行中拖着试手感(1.0 = 物理真实的 9.8)
|
||||
_body.velocity.y -= _gravity * gravity_scale * delta
|
||||
|
||||
# --- 跳跃(读取输入缓冲)---
|
||||
# --- 跳跃缓冲倒计时(执行挪到 try_jump(),由 HSM 地面状态调用)---
|
||||
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),低头看地时前进方向不会跟着往下扎。
|
||||
@@ -103,16 +119,7 @@ func step(delta: float) -> void:
|
||||
if wish.length_squared() > 0.000001:
|
||||
wish = wish.normalized()
|
||||
|
||||
# 只有真正按了方向键才允许疾跑(避免原地"疾跑"触发跑动动画)
|
||||
var wants_sprint := Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001
|
||||
crouching = Input.is_action_pressed("crouch")
|
||||
# 蹲伏优先于疾跑:蹲着时按住 Shift 不算疾跑,速度取蹲行速度。
|
||||
sprinting = wants_sprint and not crouching
|
||||
var target_speed := walk_speed
|
||||
if crouching:
|
||||
target_speed = crouch_speed
|
||||
elif sprinting:
|
||||
target_speed = sprint_speed
|
||||
# 目标速度来自 HSM 状态(target_speed),不在本组件里按蹲键/Shift 分档。
|
||||
var accel := acceleration if _body.is_on_floor() else air_acceleration
|
||||
|
||||
var horizontal := Vector2(_body.velocity.x, _body.velocity.z)
|
||||
|
||||
Reference in New Issue
Block a user