Unity3D RPG Core | 13 设置敌人的动画控制器

这节视频完善史莱姆敌人的追击逻辑,并播放相应的动画。

在此之前,我们为史莱姆对象创建预制体,以及更新角色预制体。之前为角色再单独创建一个预制体时,就有疑惑为什么要再创建一个。现在找到原因了,因为在原先预制体上,我们增加了许多组件,比如 Amimator 和 Nav Mesh Agent。创建预制体后,这些添加的组件和属性就也能复用了。

创建预制体的一个原因。

为史莱姆对象创建预制体和之前的操作一样,只需要把现有 Hierarchy 窗体中的史莱姆对象拖拽到 Assets 窗体中就可以了。

更新已有预制体,我们可以在预制体对应的 Inspector 窗体中的 Prefab - Overrides 下,点击 Apply All,更新所有修改。

1. 追击逻辑

如代码清单 1 所示,上一节中我们在发现角色后会将敌人的状态设置为追击状态(第 27 行),现在我们开始完成追击逻辑。

我们改造之前的寻找角色函数,将返回值修改为角色的 GameObject(第 49 至 59 行)。追击的逻辑先简单实现,追击时如果找到角色,则将 NavMeshAgent.destination 设置为角色的位置(第 37 行);如果没找到角色,即脱战了,敌人需要回到原来的位置,这块逻辑留作之后实现。

视频里还实现了一个小细节,敌人追击时候的速度为设置的默认值(第 34 行),当巡逻时速度减慢(第 31 行)。

代码清单 1 追击逻辑
  1. [RequireComponent(typeof(NavMeshAgent))]
  2. public class EnemyController : MonoBehaviour
  3. {
  4.     public EnemyStates m_enemyStates;
  5.     NavMeshAgent m_navMeshAgent;
  6.  
  7.     public float m_sightRadius = 5;
  8.  
  9.     float m_defaultSpeed;
  10.  
  11.     void Awake()
  12.     {
  13.         m_navMeshAgent = GetComponent<NavMeshAgent>();
  14.         m_defaultSpeed = m_navMeshAgent.speed;
  15.     }
  16.  
  17.     void SwitchState()
  18.     {
  19.         GameObject attackObject = null;
  20.  
  21.         switch (m_enemyStates)
  22.         {
  23.             case EnemyStates.GUARD:
  24.                 if (FindPlayer() != null)
  25.                 {
  26.                     Debug.Log("HasFoundPlayer");
  27.                     m_enemyStates = EnemyStates.PURSUIT;
  28.                 }
  29.                 break;
  30.             case EnemyStates.PATROL:
  31.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  32.                 break;
  33.             case EnemyStates.PURSUIT:
  34.                 m_navMeshAgent.speed = m_defaultSpeed;
  35.                 if ((attackObject = FindPlayer()) != null)
  36.                 {
  37.                     m_navMeshAgent.destination = attackObject.transform.position;
  38.                 }
  39.                 else
  40.                 {
  41.  
  42.                 }
  43.                 break;
  44.             case EnemyStates.DEATH:
  45.                 break;
  46.         }
  47.     }
  48.  
  49.     GameObject FindPlayer()
  50.     {
  51.         Collider[] colliders = Physics.OverlapSphere(transform.position, m_sightRadius);
  52.         foreach (Collider collider in colliders)
  53.         {
  54.             if (collider.CompareTag("Player"))
  55.                 return collider.gameObject;
  56.         }
  57.         return null;
  58.     }
  59. }

写完代码后,回到 Unity 窗体中运行验证。可以看到敌人发现角色后,如预期跟着角色,拉开距离后便呆在原地。

2. 设置动画控制器

目前还缺少史莱姆相关的动画,我们为其新建并指定一个动画控制器。首先如图 1 所示,在默认的 Base Layer 下,代表史莱姆守卫和巡逻状态下的动画。我们添加素材中的 IdleNormal 和 WalkFWD 动画,分别代表待机和行走状态。两者之间的状态转移通过 bool 变量控制,这边我们新建 bool 变量 Walking。同时去掉 Has Exit Time、Fixed Duration 的勾选,Transition Duration 设置为 0。

图1 设置守卫和巡逻状态下的动画

接着如图 2 所示,点击加号按钮,再创建一个新层,命名为 Attack Layer,它代表追击和攻击状态下的动画。层设置界面上,我们将权重设置为 1,融合方式设置为 Override,即新的层和原有的层是覆盖关系。这边我们有三个状态,一个空状态 BaseState、一个战斗待机状态 IdleBattle 以及奔跑 RunFWD。空状态代表其他层的任意状态。BaseState 和 IdleBattle 状态之间的转移通过 bool 变量 Pursuing;IdleBattle 和 RunFWD 状态之间的转移通过 bool 变量 Following

图2 设置攻击状态下的动画

这边尝试理解层之间的切换逻辑:此例中 Pursuing 和 Walking 是触发层切换的;空的状态代表其他层任意状态。

有一个问题,Base Layer 切到 Attack Layer,Attack Layer 中会有一个空状态。为什么 Attack Layer 切到 Base Layer,Base Layer 中没有空状态。

