Unity3D RPG Core | 11 实现攻击动画
上一节视频中,鼠标已经已经可以识别到敌人,并且更改对应的样式。在这一节中,我们需要实现:点击敌人,角色跑到敌人旁边,然后攻击敌人。
1. 移动到敌人边上
移动到敌人边上的逻辑和之前的人物移动逻辑类似,我们在 MouseManager 脚本中添加逻辑。如代码清单 1 所示,我们增加了一个攻击敌人事件(第 4 行),传递的参数为敌人对象;鼠标左键敌人时触发(第 14 行)。
- public class MouseManager : MonoBehaviour
- {
- public event Action<Vector3> OnMoveMouseClick;
- public event Action<GameObject> OnAttackMouseClick;
- public void DoMouseEvent(RaycastHit raycastHit)
- {
- if (Input.GetMouseButtonDown(0))
- {
- string tag = raycastHit.collider.gameObject.tag;
- if (tag == "Ground")
- OnMoveMouseClick.Invoke(raycastHit.point);
- else if (tag == "Enemy")
- OnAttackMouseClick.Invoke(raycastHit.collider.gameObject);
- }
- }
- }
接着我们到 PlayerController 脚本中实现攻击事件的响应。
如代码清单 2 所示,将 DoAttackAction 函数绑定到攻击事件(第 27 行);DoAttackAction 中会另起一个协程来实现攻击逻辑。目前协程中只实现了移动到敌人身边的逻辑:如果角色和敌人之间的距离不够,则反复设置 Nav Mesh Agent 的 destination 值。跑到敌人身边后需要停止移动(isStopped = true),以免发生碰撞。
- public class PlayerController : MonoBehaviour
- {
- void DoAttackAction(GameObject obj)
- {
- if (obj != null)
- {
- StartCoroutine(CoroutineAttackEnemy(obj));
- }
- }
- IEnumerator CoroutineAttackEnemy(GameObject obj)
- {
- transform.LookAt(obj.transform);
- while (Vector3.Distance(obj.transform.position, transform.position) > 2)
- {
- m_navMeshAgent.destination = obj.transform.position;
- yield return null;
- }
- m_navMeshAgent.isStopped = true;
- }
- // Start is called before the first frame update
- void Start()
- {
- MouseManager.GetInstance().OnMoveMouseClick += DoMoveAction;
- MouseManager.GetInstance().OnAttackMouseClick += DoAttackAction;
- }
- }
第一次接触协程,这边记录一下自己目前的理解。协程是在线程层面调度的(和线程不同,线程在进程层面调度)。线程的调度好理解,多处理器上真正并行,单处理器上时间片分配。但是协程的调度还不太清楚,视频以及其他资料看说,执行 "yield return" 协程会挂起,执行权给到调度者,等待下一次调度恢复。这边如何实现的挂起和恢复操作比较费解,因为之前理解的这些操作是在操作系统层面实现的,留作问题。
什么是协程?协程如何调度?
2. 设置攻击动画
以上,我们已经实现点击敌人,移动到敌人身边的功能了。在移动到敌人身边之后,我们需要播放攻击动画。
如图 1 所示,我们回到之前人物的动画设置窗体。首先我们在人物素材包里选择攻击动画,将其拖拽到界面中,并将生成的状态命名。接着我们右键选择 Make Transition,建立和移动 Blend Tree 之间的进入和退出连接。然后新建一个 Trigger 类型变量 Attack,用于触发攻击。

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


注意触发转移不需要退出时间,否则会造成攻击动画播放延迟。
3. 实现攻击逻辑
在设置好攻击动画之后,我们继续完善协程中的攻击逻辑。如代码清单 3 所示,我们使用 Animator.SetTrigger 触发攻击动画(第 18 行)。此外,我们还设置了一个攻击冷却时间(第 3 行),时间在 Update() 函数中进行更新。
- public class PlayerController : MonoBehaviour
- {
- float m_attackCoolTime = 0;
- IEnumerator CoroutineAttackEnemy(GameObject obj)
- {
- transform.LookAt(obj.transform);
- while (Vector3.Distance(obj.transform.position, transform.position) > 2)
- {
- m_navMeshAgent.destination = obj.transform.position;
- yield return null;
- }
- m_navMeshAgent.isStopped = true;
- if (m_attackCoolTime <= 0)
- {
- m_animator.SetTrigger("Attack");
- m_attackCoolTime = 0.5f;
- }
- }
- // Update is called once per frame
- void Update()
- {
- m_animator.SetFloat("Speed", m_navMeshAgent.velocity.sqrMagnitude);
- m_attackCoolTime -= Time.deltaTime;
- }
- }
至此,我们已经实现了跑到敌人身边并攻击。但是还有一个问题:攻击敌人后,人物无法继续移动。点击地板无法移动的原因是之前把人物停止了,如代码清单 4 所示,现在需要恢复回来(第 4 行)。
还有一个比较大的问题是,当攻击阶段时,点击地板无法打断。这边解决的方案是随即使用 StopAllCoroutines() 停止正在运行的协程(第 3 行)。但是这个方案还是解决不了打断时继续播放攻击动画的问题。
- void DoMoveAction(Vector3 destination)
- {
- StopAllCoroutines();
- m_navMeshAgent.isStopped = false;
- m_navMeshAgent.destination = destination;
- }
还是会偶现打断时播放攻击动画的情况。
最终的移动、攻击和打断效果如图 3 所示。
