Unity3D RPG Core | 11 实现攻击动画

上一节视频中,鼠标已经已经可以识别到敌人,并且更改对应的样式。在这一节中,我们需要实现:点击敌人,角色跑到敌人旁边,然后攻击敌人。

1. 移动到敌人边上

移动到敌人边上的逻辑和之前的人物移动逻辑类似,我们在 MouseManager 脚本中添加逻辑。如代码清单 1 所示,我们增加了一个攻击敌人事件(第 4 行),传递的参数为敌人对象;鼠标左键敌人时触发(第 14 行)。

代码清单 1 攻击事件
  1. public class MouseManager : MonoBehaviour
  2. {
  3.     public event Action<Vector3>    OnMoveMouseClick;
  4.     public event Action<GameObject> OnAttackMouseClick;
  5.  
  6.     public void DoMouseEvent(RaycastHit raycastHit)
  7.     {
  8.         if (Input.GetMouseButtonDown(0))
  9.         {
  10.             string tag = raycastHit.collider.gameObject.tag;
  11.             if (tag == "Ground")
  12.                 OnMoveMouseClick.Invoke(raycastHit.point);
  13.             else if (tag == "Enemy")
  14.                 OnAttackMouseClick.Invoke(raycastHit.collider.gameObject);
  15.         }
  16.     }
  17. }

接着我们到 PlayerController 脚本中实现攻击事件的响应。

如代码清单 2 所示,将 DoAttackAction 函数绑定到攻击事件(第 27 行);DoAttackAction 中会另起一个协程来实现攻击逻辑。目前协程中只实现了移动到敌人身边的逻辑:如果角色和敌人之间的距离不够,则反复设置 Nav Mesh Agent 的 destination 值。跑到敌人身边后需要停止移动(isStopped = true),以免发生碰撞。

代码清单 2 响应攻击事件
  1. public class PlayerController : MonoBehaviour
  2. {
  3.     void DoAttackAction(GameObject obj)
  4.     {
  5.         if (obj != null)
  6.         {
  7.             StartCoroutine(CoroutineAttackEnemy(obj));
  8.         }
  9.     }
  10.  
  11.     IEnumerator CoroutineAttackEnemy(GameObject obj)
  12.     {
  13.         transform.LookAt(obj.transform);
  14.         while (Vector3.Distance(obj.transform.position, transform.position) > 2)
  15.         {
  16.             m_navMeshAgent.destination = obj.transform.position;
  17.             yield return null;
  18.         }
  19.  
  20.         m_navMeshAgent.isStopped = true;
  21.     }
  22.  
  23.     // Start is called before the first frame update
  24.     void Start()
  25.     {
  26.         MouseManager.GetInstance().OnMoveMouseClick   += DoMoveAction;
  27.         MouseManager.GetInstance().OnAttackMouseClick += DoAttackAction;
  28.     }
  29. }
  30.  

第一次接触协程,这边记录一下自己目前的理解。协程是在线程层面调度的(和线程不同,线程在进程层面调度)。线程的调度好理解,多处理器上真正并行,单处理器上时间片分配。但是协程的调度还不太清楚,视频以及其他资料看说,执行 "yield return" 协程会挂起,执行权给到调度者,等待下一次调度恢复。这边如何实现的挂起和恢复操作比较费解,因为之前理解的这些操作是在操作系统层面实现的,留作问题。

什么是协程?协程如何调度?

2. 设置攻击动画

以上,我们已经实现点击敌人,移动到敌人身边的功能了。在移动到敌人身边之后,我们需要播放攻击动画。

如图 1 所示,我们回到之前人物的动画设置窗体。首先我们在人物素材包里选择攻击动画,将其拖拽到界面中,并将生成的状态命名。接着我们右键选择 Make Transition,建立和移动 Blend Tree 之间的进入和退出连接。然后新建一个 Trigger 类型变量 Attack,用于触发攻击。

图1 设置攻击动画

最后如图 2 所示进行转移设置。Blend Tree 到 Attack 之间转移使用 Attack 变量触发,不需要退出时间;Fixed Duration 和 Transition Duration 也按照图上设置,含义不明。Attack 到 Blend Tree 之间的转移,需要退出时间,这边设置为 1,确保攻击动画完整播放。

图2 设置转移

注意触发转移不需要退出时间,否则会造成攻击动画播放延迟。

3. 实现攻击逻辑

在设置好攻击动画之后,我们继续完善协程中的攻击逻辑。如代码清单 3 所示,我们使用 Animator.SetTrigger 触发攻击动画(第 18 行)。此外,我们还设置了一个攻击冷却时间(第 3 行),时间在 Update() 函数中进行更新。

代码清单 3 攻击逻辑
  1. public class PlayerController : MonoBehaviour
  2. {
  3.     float m_attackCoolTime = 0;
  4.  
  5.     IEnumerator CoroutineAttackEnemy(GameObject obj)
  6.     {
  7.         transform.LookAt(obj.transform);
  8.         while (Vector3.Distance(obj.transform.position, transform.position) > 2)
  9.         {
  10.             m_navMeshAgent.destination = obj.transform.position;
  11.             yield return null;
  12.         }
  13.  
  14.         m_navMeshAgent.isStopped = true;
  15.  
  16.         if (m_attackCoolTime <= 0)
  17.         {
  18.             m_animator.SetTrigger("Attack");
  19.             m_attackCoolTime = 0.5f;
  20.         }
  21.     }
  22.  
  23.     // Update is called once per frame
  24.     void Update()
  25.     {
  26.         m_animator.SetFloat("Speed", m_navMeshAgent.velocity.sqrMagnitude);
  27.  
  28.         m_attackCoolTime -= Time.deltaTime;
  29.     }
  30. }

至此,我们已经实现了跑到敌人身边并攻击。但是还有一个问题:攻击敌人后,人物无法继续移动。点击地板无法移动的原因是之前把人物停止了,如代码清单 4 所示,现在需要恢复回来(第 4 行)。

还有一个比较大的问题是,当攻击阶段时,点击地板无法打断。这边解决的方案是随即使用 StopAllCoroutines() 停止正在运行的协程(第 3 行)。但是这个方案还是解决不了打断时继续播放攻击动画的问题。

代码清单 4 完善逻辑
  1. void DoMoveAction(Vector3 destination)
  2. {
  3.     StopAllCoroutines();
  4.     m_navMeshAgent.isStopped = false;
  5.     m_navMeshAgent.destination = destination;
  6. }

还是会偶现打断时播放攻击动画的情况。

最终的移动、攻击和打断效果如图 3 所示。

图3 最终效果