设置完动画控制器和相关转移变量后,如代码清单 2 所示,我们实现动画的切换逻辑。首先获取史莱姆对象上的 Animator 组件(第 24 行);接着定义对应的动画转移变量,此处定义了 AnimatorState 结构体,转移变量分别对应 isWalking、isPursuing 和 isFollowing(第 11 至 19 行)。

动画切换的逻辑后续可以慢慢实现,因为我们逐帧更新动画对应的变量:在 Update() 中调用 SwitchAnimation() 函数。

我们先实现追击状态下的动画切换逻辑,此时需要切换到 Attack Layr,所以设置 Walking 为 false,Pursuing 为 true。能发现角色时,设置奔跑状态,Following 为 true;否则为待机状态,Following 为 fals。

代码清单 2 动画切换逻辑
  1. [RequireComponent(typeof(NavMeshAgent))]
  2. public class EnemyController : MonoBehaviour
  3. {
  4.     public EnemyStates m_enemyStates;
  5.     NavMeshAgent m_navMeshAgent;
  6.     Animator     m_animator;
  7.     public float m_sightRadius = 5;
  8.  
  9.     float m_defaultSpeed;
  10.  
  11.     struct AnimatorState
  12.     {
  13.         // Base Layer
  14.         public bool isWalking;
  15.         // Attack Layer
  16.         public bool isPursuing;
  17.         public bool isFollowing;
  18.     }
  19.     AnimatorState m_animatorState;
  20.  
  21.     void Awake()
  22.     {
  23.         m_navMeshAgent = GetComponent<NavMeshAgent>();
  24.         m_animator     = GetComponent<Animator>();
  25.         m_defaultSpeed = m_navMeshAgent.speed;
  26.     }
  27.  
  28.     // Update is called once per frame
  29.     void Update()
  30.     {
  31.         SwitchState();
  32.         SwitchAnimation();
  33.     }
  34.  
  35.     void SwitchAnimation()
  36.     {
  37.         m_animator.SetBool("Walking", m_animatorState.isWalking);
  38.         m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
  39.         m_animator.SetBool("Following", m_animatorState.isFollowing);
  40.     }
  41.  
  42.     void SwitchState()
  43.     {
  44.         GameObject attackObject = null;
  45.  
  46.         switch (m_enemyStates)
  47.         {
  48.             case EnemyStates.GUARD:
  49.                 if (FindPlayer() != null)
  50.                 {
  51.                     Debug.Log("HasFoundPlayer");
  52.                     m_enemyStates = EnemyStates.PURSUIT;
  53.                 }
  54.                 break;
  55.             case EnemyStates.PATROL:
  56.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  57.                 break;
  58.             case EnemyStates.PURSUIT:
  59.                 m_navMeshAgent.speed = m_defaultSpeed;
  60.                 m_animatorState.isWalking = false;
  61.                 m_animatorState.isPursuing = true;
  62.                 if ((attackObject = FindPlayer()) != null)
  63.                 {
  64.                     m_animatorState.isFollowing = true;
  65.                     m_navMeshAgent.destination = attackObject.transform.position;
  66.                 }
  67.                 else
  68.                 {
  69.                     m_animatorState.isFollowing = false;
  70.                 }
  71.                 break;
  72.             case EnemyStates.DEATH:
  73.                 break;
  74.         }
  75.     }
  76. }

返回 Unity 界面运行查看效果,当史莱姆发现角色后会如预期播放奔跑动画。但是还有一个小问题,脱战后史莱姆还会保持待机状态移动一段距离,效果有延迟。

为了解决上述问题,如代码清单 3 所示,当拉脱战时,将敌人的 NavMeshAgent.destination 设置为当前的坐标点(第 31 行),实现快速停止。

代码清单 3 解决“延迟”问题
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     void SwitchState()
  4.     {
  5.         GameObject attackObject = null;
  6.  
  7.         switch (m_enemyStates)
  8.         {
  9.             case EnemyStates.GUARD:
  10.                 if (FindPlayer() != null)
  11.                 {
  12.                     Debug.Log("HasFoundPlayer");
  13.                     m_enemyStates = EnemyStates.PURSUIT;
  14.                 }
  15.                 break;
  16.             case EnemyStates.PATROL:
  17.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  18.                 break;
  19.             case EnemyStates.PURSUIT:
  20.                 m_navMeshAgent.speed = m_defaultSpeed;
  21.                 m_animatorState.isWalking = false;
  22.                 m_animatorState.isPursuing = true;
  23.                 if ((attackObject = FindPlayer()) != null)
  24.                 {
  25.                     m_animatorState.isFollowing = true;
  26.                     m_navMeshAgent.destination = attackObject.transform.position;
  27.                 }
  28.                 else
  29.                 {
  30.                     m_animatorState.isFollowing = false;
  31.                     m_navMeshAgent.destination = transform.position;
  32.                 }
  33.                 break;
  34.             case EnemyStates.DEATH:
  35.                 break;
  36.         }
  37.     }
  38. }
  39.  

最终的效果如图 3 所示,可以看到史莱姆发现角色后播放奔跑动画,拉脱战之后播放待机动画。

图3 最终效果