角色动画

This commit is contained in:
2026-10-02 09:32:11 +08:00
parent 5173b45066
commit d367c8370b
10 changed files with 240 additions and 68 deletions
+39 -11
View File
@@ -5,28 +5,32 @@ extends McpTestSuite
## 三处典型的「静默失效」——都表现为「人站着不动」且不打任何错误:
## 1. player_sync.tres 键名写错 / replication_mode 落到 NEVER → 远端收不到字段;
## 2. track 路径带一层模型目录名前缀,换模型/改层级后整组失效;
## 3. glb 片段的 loop_mode 默认是 NONE,idle/walk/sprint 播一遍就定在最后一帧。
## 3. glb 片段的 loop_mode 默认是 NONE,idle/walk/crouch/run/sneak 播一遍就定在最后一帧。
## (loop 由 PlayerAnimator._ready() 在运行时补,编辑器测试覆盖不到,靠实机验证。)
const CONFIG_PATH := "res://character/player/player_sync.tres"
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
## 从 player.gd 抽出来的动画状态机。控制器只通过它间接驱动动画。
const ANIMATOR_PATH := "Body/PlayerAnimator"
## 表现层脚本就直接挂在 AnimationPlayer 上,所以这两条路径指向同一个节点。
const ANIMATOR_PATH := "Body/AnimationPlayer"
const ANIMATOR_SCRIPT := "res://character/player/player_animator.gd"
## 真正干活的动画器。PlayerAnimator 没接线时按这个名字在自己兄弟子树里找它。
const ANIM_PLAYER_PATH := "Body/character-male-e2/AnimationPlayer"
## 真正干活的动画器。表现层脚本挂在它身上。
const ANIM_PLAYER_PATH := "Body/AnimationPlayer"
## player_animator.gd 的 ANIM_CLIPS 写死了这几个名字,少一个对应状态就会静默不动。
const REQUIRED_CLIPS: Array[String] = ["idle", "walk", "sprint", "jump", "fall"]
const REQUIRED_CLIPS: Array[String] = ["idle", "walk", "crouch", "run", "sneak", "jump", "fall"]
## CROUCH / SNEAK 两个状态唯一的入口 —— 控制器读的就是这个动作。
const CROUCH_ACTION := "crouch"
## 必须被复制到远端的字段。远端物理与输入都关着,全靠这几个值回放。
const REQUIRED_SYNCED: Array[String] = [
".:position",
"Body:rotation",
".:health",
"Body/PlayerAnimator:anim_state",
"Body/PlayerAnimator:anim_speed",
"Body/AnimationPlayer:anim_state",
"Body/AnimationPlayer:anim_speed",
]
@@ -80,7 +84,7 @@ func test_player_scene_wires_synchronizer() -> void:
)
## 组件边界:动画状态机必须是挂在 Body 下的独立节点,而不是塞回 player.gd。
## 组件边界:动画状态机必须是挂在 Body 下的节点,而不是塞回 player.gd。
## 有人把 anim_state / anim_speed 挪回控制器时,这条会失败。
func test_player_scene_has_animator_component() -> void:
var player := _instantiate_player()
@@ -104,7 +108,7 @@ func test_player_scene_has_animator_component() -> void:
assert_eq(script.resource_path, ANIMATOR_SCRIPT, "%s 挂的不是动画状态机脚本" % ANIMATOR_PATH)
## PlayerAnimator 没接线时按名字在自己兄弟子树里找 AnimationPlayer。
## PlayerAnimator 没接线时在父节点的子节点里按类型找 AnimationPlayer。
## 编辑器里跑不了它的 _ready()(非 @tool 脚本),所以这里退一步、用同样的规则找一遍:
## 改名或挪位置会让这条失败。
func test_animator_auto_find_target_exists() -> void:
@@ -129,11 +133,25 @@ func test_required_animation_clips_exist() -> void:
return
for clip in REQUIRED_CLIPS:
assert_true(
anim_player.has_animation(clip),
_find_clip(anim_player, clip) != null,
"动作库里缺少片段 %s(PlayerAnimator 的 ANIM_CLIPS 会指向不存在的片段)" % clip
)
## crouch 动作是 CROUCH / SNEAK 唯一的入口;动作没了按 Ctrl 没反应,而且不报错。
## 编辑器进程的 InputMap 只带内建动作,得先按引擎启动那套加载一次,否则这条必然假红。
func test_crouch_action_exists() -> void:
InputMap.load_from_project_settings()
assert_true(InputMap.has_action(CROUCH_ACTION), "InputMap 里没有 %s 动作" % CROUCH_ACTION)
if not InputMap.has_action(CROUCH_ACTION):
return
var bound_to_ctrl := false
for event in InputMap.action_get_events(CROUCH_ACTION):
if event is InputEventKey and (event as InputEventKey).keycode == KEY_CTRL:
bound_to_ctrl = true
assert_true(bound_to_ctrl, "%s 没有绑定 Ctrl" % CROUCH_ACTION)
## 片段是从 glb 里拷出来的,track 路径带层级前缀。换模型/改层级后整组 track 会一起失效,
## 而 AnimationPlayer 不报任何错 —— 现象是「人站着不动」。用测试钉住路径真的能解析。
func test_animation_track_paths_resolve() -> void:
@@ -146,7 +164,7 @@ func test_animation_track_paths_resolve() -> void:
return
for clip in REQUIRED_CLIPS:
var anim := anim_player.get_animation(clip)
var anim := _find_clip(anim_player, clip)
if anim == null:
continue
assert_gt(anim.get_track_count(), 0, "%s 片段没有任何 track" % clip)
@@ -169,6 +187,16 @@ func test_animation_track_paths_resolve() -> void:
)
## 片段在 AnimationPlayer 里按「库名/片段名」存(导入时每个 glb 各自成一个库),
## 短名 has_animation() / get_animation() 一律查不到 —— 拿短名断言会假红,必须按库扫一遍。
func _find_clip(anim_player: AnimationPlayer, clip: String) -> Animation:
for lib_name in anim_player.get_animation_library_list():
var lib := anim_player.get_animation_library(lib_name)
if lib != null and lib.has_animation(clip):
return lib.get_animation(clip)
return null
func _find_animation_player() -> AnimationPlayer:
var player := _instantiate_player()
if player == null:
+3
View File
@@ -92,7 +92,10 @@ func test_player_scene_has_interact_hint() -> void:
## 挂索/脱手都走 InputMap 的 interact 动作;动作没了按 E 没反应,而且不报错。
## 编辑器进程的 InputMap 只带内建动作,项目动作要等游戏进程启动时才 load 进去 ——
## 不先补这一步,这条断言在编辑器测试环境里必然假红。
func test_interact_action_exists() -> void:
InputMap.load_from_project_settings()
assert_true(InputMap.has_action(INTERACT_ACTION), "InputMap 里没有 %s 动作" % INTERACT_ACTION)
if not InputMap.has_action(INTERACT_ACTION):
return