背景
PR #2984 把 AnimatorStatePlayData 升级为"per-Animator per-state 持久 handle"模型,每个 state 在每个 layer 内只有一个 PlayData,承载两类数据:
per-instance override(如 _speed)
runtime track 状态(_playedTime / _clipTime / _playState / _currentEventIndex / _isForward / _offsetFrameTime 等)
当前限制
由于一个 state 对应一份 PlayData,crossFade 到当前 src 或 active dest 时会让 srcPlayData === destPlayData,runtime 字段被双重更新:
_playedTime 在 src track 和 dest track 路径各加一次
_currentEventIndex 在 dest track 路径 reset,事件可能重复触发
_clipTime 在两条路径写入互相覆盖
为避免数据损坏,_prepareCrossFadeByTransition 加了 alias guard 把这种调用变成 no-op(Animator.ts:1454-1466,commit 9f2992b49 / 注释 5a2cd7417 / 15dd5b4b8):
if ( animatorLayerData . srcPlayData ?. state === crossState
|| animatorLayerData . destPlayData ?. state === crossState ) {
return false ;
}
失去的能力
self-restart 平滑过渡 :受击 / 重击 / 特定事件触发的"重新进入当前 state + 短 cross-fade"用户调 crossFade(currentState, 0.2) 期望平滑重启 → 当前直接 no-op
重定向到 active dest :cross-fade 进行中改变目标(如 Walk → Run 进行中又切到 Run 的新一份)→ 当前直接 no-op
同 state 多实例重叠 :多发射弹道 / 多个手部动画叠加 → 当前模型不支持
Unity 对照
Unity Mecanim 用 transient Playable + PlayableGraph 模型,不会出现 alias:
维度
Unity Mecanim
Galacean 当前
state asset 跟播放实例
1 asset : N playable(多 instance 同时活)
1 asset : 1 handle
播放实例生命周期
transient — 每次 play/crossFade 新建
persistent — layerData 生命周期
per-instance 状态
parameter 外置(name 绑定 + 乘法叠加)
handle 上的 _speed 字段
Self-crossFade
自然支持(新建 playable,旧 playable 继续衰减)
必须 alias guard no-op
Unity animator.CrossFade("Walk", 0.3) 当 Walk 已在播 → 旧 Walk playable 继续运行,新建一个 Walk fresh playable,mixer 在两者间渐变。两个 Walk playable 是不同对象,无 alias。
提议方案 — 拆两层数据所有权
现在:
AnimatorStatePlayData (1 per state, persistent)
├─ _speed // override
├─ _playedTime / _clipTime / _playState / _currentEventIndex / _isForward / _offsetFrameTime
└─ _stateData
拆成:
AnimatorStatePlaybackHandle (1 per state, persistent)
└─ _speed // per-instance override
AnimatorStatePlayTrack (N per state, transient)
├─ _playedTime / _clipTime / _playState / _currentEventIndex / _isForward / _offsetFrameTime
├─ _stateData
└─ → 关联到对应的 PlaybackHandle 拿 override
调用点:
srcPlayData / destPlayData 改为指 AnimatorStatePlayTrack(transient,每次 play / crossFade 新建)
移除 alias guard,crossFade(X→X) 时 src 是旧 track,dest 是新 track,两个对象天然不同
findAnimatorState 返回 AnimatorStatePlaybackHandle(公开 API surface 不变,承载语义不变)
override 仍按 state.name 索引 PlaybackHandle,跨 transition 保留
取舍
收益 :
self-crossFade 完整支持
runtime 状态跟 override 解耦,未来加更多 per-track 字段不影响 override
src/dest 不再可能 alias,模型本身排除一类 bug
代价 :
每次 play / crossFade 新建一个 PlayTrack 对象(脱离 lazy 暖机模型)
LayerData 数据结构变(statePlayDataMap 变 stateHandleMap + 一组 active tracks)
Animator.ts 内 ~30 处访问点要分清访问 "override" 还是 "runtime"
测试要重写(特别是 cross-fade 相关)
范围
不在本 PR 内做。本 PR (#2984 ) 的工程取舍是 alias guard no-op + 注释明确"这是设计语义而非临时兜底",给未来重构留好回放点。
引用
当前 alias guard 实现: packages/core/src/animation/Animator.ts:1454-1466
设计语义注释: packages/core/src/animation/Animator.ts:1460-1463
测试锁定当前 no-op 行为: tests/src/core/Animator.test.ts (crossFade to current state is no-op, state-machine self-transition is also a no-op)
相关 PR: fix(animation, loader): 从 #2983 抽离动画与 GLTF 加载器修复 #2984
背景
PR #2984 把
AnimatorStatePlayData升级为"per-Animator per-state 持久 handle"模型,每个 state 在每个 layer 内只有一个 PlayData,承载两类数据:_speed)_playedTime/_clipTime/_playState/_currentEventIndex/_isForward/_offsetFrameTime等)当前限制
由于一个 state 对应一份 PlayData,
crossFade到当前 src 或 active dest 时会让srcPlayData === destPlayData,runtime 字段被双重更新:_playedTime在 src track 和 dest track 路径各加一次_currentEventIndex在 dest track 路径 reset,事件可能重复触发_clipTime在两条路径写入互相覆盖为避免数据损坏,
_prepareCrossFadeByTransition加了 alias guard 把这种调用变成 no-op(Animator.ts:1454-1466,commit9f2992b49/ 注释5a2cd7417/15dd5b4b8):失去的能力
crossFade(currentState, 0.2)期望平滑重启 → 当前直接 no-opUnity 对照
Unity Mecanim 用 transient Playable + PlayableGraph 模型,不会出现 alias:
_speed字段Unity
animator.CrossFade("Walk", 0.3)当 Walk 已在播 → 旧 Walk playable 继续运行,新建一个 Walk fresh playable,mixer 在两者间渐变。两个 Walk playable 是不同对象,无 alias。提议方案 — 拆两层数据所有权
调用点:
srcPlayData/destPlayData改为指AnimatorStatePlayTrack(transient,每次 play / crossFade 新建)findAnimatorState返回AnimatorStatePlaybackHandle(公开 API surface 不变,承载语义不变)取舍
收益:
代价:
statePlayDataMap变stateHandleMap+ 一组 active tracks)范围
不在本 PR 内做。本 PR (#2984) 的工程取舍是 alias guard no-op + 注释明确"这是设计语义而非临时兜底",给未来重构留好回放点。
引用
packages/core/src/animation/Animator.ts:1454-1466packages/core/src/animation/Animator.ts:1460-1463tests/src/core/Animator.test.ts(crossFade to current state is no-op,state-machine self-transition is also a no-op)