“《永劫无间》的动作与运动系统”笔记 agile Posted on Oct 2 2021 优秀博文 > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/371165024) 前言 -- 最近 Unity 官方请到了《永劫无间》的资深客户端为我们分享了《永劫无间》的动作和运动系统,视频链接如下: [Unity X 永劫无间「Unity 大咖作客」线上分享会 — 动作动画专场【回放】_哔哩哔哩 (゜ - ゜) つロ 干杯~-bilibili](https://www.bilibili.com/video/BV1QN411f7WJ?t=197) 分享里大部分都是一个比较简单的介绍,提供了**动作类游戏**制作过程中的一些思路。作为小白就简单的做一个笔记,万一哪天需要用到相关的知识了呢。 PS:它们做的录制和回放工具看着真的很牛的样子。 动作系统 ---- 《永劫无间》里目前主要有两个英雄体型(男女两种),以及少量的非英雄体型的怪物。因此它们主要使用 Unity 里面的人形骨架系统,也就是 Humanoid 动作。  目前《永劫无间》中动作 State 的数量大概有 2000 多个,AnimationClip 的数量大概有 9000 多个。 两者的区别:举个例子,游戏里有很多不同的武器,都可能对应一个跑步的动画,它们其实是属于同一个 State 的。但是 AnimationClip 的话,不同的武器会单独做它的跑步动画。因此 AnimationClip 会远多于 State 的数量。此外有些 State 本身的实现是 BlendTree,会对应多个 AnimationClip。  《永劫无间》中的动作既有**动捕**也有美术**手工**制作的,动捕使用比较多的是角色受击和死亡的动作,因为这部分的动作希望能够更真实丰富和流畅,所以使用动捕。 然后还使用了较多的**分层动画**,例如目前英雄就有 10 个 Layer:基础层、左右手、头加左右手、上半身、手指层以及逻辑层等,应用于各种场景。比较常见的下半身跑步,上半身做射击瞄准的动作就可以使用分层动画来实现。  动作与角色位移 ------- 做动作系统的时候经常会遇到的一个问题是如何去**控制角色位移**,两种主流的做法是: 1. 用动作本身来进行驱动,也就是开启 RootMotion,即由美术来控制。 2. 程序里面去计算和应用动作的位移,即由程序来控制。  在《永劫无间》里更多的是使用 RootMotion 的方式,因为有经验的动作美术可以把跑步动作做得更加有节奏感,表现的更加真实。所以大部分的移动和攻击类动作的主动权都是交给美术。 程序控制的位移主要应用在下面几种情况: * 重力贴地效果 * 上下坡时候的位移补偿 * 跳跃要根据角色按跳跃键时间的长短来做不同高度的跳跃 * 飞索也有不同方向和不同速度的飞行 * 一些受击的位移需要配置不同的参数来表现不同距离的位移以及不同时间的硬质效果 Playable API ------------ 《永劫无间》使用 Playable API 重新构建底层的动作控制系统代替 Animator 状态机。 官方文档: [https://docs.unity3d.com/Manual/Playables.html](https://docs.unity3d.com/Manual/Playables.html) 自己之前的学习笔记(写的比较简单 =。=): [王江荣:【Unity】简单使用 Playable API 控制动画](https://zhuanlan.zhihu.com/p/380124248) 分享中还推荐了一个不错的插件: [https://assetstore.unity.com/packages/tools/animation/animancer-pro-116514](https://assetstore.unity.com/packages/tools/animation/animancer-pro-116514) 在 Unity 里用 Animator 来构建一个状态机本身是非常方便的,可以直接在 AnimatorController 里面创建一些动作 State,然后添加一些 Transition 的动作过渡连线,设置一些参数和过渡条件,还有给每个 State 配上具体的对应动画,基本上一个可以跑起来的 Animator 状态机就构建好了。 但是这样的方式在非常注重动作的游戏里,应用起来还是有一些困难的,例如前面说到动作 State 有 2000 多个,如果把 2000 多个 State 都放到 AnimatorController 里的话,几乎是**无法维护的**。不管是从编辑速度,还是多人协同角度考虑,用原生的 Animator 都是无法实现的。除此之外还存在着 **Override 的性能热点**,这也是后面高川老师分享的内容,后面再介绍。 Unity 提供了 Playable API,允许开发者进行自定义的,来重新写一套动作控制的系统,有如下几个好处: * 可控制动画加载的策略,可按需加载以及异步加载 * 可以更加灵活的控制 PlayableGraph 的数据流,可插入自定义的 AnimationJob 来做一些特殊的动作机制和动作表现 * 加载自定义的配置数据,能让动作系统更方便和其他游戏系统结合 * 自由度更高的 Override 机制,Animator 本身有一个 Override Animator 的概念,《永劫无间》里做了一个更加高阶的 Override 机制,能够让一些动作进行更加方便的组合和覆盖 当然它也存在一些缺点: * 没有直接使用 Animator(Mecanim)来的直观 * 需要程序员们开发更多的配套工具 * 需要一定的学习成本 动作事件编辑工具 -------- 《永劫无间》里很多内部逻辑是由动作上的事件来进行驱动的,目前已经有大约 160 种动作事件。包括像一些音效特效,打击盒的开关,镜头的控制,武器的显隐,动作的打断点等都是由动作事件进行驱动的。 为了实现这个功能,就重新做一个动作事件编辑的工具,如下图(图看不太清,了解个大概吧):  在这个编辑工具里,可以看到是由很多的轨道进行构成的。不管是策划还是美术,都可以在各个轨道上面添加事件点,像一个简单的攻击动作就可能有二三十个事件。 在这些轨道里面也有一些特殊的轨道,例如第一行的 Enter 轨道,这里面的事件是在进入这个动作的时间点进行触发的。同样会有一个 Exit 的轨道,这个轨道上的事件是在任意退出这个动作的时候进行触发。其他诸如特效事件的特殊轨道等。这样做区分可以使得编辑更加方便,流程控制里也会更加清晰。 此外还会为轨道事件做一些针对性的检查,例如一些轨道事件的参数,检查其范围。然后有些事件只能配置在特定轨道上,有些需要成对配置等。 层次化的动作 Tag 系统 ------------- 《永劫无间》还做了一个层次化的动作 Tag 系统,在 Animator 里面使用的时候,我们可以给动作 State 添加一个 Tag,方便我们做一些批量的逻辑处理,例如下图:  但是很多大型的项目会发现单一的 Tag 是不够用的,因此还需要做了一个多重 Tag 的功能。他们在这个基础上还增加了层次化 Tag 的概念,例如下图:  最上层是最高层级的 Tag,例如运动系统相关动作,战斗系统相关动作。然后中间还有一层 Tag,例如运动中的跳,战斗中的攻击等。最下面一层就是我们的 State。 这么做的好处有: * 可以从不同的力度把动作进行归类 * 新加动作的时候会更加的方便。例如新加一个 walk_02,用多重 Tag 要把它添加到 Walk 和 Movement 里。但是层次化的话,只需要添加到 Walk 里即可,会被自动添加到 Movement 里,提高了编辑效率。 动态的设置动作过渡时间 ----------- 在动作发生过渡的时候,程序可以比较角色当前的 Pose 和目标动作第一帧的 Pose 的差异性,根据这个差异性来动态的设置过渡时间,差异越大那么过渡时间会动态的变得越长。 自动匹配目标动作 -------- 在《永劫无间》中会有不同脚步周期的跑步动作,例如慢跑,快跑等,那么为了让跑步停止时动作衔接变得更加的平滑,就会有很多不同的停止动画与之一一对应。 一开始哪个周期对应哪个动作都是人为配置的,需要很大的精力去调整。后面借鉴了一些现在先进的动作系统技术,比如说 **Motion Matching** 里的一些概念。即按照角色当前的 Pose 去自动的选择一个和它更匹配的动作过渡,这样就不需要手工的去配置。 实现原理:事先对一些动作进行计算,**采样它们骨骼的一些位置和速度**,得到相关数据。然后在运行时根据这些数据去匹配相似的一个 Pose。 Motion Matching 也是《黑悟空》中采用的方案,由于没看过分享视频,贴几个 PPT 截图:  动作后处理 ----- 《永劫无间》做了一个叠加受击的功能,它可以**参数化**的表现一些受击的方向和力度。还有比较常规的 IK 系统,除了台阶上脚步的 IK,还做了爬树动作的 IK 适配等。 运动系统 ---- 前面提到的动作系统可以认为是一个底层的功能,在这个功能上我们可以构建运动系统,战斗系统等。 《永劫无间》里角色的运动类型非常的丰富,玩家也能发现里面有很多的跑酷元素,运动类型主要分如下三种: * 基础的运动类型有:走,跑,疾奔,跳跃,蹲伏,滑铲等 * 特殊的运动类型有:攀爬,贴墙,索梁,飞檐走壁,爬树等 * 飞索系统:全场景可交互,衔接各种基础运动和特殊运动 特殊运动的触发方式 --------- 特殊的运动系统需要一些逻辑上的触发方式,例如什么墙我可以飞檐走壁,什么树我可以爬上去,常见的做法有两种: 第一种做法是做离线的标注,我们需要离线的去标注哪些地方是屋檐,哪些地方是索梁,哪些地方可以爬树等等。利用了 Unity 物体引擎的 **Trigger** 系统来标注哪些区域可以触发。 第二种做法是在运动时实时检测周边的环境达成的,例如飞檐走壁,跑步时自动翻越一些小障碍,还有就是飞索的运动。一般利用射线检测的方式。 两种方式有各自的优缺点,离线标注的可控性和精确性高,但是工作量很大。运行时的物理检测灵活性高,但是性能开销较大。两者需要进行平衡。 ProBuilder 的运用 -------------- ProBuilder 我们可以在 Unity 的 Package Manager 里找到它:  官方文档: [About ProBuilder](https://docs.unity3d.com/Packages/com.unity.probuilder@5.0/manual/index.html) ProBuilder 在《永劫无间》里主要应用在如下三个方面: 1. 设计关卡原型(构建流程如下图),其中场景的白模搭建基本上是有 ProBuilder 来实现的。《永劫无间》需要很多复杂的场景,例如可以攀爬的树木,可以飞檐走壁的墙体,用于 FPS 玩法的掩体,这些都需要不断的迭代优化。  2. 构建测试场景,例如运动触发区,障碍等,策划搭建一些不同的规格的物体在一个小的测试场景里,这样可以方便程序开发和 QA 测试。  3. 辅助运动触发区设置,开发 ProBuilder 插件,用于运动触发区的制作。使用 ProBuilder 可以比较方便的选取模型当中的一些点、线和面,例如数目可以直接选择模型的面,屋檐选择模型边缘的线。然后通过一些运算,来制作出 Trigger,这样做比人工来摆放和对齐效率要高很多。  运行时周边环境检测 --------- 运行时周边环境检测主要使用的是 unity 的物理系统,应用范围主要有如下几种: 1. 地面坡度检测,检测到地面坡度的话就可以用它来做上下坡动作融合。 2. 自动跨越小障碍,在人物前方使用多条射线来判断是否有小的障碍,来进行跨越动作。 3. 飞檐走壁以及贴墙动作,同样利用射线来判断人物前方是否有墙,然后通过墙的法线来判断该墙是否允许做飞檐走壁的动作。 4. 跳跃目标点连通性检测。 要实现上述这些检测,就需要利用物体引擎提供的一些接口,例如: * [RayCast](https://docs.unity3d.com/ScriptReference/Physics.Raycast.html):打出射线判断 Colliders * [SphereCast](https://docs.unity3d.com/ScriptReference/Physics.SphereCast.html):与 RayCast 不同的是,SphereCast 是往射线方向打出一个球体,然后来判断 Colliders * [OverlapSphere](https://docs.unity3d.com/ScriptReference/Physics.OverlapSphere.html):获取所有接触到球或在球内的 Colliders * [OverlapCapsule](https://docs.unity3d.com/ScriptReference/Physics.OverlapCapsule.html):获取所有接触到胶囊体或在胶囊体内的 Colliders 性能问题的优化: * 控制好检测频率 * [autoSyncTransforms](https://docs.unity3d.com/ScriptReference/Physics-autoSyncTransforms.html) 的开关,这个开关用来控制当 Transform 发生改变时是否在物理引擎里是否进行同步。如果勾选,那么当 Transform 发生改变时,其自身和 children 的 Rigidbody 和 Collider 组件都会同步更新,可能会造成一个性能的峰值。若设为 false,则只会在 FixedUpdate 期间的物理模拟步骤前进行同步。一般我们设为 false,在需要的时候手动调用 Physics.SyncTransforms 方法来触发。 * 用好 Profiler 来看下我们的物理检测是否是当前的瓶颈。 物理数据管理 ------ 《永劫无间》目前的规则是,场景物件 Prefab 在编辑状态下,它的物理和视觉资源是在同一个原始 Prefab 的。但是在运行前,程序会自动化的**将原始 Prefab 拆分成视觉 prefab 和物理 prefab**。如下图:  这样就可以使用不同的加载策略加载这两种 prefab。例如一些视觉的 prefab 离人物很远的时候就需要加载,但是物理的 prefab 相对而言就可以在离人物较近的时候在进行加载。 除此之外,对于物理数据(例如碰撞信息,运动触发区信息)会日常的进行导出。然后在服务器上也跑了一个物理引擎(PhysX),通过导出的数据来进行一些物理查询和模拟的服务。 存着的问题以及优化方向 ----------- 1.Trigger 的误触发问题,例如游戏里有些屋檐,如果角色跳在空中并且朝向这屋檐,那么就可以直接抓上去,大部分情况下玩家想上屋檐就会很方便,大大降低了用户交互。但是有些情况下,例如逃跑时,预期不是去抓屋檐,然而却造成了抓到屋檐上去的结果。为了避免这样的情况,就需要改变部分交互方式,更加准确的识别玩家的操作意图。例如新增按钮,但是这样会使得交互复杂,后续可能尝试使用按键长短按的方式来判断。 2. 运动的流畅性和多样性,程序过程化动画,使用更轻量级的新增动作方法。 3. 运动同步的优化,《永劫无间》主要是基于位置同步的,测试结果来看,在玩家的网络较好没啥问题。但是当网络较差时,还需要进下进一步的优化。 如果是 RootMotion 的话,有个问题需要解决,比如说除了位移之外还有一些角色转向的同步。如果转向动作是美术用做的,RootMotion 按照一些特定的曲线来转向的话,在网络同步之后会存在一定的问题,因此在同步时,需要客户端本地模拟来优化。 配套工具开发 ------ 为了提高开发效率与协调性,往往需要开发很多的工具。《永劫无间》团队制作了诸如动作录制和回放的工具,工作流程图工具,层次化 Tag 编辑工具,可视化连招图工具,可视化运动触发区调试工具,离线动作采样工具等等大量的工具。 动作录制和回放的工具 ---------- 动作录制和回放的工具,在永劫无间的动作和运动系统里扮演了非常重要的一个角色。 具体流程如下:点击开始录制,然后可以对游戏内的角色进行一些运动和动作的操作,录制完成后就可以在 Timeline 上进行一个逐帧的回放,如下图:  点击每帧,就能够观察到这一帧里角色运动和动作的详情了。其中包括动作的构成和权重,动作的位移,动作的过渡详情,动作的输入事件,以及在这一帧里到底经历了哪些动作的 State 等等。 只能说,很牛啊!!! 实现原理:在一些合适的时机把一些动作内部的状态给记录下来。比如说我们知道 Animator 大部分情况下是在 LaterUpdate 之前,Update 之后去处理它的数据的。那么我们可以在 LaterUpdate 开头去记录下这个 Animator 对应的 PlayableGraph 里面它的每个 clip 运行到什么状态以及它的权重是多少等等,这些数据的获取在 Playable API 里都有提供。这样就可以吧人物动作主要的内部状态给记录下来。 有了这样的工具,程序的调试就会变得更加的简单,它可以不用再依赖于打各种的日志,跑各种断点,而是通过录制这样一个能够回放的文件,来观察一些动作系统内部的状态。 此外它本身可以保存成一个文件分享给团队其他人员,这样 QA 在测试的时候,发现问题可以直接给程序反馈该文件,让程序进行调查,方便处理一些难以复现的动作问题。 此外团队后期还增加了更多的功能,例如 **IK 的分析**。记录了每个角色的动作中间状态,添加了一些开关可以观察在 IK 开启和关闭时,人物的不同状态,如下图:  这样就能够分析 IK 是否达到了期望的目的。QA 在处理这个问题的时候也能更加方便的观察出到底是动作本身的问题还是 IK 导致的问题。 此外还有**动作流畅性的分析**,如下图:  按一定的算法(分析相邻帧的差异)计算动作的流畅性,然后会在一个曲线上将流畅性展现出来。 曲线的峰值一般代表这个地方有不流畅的动作发生,例如前后帧可以发现角色发生了较大的动作跳变,如下图,前后两帧的动作有明显的差异:  我们就可以分析它到底是由什么原因引起的,比如说是否是过渡时间设置的过短了,并且提供了编辑界面可以用于调整过渡时间,如下:  这样就为团队提供了一个比较客观的评估动作流畅性的功能,并且快速的定位到其问题。 工作流程图工具 ------- 整个动作和运动系统的工作流其实是蛮长的,不同的职能职位也有不同的工作,该工具把所有的工作节点给可视化的展现在一个工作流程图里,如下图:  同时也对于其中用到的一些工具进行了快捷的跳转。这样不管是老员工还是新入职的员工运用游戏里的工具做动作系统相关的配置就能够有比较一个清晰的了解,以及自己应该做哪一部分。 经验总结 ---- 1. 对于复杂的配置数据,应该考虑将编辑状态下的数据和运行状态下的数据进行分离。 对于编辑状态下的数据需要做到高可读写,版本管理更加友好,协调编辑更加友好,数据方面可以有些冗余,可以自由的添加一些配置。但是运行状态的数据要从性能方面进行考虑,**低冗余、高性能**。 例如对动画进行引用的时候,在运行状态下应该直接使用它的 Hash,即 int 型的数据。而编辑状态下可以是用字符串(动画名称)来表达的。 2. 游戏开发时经常讨论到热更新、热加载,大部分都是指游戏上线之后的站在玩家角度的更新,其实在开发的时候我们应该也尽量做到数据在运行时进行重载。 比较好的方式就是使用 Unity 的 [ScriptableObject](https://docs.unity3d.com/ScriptReference/ScriptableObject.html) 作为数据载体,它支持运行时修改,且立即生效。此外一些自定义的数据格式也应该尽量的支持热加载。 3. 推进工具在项目内使用比开发工具本身更重要。 4. 注重文档建设和规范性检查。 路径追踪(Path Tracing)与渲染方程(Render Equation) Unity中Animator Override的性能问题