一、攻击动画的设置
如图,为PlayerAnim添加攻击动画,设置一个名为Attack的Trigger参数用于触发攻击动画
注意:Locomotion => BaseAttack的过程不需要设置动画退出时间,并且要添加Attack参数以触发动画
但是,Base Attack => Locomotion的过程需要设置1秒的动画退出时间以保证攻击动画完全播放,并且此处不需要设置Attack参数(Trigger就像一个按钮,按下去就会弹起来)
二、通过订阅事件实现鼠标点击朝敌人移动
类似于鼠标点击地面移动,我们需要在MouseManager中创建一个事件来连接鼠标点击敌人与玩家向敌人移动的操作,不同的是,此处的Action事件需要传入一个GameObject的数据(敌人为GameObject类型)
public event Action<GameObject> ClickToAttackEvent; //创建鼠标点击敌人的事件
由于这里不同事件的触发是通过gameObject.tag辨别的,故MouseManager中的事件触发代码可做以下优化:
/// <summary>
/// 鼠标点击控制
/// </summary>
public void OnMouseClick()
{
if(Input.GetMouseButtonDown(0) && hitInfo.collider != null) //防止鼠标点击地图外时返回引用为空的错误
{
switch (hitInfo.collider.gameObject.tag)
{
case "Ground":
{
ClickToMoveEvent?.Invoke(hitInfo.point); //触发点击地面事件
break;
}
case "Enemy":
{
ClickToAttackEvent?.Invoke(hitInfo.collider.gameObject); //触发点击敌人事件
break;
}
}
}
}
相应的,也要在PlayerController脚本中订阅该事件。由于该事件需要可接受GameObject类型形参的方法,故需要先创建一个这样的方法来接收事件传入的敌人信息(不能直接传入协程方法是因为协程的方法具有IEnumerator的返回值,与Action事件不符)
MouseManager.Instance.ClickToAttackEvent += MoveToAttackEvent; //订阅鼠标点击敌人事件
/// <summary>
/// 接收鼠标点击敌人的信息
/// </summary>
/// <param name="obj"></param>
private void MoveToAttackEvent(GameObject obj)
{
if(obj.gameObject != null) //判断攻击的敌人物体存在
{
AttackTarget = obj;
}
}
三、使用协程实现移动到敌人面前并攻击的效果
定义一个协程方法MoveToAttack()
首先,判断敌人是否在攻击距离之外,如果是,则Player要持续向敌人移动;当敌人进入攻击距离,则跳出while循环,进入攻击判定
注意:此处的攻击距离设置不当,会导致玩家到敌人面前时把敌人推开,或当敌人体积过大时会无法跳出循环。经过调试,此处的攻击范围判定为(玩家到敌人的距离)-(敌人导航代理半径)> 1(该数值可根据武器长度调整)时,应向敌人移动。同时,Player的导航代理应将Stopping Distance = 1(距离目标点1个单位时停下)
/// <summary>
/// 向敌人移动并攻击
/// </summary>
/// <returns></returns>
private IEnumerator MoveToAttack()
{
//攻击距离判断
//调整距离避免玩家推着敌人走的情况(手感优化)
while(Vector3.Distance(AttackTarget.transform.position,transform.position) - AttackTarget.GetComponent<NavMeshAgent>().radius > 1)
{
agent.SetDestination(AttackTarget.transform.position);
yield return null;
}
transform.LookAt(AttackTarget.transform); //使Player向前的方向指向敌人
}
然后我们在上面的订阅事件的方法中启用这个协程即可
/// <summary>
/// 接收鼠标点击敌人的信息
/// </summary>
/// <param name="obj"></param>
private void MoveToAttackEvent(GameObject obj)
{
if(obj.gameObject != null) //判断攻击的敌人物体存在
{
AttackTarget = obj;
StartCoroutine("MoveToAttack"); //使用方法名字符串启用协程
}
}
进行完上面的操作后,我们会发现一个问题:当点击了敌人并向敌人移动时,若鼠标点击地面也不会中断移动,Player会一直朝着敌人走。这时。我们需要在点击地面移动的订阅事件中将该协程停止
注意:此处StopCoroutine()方法的调用方式应与StartCoroutine()一致,此处都使用方法的字符串名称调用,因为直接传入方法会出bug(我也不知道为什么)
/// <summary>
/// 地面移动
/// </summary>
/// <param name="destination"></param>
private void MoveToPoint(Vector3 destination)
{
StopCoroutine("MoveToAttack"); //暂停协程与启用协程均采用方法名字符串调用(二者调用方式需保持一致),用方法会出bug
//StopAllCoroutines(); //使用该方法会暂停所有协程
agent.SetDestination(destination);
}
四、设置一个简单的攻击冷却时间
先定义一个冷却时间
private double lastAttackTime = 0.5f; //攻击CD
冷却时间每帧衰减,在Update方法中实现
private void Update()
{
lastAttackTime -= Time.deltaTime; //冷却衰减
}
进行冷却判断,如果冷却时间<= 0,则可执行攻击,执行后重置CD。将此段代码放入协程方法即可
//攻击判断(冷却时间结束后即可进行下一次攻击)
if (lastAttackTime <= 0)
{
PlayerAnim.SetTrigger("Attack");
//重置CD
lastAttackTime = 0.5f;
}
五、实现上述功能的PlayerController脚本代码
public class PlayerController : MonoBehaviour
{
private NavMeshAgent agent;
private Animator PlayerAnim;
//攻击敌人相关参数
private GameObject AttackTarget;
private double lastAttackTime = 0.5f; //攻击CD
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
PlayerAnim = GetComponent<Animator>();
}
private void Start()
{
MouseManager.Instance.ClickToMoveEvent += MoveToPoint; //订阅鼠标点击地面移动事件
MouseManager.Instance.ClickToAttackEvent += MoveToAttackEvent; //订阅鼠标点击敌人事件
}
private void Update()
{
lastAttackTime -= Time.deltaTime; //冷却衰减
}
/// <summary>
/// 地面移动
/// </summary>
/// <param name="destination"></param>
private void MoveToPoint(Vector3 destination)
{
StopCoroutine("MoveToAttack"); //暂停协程与启用协程均采用方法名字符串调用(二者调用方式需保持一致),用方法会出bug
//StopAllCoroutines(); //使用该方法会暂停所有协程
agent.SetDestination(destination);
}
/// <summary>
/// 接收鼠标点击敌人的信息
/// </summary>
/// <param name="obj"></param>
private void MoveToAttackEvent(GameObject obj)
{
if(obj.gameObject != null) //判断攻击的敌人物体存在
{
AttackTarget = obj;
StartCoroutine("MoveToAttack"); //使用方法名字符串启用协程
}
}
/// <summary>
/// 向敌人移动并攻击
/// </summary>
/// <returns></returns>
private IEnumerator MoveToAttack()
{
//攻击距离判断
//调整距离避免玩家推着敌人走的情况(手感优化)
while(Vector3.Distance(AttackTarget.transform.position,transform.position) - AttackTarget.GetComponent<NavMeshAgent>().radius > 1)
{
agent.SetDestination(AttackTarget.transform.position);
yield return null;
}
transform.LookAt(AttackTarget.transform); //使Player向前的方向指向敌人
//攻击判断(冷却时间结束后即可进行下一次攻击)
if (lastAttackTime <= 0)
{
PlayerAnim.SetTrigger("Attack");
//重置CD
lastAttackTime = 0.5f;
}
}
}