【Unity】动画图插件的开发总结

动画图插件的开发总结

本文简单列出了动画图插件的目标、实现思路、开发过程中遇到的一些问题以及开发后的结论,作为备忘。

注意:此插件并不完善,仅供参考,请勿直接用于产品。

插件地址: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节点:实现动画逻辑(资产播放、 IAnimationJob 等)
    • 初始化流程:
      • 收集动画图参数等可能用到的数据,并将其注册到对应的查找表中
      • 遍历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与UE的底层动画处理逻辑不同,造成了额外的限制:
    • Unity动画Playable中能够访问到的数据比UE的 FAnimNode_Base 中能够访问到的数据少很多
      • 动画逻辑中只能通过 IAnimationJob 访问纯值类型的数据,限制颇多,也会进一步加剧前面提到的性能问题
    • UE的AnimGraph和Montage只输出各自的最终姿态和跟运动,由 USkeletalMeshComponent 组件将两者的数据应用到骨骼上;而Unity的PlayableGraph和Timeline会各自直接将自身产生的姿态和跟运动数据应用到骨骼,导致两者没法无缝切换
      • 【可能的解决方案】在Timeline中实现一个自定义动画Track,此Track不直接在Timeline中播放动画,而是控制动画图去动态地增加一个动画Playable分支并过渡到这个分支
  • Unity Visual Scripting(Bolt)插件的性能问题:
    • 要实现类似UE的Event Graph功能,动画图需要公开动画控制API,自定义相应的Bolt节点
    • Bolt的性能比脚本代码差很多,但若不使用Bolt,直接写代码,那可视化的动画图的意义就被弱化了
    • Bolt的使用体验(连图思路、图结构管理等方面)比UE的Event Graph差很多,这种情况下,能把控制复杂的动画逻辑的Bolt ScriptGraph管理好的人,代码水平想必不会差,不如直接写代码
  • 动画简单的游戏用不上动画图,动画复杂的游戏,为何不用UE?

可优化项

  • 接口隔离
  • 动画节点的序列化数据与运行时逻辑分离
  • 运行时动画图剪枝(仅暴露活动节点,删除状态机的非活跃输入分支)

替代方案

  • 封装Playable接口,提供一个具有基础的“动画Layer管理+动画节点管理+动画混合管理”功能的通用组件,项目的具体动画逻辑完全使用代码控制,不使用图形化编辑:
  • 提供可视化的动画调试工具,辅助调试:

猜你喜欢

转载自blog.csdn.net/qq_21397217/article/details/128800052