Unity3D RPG Core | 18 守卫状态和死亡状态

在攻击方面的内容基本完成之后,在这节中,我们继续实现守卫状态和死亡状态的逻辑。

1. 实现守卫状态逻辑

在追击时被拉脱时,需要回到守卫状态。守卫时的位置和朝向和初始守卫时一致,所以我们需要记录初始时的这些信息。如代码清单 1 所示,我们在 Awake() 阶段记录初始化信息。

代码清单 1 记录初始方位信息
  1. [RequireComponent(typeof(NavMeshAgent))]
  2. public class EnemyController : MonoBehaviour
  3. {
  4.     struct InitialTransformParams
  5.     {     
  6.         public Vector3    initPosition;
  7.         public Quaternion initRotation;
  8.     }
  9.     InitialTransformParams m_initTransform;
  10.  
  11.     void Awake()
  12.     {
  13.         m_initTransform.initPosition = transform.position;
  14.         m_initTransform.initRotation = transform.rotation;
  15.     }
  16. }

这边不同于视频,我添加了一个“返回”状态。因为我想要敌人脱战返回初始状态的时候,走得会更快一点。

如代码清单 2 中的第 91 行所示,脱战是转移到返回状态。返回状态下(第 95 至 112 行),将速度设快。同时判断敌人是否走到了初始点,如果走到了则转移到初始状态(守卫或巡逻)。

巡逻状态在之前已经完成,现在完成守卫状态(第 7 至 19 行)。守卫状态下还需要恢复原来的朝向角度,直接赋值是“跳变”的效果。如果想要慢慢恢复到原来的朝向,可以使用 Quaternion.Lerp() 函数。

代码清单 2 返回和守卫状态
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     void SwitchState()
  4.     {
  5.         switch (m_enemyStates)
  6.         {
  7.             case EnemyStates.GUARD:
  8.                 m_animatorState.isWalking = false;
  9.                 m_animatorState.isPursuing = false;
  10.                 if (transform.rotation != m_initTransform.initRotation)
  11.                 {
  12.                     transform.rotation = Quaternion.Lerp(transform.rotation, m_initTransform.initRotation, 0.01f);
  13.                 }
  14.                 if (FindPlayer() != null)
  15.                 {
  16.                     Debug.Log("HasFoundPlayer");
  17.                     m_enemyStates = EnemyStates.PURSUIT;
  18.                 }
  19.                 break;
  20.             case EnemyStates.PATROL:
  21.                 m_navMeshAgent.isStopped = false;
  22.                 if (FindPlayer() != null)
  23.                 {
  24.                     Debug.Log("HasFoundPlayer");
  25.                     m_enemyStates = EnemyStates.PURSUIT;
  26.                     break;
  27.                 }
  28.  
  29.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  30.                 m_animatorState.isPursuing = false;
  31.  
  32.                 //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
  33.                 if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
  34.                 {
  35.                     m_navMeshAgent.destination = transform.position;
  36.                     m_animatorState.isWalking = false;
  37.                     m_animatorState.isSensing = true;
  38.                     if (m_patrolStoppingRemainTime > 0)
  39.                     {
  40.                         m_patrolStoppingRemainTime -= Time.deltaTime;
  41.                     }
  42.                     else
  43.                     {
  44.                         m_patrolStoppingRemainTime = m_patrolStoppingTime;
  45.                         m_patrolDestPoint = GetNewPatrolDestPoint();
  46.                     }
  47.                 }
  48.                 else
  49.                 {
  50.                     m_animatorState.isWalking = true;
  51.                     m_animatorState.isSensing = false;
  52.                     m_navMeshAgent.destination = m_patrolDestPoint;
  53.                 }
  54.                 break;
  55.             case EnemyStates.PURSUIT:
  56.                 m_navMeshAgent.isStopped = false;
  57.                 m_navMeshAgent.speed = m_defaultSpeed;
  58.                 m_animatorState.isWalking = false;
  59.                 m_animatorState.isSensing = false;
  60.                 m_animatorState.isPursuing = true;
  61.                 if ((m_playerObj = FindPlayer()) != null)
  62.                 {
  63.                     m_animatorState.isFollowing = true;
  64.                     m_navMeshAgent.destination = m_playerObj.transform.position;
  65.                     if (IsPlayerInAttackRange(m_playerObj))
  66.                     {
  67.                         m_animatorState.isFollowing = false;
  68.                         m_navMeshAgent.isStopped = true;
  69.  
  70.                         if (m_attackCoolTime < 0)
  71.                         {
  72.                             m_attackCoolTime = m_attackData.CoolDown;
  73.                             m_attackData.m_isCriticalHit = Random.value < m_attackData.CriticalChance;
  74.                             Attack(m_playerObj);
  75.                         }
  76.                     }
  77.                 }
  78.                 else
  79.                 {
  80.                     m_animatorState.isFollowing = false;
  81.                     m_navMeshAgent.destination = transform.position;
  82.  
  83.                     if (m_patrolStoppingRemainTime > 0)
  84.                     {
  85.                         m_patrolStoppingRemainTime -= Time.deltaTime;
  86.                     }
  87.                     else
  88.                     {
  89.                         m_patrolStoppingRemainTime = m_patrolStoppingTime;
  90.  
  91.                         m_enemyStates = EnemyStates.RETURN;
  92.                     }
  93.                 }
  94.                 break;
  95.             case EnemyStates.RETURN:
  96.                 m_navMeshAgent.speed = m_defaultSpeed * 1.5f;
  97.                 m_animatorState.isWalking = true;
  98.                 m_animatorState.isSensing = false;
  99.                 m_animatorState.isPursuing = false;
  100.                 if (transform.position != m_initTransform.initPosition)
  101.                 {
  102.                     m_navMeshAgent.destination = m_initTransform.initPosition;
  103.                 }
  104.  
  105.                 if (Vector3.Distance(transform.position, m_initTransform.initPosition) <= m_navMeshAgent.stoppingDistance)
  106.                 {
  107.                     if (m_enemyType == EnemyType.PATROL)
  108.                         m_enemyStates = EnemyStates.PATROL;
  109.                     else
  110.                         m_enemyStates = EnemyStates.GUARD;
  111.                 }
  112.                 break;
  113.             case EnemyStates.DEATH:
  114.                 break;
  115.         }
  116.     }
  117. }

