动画图插件的开发总结
本文简单列出了动画图插件的目标、实现思路、开发过程中遇到的一些问题以及开发后的结论,作为备忘。
注意:此插件并不完善,仅供参考,请勿直接用于产品。
插件地址:Puppeteer
目标
- 通过 Playable + GraphView 实与UE AnimationBlueprint中的AnimGraph类似的动画图,替代Animator Controller:
- 各种Mixer节点(用于任意姿态叠加)和BlendSpace节点
- LayerMixer节点(用于骨骼Mask)
- 自定义动画节点(
IAnimationJob
,用于逐骨骼动画控制) - 动画状态机
- LayerLink(引用子图层,不同于动画中LayerMask的Layer)
- 【未实现】PoseCache(用于复用姿态,预期使用
AnimationScriptPlayable
实现) - SubGraphLink(引用其他动画图资产)
- 【未实现】运行时动态替换节点动画Clip
- 【未实现】按需加载动画资产,而不立即加载整个图中的所有动画资产
- 通过 Unity Visual Scripting(Bolt) 插件实现与UE AnimationBlueprint中的EventGraph类似的动画逻辑控制:
- 动画图公开动画控制API,供外部调用
- 自定义Bolt节点,简化动画图控制
实现思路
- Editor
- 使用
GraphView
构建动画图编辑器
- 使用
- Runtime
- 使用
ScriptableObject
存储动画图数据:- 所有参数平铺存储,由Guid索引
- 所有节点平铺存储,由Guid索引
- 所有图层平铺存储,由Guid索引
- 每个Editor节点在Runtime有2个对应的Playable节点:
- 脚本Playable节点:控制节点自身状态(包括输入权重、状态机逻辑等)
- 脚本Playable节点不是必须的,也可以自己在其他位置实现节点状态控制逻辑,只要保证能在
PrepareFrame
时期按照前序遍历顺序完成节点状态更新即可
- 脚本Playable节点不是必须的,也可以自己在其他位置实现节点状态控制逻辑,只要保证能在
- 动画Playable节点:实现动画逻辑(资产播放、
IAnimationJob
等)
- 脚本Playable节点:控制节点自身状态(包括输入权重、状态机逻辑等)
- 初始化流程:
- 收集动画图参数等可能用到的数据,并将其注册到对应的查找表中
- 遍历LayerLink和SubGraphLink,并将其注册到对应的Graph表
- 遍历节点,创建节点和初始化节点,将节点注册到节点表
- 再次遍历节点,重建节点的连接关系
- Tick流程:
- 由
PlayableGraph
驱动整个动画图的Tick流程 - 先前序遍历脚本Playable节点(
PrepareFrame
),完成图节点状态更新 - 再后续遍历动画Playable节点(
ProcessFrame
),完成动画逻辑
- 由
- 自定义动画节点:
- 使用
IAnimationJob
实现自定义的姿态和跟运动控制逻辑 - 动画图初始化时,收集可能用到的骨骼数据(
TransformStreamHandle
),供自定义动画控制逻辑使用 - 动画图初始化时,收集可能用到的Curve等其他数据(
PropertyStreamHandle
),供自定义动画控制逻辑使用
- 使用
- 动画状态机:
- 状态机中的每个状态是一个LayerLink,作为状态机节点的输入,切换状态即是调整LayerLink的输入权重
- 在脚本Playable中完成状态机的状态控制逻辑(条件检查、权重过渡等)
- LayerLink和SubGraphLink:
- 运行时会将所链接的子图层展开到PlayableGraph中
- PoseCache:
- 使用
IAnimationJob
实现读取输入的姿态数据,缓存到NativeArray<T>
或NativeHashMap<T,V>
中,供使用方读取
- 使用
- 使用
缺陷
- 复杂动画的性能问题:
- 仅靠Unity提供的动画Playable无法实现精准的动画控制,必然要大量使用
IAnimationJob
IAnimationJob
会频繁的从/向AnimationStream
中读/写数据,这一操作开销很高- 脚本Playable影响
PlayableGraph
的多线程计算(ScriptingObjectPtr
不为空的PlayableGraph
被标记为了不可多线程处理,但不确定实际执行逻辑)
- 仅靠Unity提供的动画Playable无法实现精准的动画控制,必然要大量使用
- Unity与UE的底层动画处理逻辑不同,造成了额外的限制:
- Unity动画Playable中能够访问到的数据比UE的
FAnimNode_Base
中能够访问到的数据少很多- 动画逻辑中只能通过
IAnimationJob
访问纯值类型的数据,限制颇多,也会进一步加剧前面提到的性能问题
- 动画逻辑中只能通过
- UE的AnimGraph和Montage只输出各自的最终姿态和跟运动,由
USkeletalMeshComponent
组件将两者的数据应用到骨骼上;而Unity的PlayableGraph和Timeline会各自直接将自身产生的姿态和跟运动数据应用到骨骼,导致两者没法无缝切换- 【可能的解决方案】在Timeline中实现一个自定义动画Track,此Track不直接在Timeline中播放动画,而是控制动画图去动态地增加一个动画Playable分支并过渡到这个分支
- Unity动画Playable中能够访问到的数据比UE的
- Unity Visual Scripting(Bolt)插件的性能问题:
- 要实现类似UE的Event Graph功能,动画图需要公开动画控制API,自定义相应的Bolt节点
- Bolt的性能比脚本代码差很多,但若不使用Bolt,直接写代码,那可视化的动画图的意义就被弱化了
- Bolt的使用体验(连图思路、图结构管理等方面)比UE的Event Graph差很多,这种情况下,能把控制复杂的动画逻辑的Bolt ScriptGraph管理好的人,代码水平想必不会差,不如直接写代码
- 动画简单的游戏用不上动画图,动画复杂的游戏,为何不用UE?
可优化项
- 接口隔离
- 动画节点的序列化数据与运行时逻辑分离
- 运行时动画图剪枝(仅暴露活动节点,删除状态机的非活跃输入分支)
替代方案
- 封装Playable接口,提供一个具有基础的“动画Layer管理+动画节点管理+动画混合管理”功能的通用组件,项目的具体动画逻辑完全使用代码控制,不使用图形化编辑:
- 提供可视化的动画调试工具,辅助调试: