Unity3D RPG Core | 22 设置兽人士兵

这节我们设置新敌人兽人士兵的相关内容。

1. 设置兽人士兵动画控制器和数据

上一节敌人乌龟的动画控制器是使用复用动画控制器来制作的。兽人士兵与它逻辑不同,因为我们想加一个推开角色人物的功能。因此这边我们复制一份史莱姆的动画控制器,在其基础上进行修改。

我们需要使用兽人的动画替换之前的每一个动画(没有对应的动画可以使用 Idle 动画代替)。如图 2 所示,我们将推开人物功能当成兽人技能触发,对应触发变量 Skill,其对应动画 Attack02 动画。同时注意将之前的暴击转移逻辑去掉。

图1 修改设置动画控制器

1.1 创建 ScriptableObject 文件

同样兽人士兵也需要创建属性和攻击的 ScriptableObject 文件。

攻击数据如图 2 所示,技能距离设置短一点,当人物靠近穿过兽人的时候,会触发技能将人物推开。

图2 攻击数据

2. 新的控制脚本

我们新建兽人对应的控制脚本 GruntController,使它继承于 EnemyController 类:

  • public class GruntController : EnemyController

注意删掉自动生成的生命周期空函数,否则会覆盖父类原有的函数。

接着我们把 GruntController 拖拽到兽人对象上,设置好 Nav Mesh Agent 参数、指定属性 ScriptableObject 文件、指定攻击 ScriptableObject 文件。此时运行游戏,可以看到兽人士兵逻辑已经基本完成了。

2.1 实现推开功能

推开的功能和受伤的实现一样,放在动画帧事件里触发实现。首先我们先将基础攻击的帧绑定到之前的 Hit() 函数上。

推开的事件绑定到另一帧上,绑定事件为 KickOff() 函数。

代码清单 1 推开事件函数
  1. public class GruntController : EnemyController
  2. {
  3.     public float m_kickForce = 20;
  4.  
  5.     public void KickOff()
  6.     {
  7.         if (m_playerObj != null)
  8.         {
  9.             transform.LookAt(m_playerObj.transform);
  10.  
  11.             Vector3 direction = m_playerObj.transform.position - transform.position;
  12.             direction.Normalize();
  13.  
  14.             NavMeshAgent agent = m_playerObj.GetComponent<NavMeshAgent>();
  15.             agent.isStopped = true;
  16.             agent.velocity = m_kickForce * direction;
  17.         }
  18.     }
  19. }

KickOff() 函数如代码清单 1 所示,首先使兽人朝向角色,接着计算推出方向。使用 NavMeshAgent.velocity 将人物移动,距离大小使用 m_kickForce 变量控制。

注意角色人物的 Stopping Distance 不要设置为 0,否则推开会无效。原因未知。

补上技能触发的逻辑,如代码清单 2 所示,我们以技能触发为优先。技能这块逻辑还不是很清楚,日后再做完善。此时运行程序,可以验证推开功能已经实现。

代码清单 2 触发技能逻辑
  1. public class EnemyController : MonoBehaviour, IEndGameObserver
  2. {
  3.     void Attack()
  4.     {
  5.         transform.LookAt(m_attackTarget.transform);
  6.         if (TargetInSkillRange())
  7.         {
  8.             m_animator.SetTrigger("Skill");
  9.         }
  10.         else if (TargetInAttackRange())
  11.         {
  12.             m_animator.SetTrigger("Attack");
  13.         }
  14.     }
  15. }

2.2 实现推开眩晕

我们利用上角色素材包里的眩晕动画。如图 3 所示,我们将眩晕动画拖拽到角色人物的动画控制器中,并使用变量 Dizzy 触发。

图3 添加眩晕动画