2. 实现死亡状态逻辑

在实现死亡状态的代码逻辑之前,我们先设置死亡相关的动画。如图 1 所示,我们添加新的一个层,在这个层里,除了设置死亡动画,也顺带设置一下暴击受伤时的动画。

我们添加两个变量:布尔变量 Dead 指示是否死亡;触发型变量 Hurt 指定触发受伤动画。受伤动画之间的转移之前了解过,死亡的动画有所不同。死亡动画任何状态都可以转移,设置为从 Any State 转移。由于是单向转移,注意把 Can Transition To Self 取消勾选。

图1 设置死亡和受伤动画

注意新加层的时候,不要忘记设置层的权重为 1。0 的话虽然会转移,但是播放不了动画。

2.1 死亡逻辑

在设置完史莱姆的相关动画后,我们实现敌人的死亡逻辑。如代码清单 3 所示,在状态转移函数中,首先判断是否达到死亡条件(第 14 至 15 行)。敌人达到死亡状态时(第 19 至 23 行),需要将碰撞体和导航组件都失效,防止敌人死亡时还会移动、防止人物角色还能触发攻击。最后将整个对象销毁掉。

同样动画变量增加对 Dead 的更新(第 9 行)。

代码清单 3 敌人死亡逻辑
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     void SwitchAnimation()
  4.     {
  5.         m_animator.SetBool("Walking", m_animatorState.isWalking);
  6.         m_animator.SetBool("Sensing", m_animatorState.isSensing);
  7.         m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
  8.         m_animator.SetBool("Following", m_animatorState.isFollowing);
  9.         m_animator.SetBool("Dead", m_characterData.CurrentHealth == 0);
  10.     }
  11.  
  12.     void SwitchState()
  13.     {
  14.         if (m_characterData.CurrentHealth <= 0)
  15.             m_enemyStates = EnemyStates.DEATH;
  16.  
  17.         switch (m_enemyStates)
  18.         {
  19.             case EnemyStates.DEATH:
  20.                 m_collider.enabled = false;
  21.                 m_navMeshAgent.enabled = false;
  22.                 Destroy(gameObject, 2.0f);
  23.                 break;
  24.         }
  25.     }
  26. }

2.2 受伤逻辑

最后我们实现受伤动画的触发。我们将其放在之前的伤害计算函数中,如代码清单 4 所示,如果攻击者暴击,则播放被攻击者的受伤动画。

代码清单 4 受伤逻辑
  1. public void TakeDamage(CharacterData defender)
  2. {
  3.     if (m_isCriticalHit)
  4.     {
  5.         defender.GetComponent<Animator>().SetTrigger("Hurt");
  6.     }
  7.  
  8.     int damage = Mathf.Max(GetDamage() - defender.CurrentDefence, 1);
  9.     defender.CurrentHealth = Mathf.Max(defender.CurrentHealth - damage, 0);
  10.     //TODO:更新UI
  11.     //TODO:更新经验
  12. }

最终实现的效果如图 2 所示,暴击时会播放受伤动画,死亡时会播放死亡动画。

图2 最终效果