角色动画
This commit is contained in:
+113
-19
@@ -1,32 +1,51 @@
|
||||
@tool
|
||||
extends McpTestSuite
|
||||
## 回归测试:Player 的同步配置必须真的被解析成条目。
|
||||
## 回归测试:Player 的同步配置与动作库必须真的被解析。
|
||||
##
|
||||
## 这是一处典型的「静默失效」:如果 .tres 里的键名写错,或 replication_mode
|
||||
## 落到 NEVER,远端玩家会僵在原地不动,而且不打任何错误。用测试钉住它。
|
||||
## 两处典型的「静默失效」:
|
||||
## 1. player_sync.tres 键名写错 / replication_mode 落到 NEVER
|
||||
## → 远端玩家僵在原地,而且不打任何错误。
|
||||
## 2. 动作库是从 .glb 拷进来的,track 路径带一层 character-male-e/ 前缀。
|
||||
## 一旦模型换名、层级变动、蒙皮骨骼改名,AnimationPlayer 会「有片段但什么都播不出来」,
|
||||
## 同样不报错。
|
||||
|
||||
const CONFIG_PATH := "res://player/player_sync.tres"
|
||||
const PLAYER_SCENE_PATH := "res://player/player.tscn"
|
||||
const CONFIG_PATH := "res://character/player/player_sync.tres"
|
||||
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
|
||||
const ANIM_PLAYER_PATH := "Body/character-male-e2/AnimationPlayer"
|
||||
|
||||
## player.gd 的 ANIM_CLIPS 写死了这几个名字,少一个对应状态就会静默不动。
|
||||
const REQUIRED_CLIPS: Array[String] = ["idle", "walk", "sprint", "jump", "fall"]
|
||||
|
||||
## 必须被复制到远端的字段。远端物理与输入都关着,全靠这几个值回放。
|
||||
const REQUIRED_SYNCED: Array[String] = [
|
||||
".:position",
|
||||
"Body:rotation",
|
||||
".:health",
|
||||
".:anim_state",
|
||||
".:anim_speed",
|
||||
]
|
||||
|
||||
|
||||
func suite_name() -> String:
|
||||
return "net"
|
||||
|
||||
|
||||
func test_config_has_expected_paths() -> void:
|
||||
var config := load(CONFIG_PATH) as SceneReplicationConfig
|
||||
assert_true(config != null, "player_sync.tres 未能加载为 SceneReplicationConfig")
|
||||
func test_config_has_required_synced_fields() -> void:
|
||||
var config := _load_config()
|
||||
assert_true(config != null, "%s 未能加载为 SceneReplicationConfig" % CONFIG_PATH)
|
||||
if config == null:
|
||||
return
|
||||
var paths := config.get_properties()
|
||||
assert_eq(paths.size(), 2, "同步字段数量不对:%s" % [paths])
|
||||
assert_true(config.has_property(^".:position"), "缺少 .:position")
|
||||
assert_true(config.has_property(^"Body:rotation"), "缺少 Body:rotation")
|
||||
for required in REQUIRED_SYNCED:
|
||||
assert_true(
|
||||
config.has_property(NodePath(required)),
|
||||
"缺少同步字段 %s,实际为 %s" % [required, paths]
|
||||
)
|
||||
|
||||
|
||||
func test_config_replication_mode_is_always() -> void:
|
||||
var config := load(CONFIG_PATH) as SceneReplicationConfig
|
||||
assert_true(config != null, "player_sync.tres 未能加载")
|
||||
var config := _load_config()
|
||||
assert_true(config != null, "%s 未能加载" % CONFIG_PATH)
|
||||
if config == null:
|
||||
return
|
||||
var paths := config.get_properties()
|
||||
@@ -35,20 +54,95 @@ func test_config_replication_mode_is_always() -> void:
|
||||
assert_eq(
|
||||
config.property_get_replication_mode(path),
|
||||
SceneReplicationConfig.REPLICATION_MODE_ALWAYS,
|
||||
"%s 的 replication_mode 不是 ALWAYS" % path
|
||||
"%s 的 replication_mode 不是 ALWAYS(改为 NEVER 会静默断掉同步)" % path
|
||||
)
|
||||
|
||||
|
||||
func test_player_scene_wires_synchronizer() -> void:
|
||||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||||
assert_true(packed != null, "player.tscn 未能加载")
|
||||
if packed == null:
|
||||
var player := _instantiate_player()
|
||||
if player == null:
|
||||
return
|
||||
var player := packed.instantiate()
|
||||
track(player)
|
||||
var sync := player.get_node_or_null("Sync") as MultiplayerSynchronizer
|
||||
assert_true(sync != null, "Player 下缺少 Sync 节点")
|
||||
if sync == null:
|
||||
return
|
||||
assert_true(sync.replication_config != null, "Sync 没有绑定 SceneReplicationConfig")
|
||||
assert_eq(sync.root_path, ^"..", "Sync.root_path 应为 .. (相对 Player)")
|
||||
if sync.replication_config != null:
|
||||
assert_eq(
|
||||
sync.replication_config.resource_path,
|
||||
CONFIG_PATH,
|
||||
"Sync 绑定的是别的同步配置:%s" % sync.replication_config.resource_path
|
||||
)
|
||||
|
||||
|
||||
func test_required_animation_clips_exist() -> void:
|
||||
var anim_player := _find_animation_player()
|
||||
if anim_player == null:
|
||||
return
|
||||
for clip in REQUIRED_CLIPS:
|
||||
assert_true(
|
||||
anim_player.has_animation(clip),
|
||||
"动作库里缺少片段 %s(player.gd 的 ANIM_CLIPS 会指向不存在的片段)" % clip
|
||||
)
|
||||
|
||||
|
||||
## 片段是从 glb 里拷出来的,track 路径带层级前缀。换模型/改层级后整组 track 会一起失效,
|
||||
## 而 AnimationPlayer 不报任何错 —— 现象是「人站着不动」。用测试钉住路径真的能解析。
|
||||
func test_animation_track_paths_resolve() -> void:
|
||||
var anim_player := _find_animation_player()
|
||||
if anim_player == null:
|
||||
return
|
||||
var root := anim_player.get_node_or_null(anim_player.root_node)
|
||||
assert_true(root != null, "AnimationPlayer.root_node 解析不到:%s" % anim_player.root_node)
|
||||
if root == null:
|
||||
return
|
||||
|
||||
# 逐帧验证 5 个要用到的片段,任何一个 track 断了都会让该动作失效
|
||||
for clip in REQUIRED_CLIPS:
|
||||
var anim := anim_player.get_animation(clip)
|
||||
if anim == null:
|
||||
continue
|
||||
assert_gt(anim.get_track_count(), 0, "%s 片段没有任何 track" % clip)
|
||||
for i in anim.get_track_count():
|
||||
var track_path := anim.track_get_path(i)
|
||||
# 不能直接 get_node(track_path):路径含 ":骨骼名" 属性部分,get_node 只认节点段
|
||||
var owner_node := root.get_node_or_null(NodePath(track_path.get_concatenated_names()))
|
||||
assert_true(
|
||||
owner_node != null,
|
||||
"%s 的 track %s 在 %s 下解析不到(动画会静默无效)" % [clip, track_path, anim_player.root_node]
|
||||
)
|
||||
if owner_node == null:
|
||||
continue
|
||||
# 属性段是骨骼名,模型换皮时要确保骨骼名没变
|
||||
var bone := str(track_path.get_concatenated_subnames())
|
||||
if owner_node is Skeleton3D and not bone.is_empty():
|
||||
assert_true(
|
||||
(owner_node as Skeleton3D).find_bone(bone) != -1,
|
||||
"%s 引用了骨骼 %s,但骨架里没有这根骨头(模型换过了?)" % [clip, bone]
|
||||
)
|
||||
|
||||
|
||||
## 直接用 load() 拿到的是编辑器进程里的内存副本 —— 外部刚改过磁盘时它可能还是旧的,
|
||||
## 测试会因此假绿或假红。这里强制按磁盘重读,断言的是文件本身。
|
||||
func _load_config() -> SceneReplicationConfig:
|
||||
return ResourceLoader.load(CONFIG_PATH, "", ResourceLoader.CACHE_MODE_REPLACE) as SceneReplicationConfig
|
||||
|
||||
|
||||
func _find_animation_player() -> AnimationPlayer:
|
||||
var player := _instantiate_player()
|
||||
if player == null:
|
||||
return null
|
||||
var anim_player := player.get_node_or_null(ANIM_PLAYER_PATH) as AnimationPlayer
|
||||
assert_true(anim_player != null, "找不到 %s" % ANIM_PLAYER_PATH)
|
||||
return anim_player
|
||||
|
||||
|
||||
func _instantiate_player() -> Node:
|
||||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||||
assert_true(packed != null, "%s 未能加载" % PLAYER_SCENE_PATH)
|
||||
if packed == null:
|
||||
return null
|
||||
var player := packed.instantiate()
|
||||
track(player)
|
||||
return player
|
||||
|
||||
Reference in New Issue
Block a user