limbo hsm

This commit is contained in:
2026-10-15 16:56:45 +08:00
parent 18e7b2daa8
commit a76c4aebef
30 changed files with 665 additions and 121 deletions
+26 -69
View File
@@ -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