同样在 KickOff() 函数中触发角色人物眩晕。

  1. public class GruntController : EnemyController
  2. {
  3.     public float m_kickForce = 20;
  4.  
  5.     public void KickOff()
  6.     {
  7.         if (m_playerObj != null)
  8.         {
  9.             transform.LookAt(m_playerObj.transform);
  10.  
  11.             Vector3 direction = m_playerObj.transform.position - transform.position;
  12.             direction.Normalize();
  13.  
  14.             NavMeshAgent agent = m_playerObj.GetComponent<NavMeshAgent>();
  15.             agent.isStopped = true;
  16.             agent.velocity = m_kickForce * direction;
  17.  
  18.             m_playerObj.GetComponent<Animator>().SetTrigger("Dizzy");
  19.         }
  20.     }
  21. }

3. 一些问题

运行过程中遇到一些问题。

3.1 推开后无法无法攻击

推开后直接点人物会无法攻击。KickOff() 函数中会将 agent 停止,在攻击事件中需要将 agent 恢复。

3.2 眩晕、受伤等过程还能移动

人物在眩晕或者受伤动画过程中,还能点地板取消移动,这点不太合理。我们通过指定动画行为脚本来修复。如图 4 所示,我们可以在相应动画节点界面上点击 Add Behaviour 添加一个脚本,命名为 StopAgent

图4 添加动画行为脚本

打开创建的脚本文件。如代码清单 3 所示,里面对应了动画执行的各个阶段。animator 参数就是当前执行挂载的 Animator 组件,我们可以通过它找到 NavMeshAgent 组件。

动画开始时(OnStateEnter),我们使 NavMeshAgent 停止;结束时(OnStateExit),恢复 NavMeshAgent。因为点地板时会恢复 NavMeshAgent,所以我们需要在动画执行过程中(OnStateUpdate)反复停止 NavMeshAgent。

代码清单 3 动画行为脚本
  1. public class StopAgent : StateMachineBehaviour
  2. {
  3.     // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
  4.     override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  5.     {
  6.         animator.GetComponent<NavMeshAgent>().isStopped = true;
  7.     }
  8.  
  9.     // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
  10.     override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  11.     {
  12.         animator.GetComponent<NavMeshAgent>().isStopped = true;
  13.     }
  14.  
  15.     // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
  16.     override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  17.     {
  18.         animator.GetComponent<NavMeshAgent>().isStopped = false;
  19.     }
  20.  
  21.     // OnStateMove is called right after Animator.OnAnimatorMove()
  22.     //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  23.     //{
  24.     //    // Implement code that processes and affects root motion
  25.     //}
  26.  
  27.     // OnStateIK is called right after Animator.OnAnimatorIK()
  28.     //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  29.     //{
  30.     //    // Implement code that sets up animation IK (inverse kinematics)
  31.     //}
  32. }

我们在角色人物的眩晕和受伤动画上加上 StopAgent 脚本。目前兽人攻击时,如果人物移动,兽人也会漂移着攻击,所以也将兽人的两个攻击动画加上 StopAgent 脚本。

3.3 StopAgent 脚本带来的问题

StopAgent 脚本会带来一个问题:因为动画期间一直使用 NavMeshAgent 组件,而在敌人死亡前的一段时间,我们会关闭 NavMeshAgent,这就会带来以下报错。

  1. "Stop" can only be called on an active agent that has been placed on a NavMesh.
  2. UnityEngine.StackTraceUtility:ExtractStackTrace ()
  3. StopAgent:OnStateUpdate (UnityEngine.Animator,UnityEngine.AnimatorStateInfo,int) (at Assets/Scripts/AnimatorBehaviour/StopAgent.cs:17)

当时关闭 NavMeshAgent 的原因是怕死亡时模型还挡着道。如果不关闭,设置 NavMeshAgent.radius 为零,同样能达到这个效果,同时还能修复上面的报错。

  1. public class EnemyController : MonoBehaviour, IEndGameObserver
  2. {
  3.     void SwitchState()
  4.     {
  5.             case EnemyStates.DEATH:
  6.                 m_collider.enabled = false;
  7.                 //m_navMeshAgent.enabled = false;
  8.                 m_navMeshAgent.radius = 0;
  9.                 Destroy(gameObject, 2.0f);
  10.                 break;
  11.         }
  12.     }
  13. }

修复掉现有发现的问题后,最终的效果如图 5 所示。

图5 最终效果