Unity3D RPG Core | 18 守卫状态和死亡状态
在攻击方面的内容基本完成之后,在这节中,我们继续实现守卫状态和死亡状态的逻辑。
1. 实现守卫状态逻辑
在追击时被拉脱时,需要回到守卫状态。守卫时的位置和朝向和初始守卫时一致,所以我们需要记录初始时的这些信息。如代码清单 1 所示,我们在 Awake() 阶段记录初始化信息。
- [RequireComponent(typeof(NavMeshAgent))]
- public class EnemyController : MonoBehaviour
- {
- struct InitialTransformParams
- {
- public Vector3 initPosition;
- public Quaternion initRotation;
- }
- InitialTransformParams m_initTransform;
- void Awake()
- {
- m_initTransform.initPosition = transform.position;
- m_initTransform.initRotation = transform.rotation;
- }
- }
这边不同于视频,我添加了一个“返回”状态。因为我想要敌人脱战返回初始状态的时候,走得会更快一点。
如代码清单 2 中的第 91 行所示,脱战是转移到返回状态。返回状态下(第 95 至 112 行),将速度设快。同时判断敌人是否走到了初始点,如果走到了则转移到初始状态(守卫或巡逻)。
巡逻状态在之前已经完成,现在完成守卫状态(第 7 至 19 行)。守卫状态下还需要恢复原来的朝向角度,直接赋值是“跳变”的效果。如果想要慢慢恢复到原来的朝向,可以使用 Quaternion.Lerp() 函数。
- public class EnemyController : MonoBehaviour
- {
- void SwitchState()
- {
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- m_animatorState.isWalking = false;
- m_animatorState.isPursuing = false;
- if (transform.rotation != m_initTransform.initRotation)
- {
- transform.rotation = Quaternion.Lerp(transform.rotation, m_initTransform.initRotation, 0.01f);
- }
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- m_navMeshAgent.isStopped = false;
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- break;
- }
- m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
- m_animatorState.isPursuing = false;
- //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
- if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
- {
- m_navMeshAgent.destination = transform.position;
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = true;
- if (m_patrolStoppingRemainTime > 0)
- {
- m_patrolStoppingRemainTime -= Time.deltaTime;
- }
- else
- {
- m_patrolStoppingRemainTime = m_patrolStoppingTime;
- m_patrolDestPoint = GetNewPatrolDestPoint();
- }
- }
- else
- {
- m_animatorState.isWalking = true;
- m_animatorState.isSensing = false;
- m_navMeshAgent.destination = m_patrolDestPoint;
- }
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.isStopped = false;
- m_navMeshAgent.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = false;
- m_animatorState.isPursuing = true;
- if ((m_playerObj = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = m_playerObj.transform.position;
- if (IsPlayerInAttackRange(m_playerObj))
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.isStopped = true;
- if (m_attackCoolTime < 0)
- {
- m_attackCoolTime = m_attackData.CoolDown;
- m_attackData.m_isCriticalHit = Random.value < m_attackData.CriticalChance;
- Attack(m_playerObj);
- }
- }
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- if (m_patrolStoppingRemainTime > 0)
- {
- m_patrolStoppingRemainTime -= Time.deltaTime;
- }
- else
- {
- m_patrolStoppingRemainTime = m_patrolStoppingTime;
- m_enemyStates = EnemyStates.RETURN;
- }
- }
- break;
- case EnemyStates.RETURN:
- m_navMeshAgent.speed = m_defaultSpeed * 1.5f;
- m_animatorState.isWalking = true;
- m_animatorState.isSensing = false;
- m_animatorState.isPursuing = false;
- if (transform.position != m_initTransform.initPosition)
- {
- m_navMeshAgent.destination = m_initTransform.initPosition;
- }
- if (Vector3.Distance(transform.position, m_initTransform.initPosition) <= m_navMeshAgent.stoppingDistance)
- {
- if (m_enemyType == EnemyType.PATROL)
- m_enemyStates = EnemyStates.PATROL;
- else
- m_enemyStates = EnemyStates.GUARD;
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- }
2. 实现死亡状态逻辑
在实现死亡状态的代码逻辑之前,我们先设置死亡相关的动画。如图 1 所示,我们添加新的一个层,在这个层里,除了设置死亡动画,也顺带设置一下暴击受伤时的动画。
我们添加两个变量:布尔变量 Dead 指示是否死亡;触发型变量 Hurt 指定触发受伤动画。受伤动画之间的转移之前了解过,死亡的动画有所不同。死亡动画任何状态都可以转移,设置为从 Any State 转移。由于是单向转移,注意把 Can Transition To Self 取消勾选。

注意新加层的时候,不要忘记设置层的权重为 1。0 的话虽然会转移,但是播放不了动画。
2.1 死亡逻辑
在设置完史莱姆的相关动画后,我们实现敌人的死亡逻辑。如代码清单 3 所示,在状态转移函数中,首先判断是否达到死亡条件(第 14 至 15 行)。敌人达到死亡状态时(第 19 至 23 行),需要将碰撞体和导航组件都失效,防止敌人死亡时还会移动、防止人物角色还能触发攻击。最后将整个对象销毁掉。
同样动画变量增加对 Dead 的更新(第 9 行)。
- public class EnemyController : MonoBehaviour
- {
- void SwitchAnimation()
- {
- m_animator.SetBool("Walking", m_animatorState.isWalking);
- m_animator.SetBool("Sensing", m_animatorState.isSensing);
- m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
- m_animator.SetBool("Following", m_animatorState.isFollowing);
- m_animator.SetBool("Dead", m_characterData.CurrentHealth == 0);
- }
- void SwitchState()
- {
- if (m_characterData.CurrentHealth <= 0)
- m_enemyStates = EnemyStates.DEATH;
- switch (m_enemyStates)
- {
- case EnemyStates.DEATH:
- m_collider.enabled = false;
- m_navMeshAgent.enabled = false;
- Destroy(gameObject, 2.0f);
- break;
- }
- }
- }
2.2 受伤逻辑
最后我们实现受伤动画的触发。我们将其放在之前的伤害计算函数中,如代码清单 4 所示,如果攻击者暴击,则播放被攻击者的受伤动画。
- public void TakeDamage(CharacterData defender)
- {
- if (m_isCriticalHit)
- {
- defender.GetComponent<Animator>().SetTrigger("Hurt");
- }
- int damage = Mathf.Max(GetDamage() - defender.CurrentDefence, 1);
- defender.CurrentHealth = Mathf.Max(defender.CurrentHealth - damage, 0);
- //TODO:更新UI
- //TODO:更新经验
- }
最终实现的效果如图 2 所示,暴击时会播放受伤动画,死亡时会播放死亡动画。
