Unity3D Mecanim 中 Transition 的 Atomic 属性是个什么鬼

我们游戏中一直有一个让我非常恼火的问题,关键是查了已经无数次了,根本尼玛不屌我啊,像我如此这般坚强的程序猿都要拜倒了。谁知前几天再次出现,因为已经把 Log 打得有点天罗地网的意思了,而且还持久化到磁盘 Log 文件上了,这下也不担心查着查着突然被其他的事情打断之后,然后再次回来想查问题的时候发现 Log 已经不见了,尼玛不是说好的要保护第一现场的咩?

不扯淡了,先说这个问题是啥。我们的游戏中,对于战斗 NPC 的动作都加了 AnimationEvent 回调,游戏中很多逻辑都依赖于 AnimationEvent 的回调,例如战斗 NPC 释放了一个技能,那么在这个技能动画刚刚开始播放和即将结束的某个时间点,会分别调用 OnSkillStart 和 OnSkillEnd 这两个 AnimationEvent,在这两个回调函数中会有相应的处理逻辑,例如在 OnSkillStart 中给 NPC 加上霸体的 Buf,在 OnSkillEnd 中将 NPC 的 Buf 取消。我碰到的诡异现象是这样的,NPC 的技能 A 的动画时间长度为 3 秒,OnSkillStart 方法和 OnSkillEnd 方法分别处于整个动画时间轴的 0.2 秒和 2.8 秒,NPC 在 AI 判断确定释放技能 A,在技能 A 动画播放到 0.1 秒的时候,受到了主角的攻击,此时 NPC 应该立即切换到受击状态,开始播放受击动画,但是事实上却不是这样,我们能看到的日志信息如下:

10:34:05.565 AM ya_zhang_tie_qi_jun(Clone)001 TriggerSkill ya_zhang_tie_qi_jun_attack_3

10:34:05.573 AM #Network# GameClient Emit a Message: [Action, Ping]

10:34:05.667 AM Player OnSkillStart: player_female_suplex_back
10:34:05.792 AM Player OnSkillTrigger (player_female_suplex_back#0)
10:34:05.799 AM ya_zhang_tie_qi_jun(Clone)001 OnHookAttackHit by Player with player_female_suplex_back#0
10:34:05.803 AM ya_zhang_tie_qi_jun(Clone)001 Hit to Hooking from None
10:34:05.817 AM ya_zhang_tie_qi_jun(Clone)001 OnSkillStart: ya_zhang_tie_qi_jun_attack_3
10:34:06.297 AM GameClient Received a Message: [Action, Ping]

10:34:06.338 AM Player OnSkillTrigger (player_female_suplex_back#1)
10:34:06.345 AM ya_zhang_tie_qi_jun(Clone)001 OnSuplexAttackHit by Player with player_female_suplex_back#1
10:34:06.352 AM ya_zhang_tie_qi_jun(Clone)001 is SuperArmored, cannot be Suplex
10:34:06.359 AM ya_zhang_tie_qi_jun(Clone)001 ApplySuperArmoredFX
10:34:06.744 AM ya_zhang_tie_qi_jun(Clone)001 CancelSuperArmoredFX
10:34:06.859 AM Player OnSkillEnd: player_female_suplex_back
10:34:08.589 AM #Network# GameClient Emit a Message: [Action, Ping]

从日志里头看到的信息就是这个牙帐铁骑军已经释放了一个技能,但是在其技能动画播放到 OnSkillStart 的时间点时,已经被主角攻击了,此时其并未立即切换到受击状态,而是继续播放其技能动画并且触发了 OnSkillStart 的回调。所以游戏的逻辑就出问题了,NPC 因为触发了 OnSkillStart 回调进入霸体状态了,然后主角后续的所有攻击都无法让 NPC 做出相应的受击动作了,关键是这个 NPC 在回调了 OnSkillStart 函数进入霸体之后呢,还是会进入到对应的受击状态,这下就奇了怪了。你要么就不进入受击状态呗,要么就别触发霸体啊,这不是要疯吗。

最后仔细分析排查,知道了这个问题是因为 OnSkillStart 的触发时间点处于动画过渡的时间区间内,也就是说 NPC 从 Idle 状态进入到技能 Attack_3 的状态时间长为 0.3 秒,并且这个动画的过渡 Atomic 属性为 True。问题就这么来了,NPC 在从 Idle 状态时释放技能 Attack_3,动画从 Idle 切换到 Attack_3,动画切换到 0.1 秒的时候,受到了主角的攻击,NPC 的整个 AnimatorController 状态图中,受击状态都是直接从 Any State 跳转过去的。按理来说就应该在任意状态下都能切换到受击状态才对啊,为啥动画还会继续往后播放呢,直到 OnSkillStart 回调完了之后再切换到受击状态呢?

最终发现了就是这个 Atomic 属性给闹的,这个 Atomic 属性的字面意思已经很清楚了,那就是当它为 True 的时候,这个过渡是原子的,也就是说只要进入了这个过渡,那么这个过渡就一定会播放完成,不论是发生任何情况这个过渡都会播放完成,而刚好我们的这个 OnSkillStart 回调函数所处的时间点就在这个过渡时间段里头,所以就出现了,虽然最终 NPC 还是进入了受击状态,但是在进入受击状态之前因为已经回调了 OnSkillStart 函数,所以导致逻辑上出现了错误。

这个问题呢,有两个解决方法:

  1. 取消所有的动画状态切换之间的过渡时间,让动画之间的切换不再有过渡的过程,都是直接切换,这样就可以避免出现过渡的时候触发 AnimationEvent 了;
  2. 将牙帐铁骑军的 AnimatorController 中从 Idle 到 Attack_3 状态的 Transition 的 Atomic 选项取消,允许该 Transition 被打断,这个解决方法看上去更有技术含量一些,也更符合我们设计意图,当我们不论处于一种什么状态的时候,受击之后就应该立刻切换到某个受击状态下,这个从语义上也很能说得通,对伐?