Unity3D RPG Core | 14 随机巡逻点

这篇内容将完善敌人的巡逻逻辑。

1. 定义巡逻范围

首先和敌人的监视范围类似,我们需要定义敌人的巡逻范围。目前存在一个问题,我们无法感知这些范围,以便进行调整。为了解决这个问题,视频中介绍可以使用 Gizmos 将范围画出来。

如代码清单 1 所示,我们在 OnDrawGizmosSelected() 函数中进行 Gizmos 绘制。'Selected' 表示只有在界面上选择了这个对象,才进行绘制,可以使画面不会太杂乱。

代码清单 1 巡逻范围
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     public float m_sightRadius = 5;
  4.     public float m_patrolRadius = 5;
  5.  
  6.     void OnDrawGizmosSelected()
  7.     {
  8.         Gizmos.color = Color.blue;
  9.         Gizmos.DrawWireSphere(transform.position, m_patrolRadius);
  10.     }
  11. }

很好的调试手段。

写好代码后,我们返回到 Unity 界面。选择史莱姆对象,就可以看到如图 1 这样,画出了范围球体线框。

图1 Gizmos

2. 实现巡逻逻辑

巡逻的整体逻辑是:基于敌人的初始坐标上,随机生成巡逻范围之内的偏移,并使敌人走到这个点上。前往这个点的过程中播放行走动画;走到了这个点时,播放待机动画,并计算新的随机点。

我们看到代码清单 2,再次梳理上述逻辑。首先看到随机点生成的 GetNewPatrolDestPoint() 函数,x 和 z 坐标计算基于初始坐标的随机偏移,y 坐标不变,防止所在地形高度不一。

SwitchState() 函数中的 EnemyStates.PATROL 分支是巡逻逻辑的主要实现。巡逻时速度相对于追击时减少,isPursuing 变量设置为 false,让动画层在基础层(第 48 至 49 行)。判断是否走到对应点使用 Vector3.Distance() 函数,注意设置的阈值,调试中发现距离不会到达 0,只会无限接近 0。

注意 Vector3.Distance() 的判断阈值。

如果到达位置点,则播放待机动画,并计算新的随机点(第 54 至 55 行);如果没有到达,则播放行走动画,并设置 NavMeshAgent.destination。

代码清单 2 巡逻逻辑
  1. public enum EnemyType
  2. {
  3.     GUARD,
  4.     PATROL,
  5. }
  6.  
  7. [RequireComponent(typeof(NavMeshAgent))]
  8. public class EnemyController : MonoBehaviour
  9. {
  10.     public EnemyType m_enemyType = EnemyType.PATROL;
  11.     public float     m_patrolRadius = 5;
  12.     Vector3          m_patrolDestPoint;
  13.     Vector3          m_initPosition;
  14.  
  15.     void Awake()
  16.     {
  17.         m_navMeshAgent = GetComponent<NavMeshAgent>();
  18.         m_animator     = GetComponent<Animator>();
  19.         m_defaultSpeed = m_navMeshAgent.speed;
  20.         m_initPosition = transform.position;
  21.     }
  22.  
  23.     // Start is called before the first frame update
  24.     void Start()
  25.     {
  26.         if (m_enemyType == EnemyType.PATROL)
  27.             m_enemyStates = EnemyStates.PATROL;
  28.         else
  29.             m_enemyStates = EnemyStates.GUARD;
  30.  
  31.         m_patrolDestPoint = GetNewPatrolDestPoint();
  32.     }
  33.  
  34.     void SwitchState()
  35.     {
  36.         GameObject attackObject = null;
  37.  
  38.         switch (m_enemyStates)
  39.         {
  40.             case EnemyStates.GUARD:
  41.                 if (FindPlayer() != null)
  42.                 {
  43.                     Debug.Log("HasFoundPlayer");
  44.                     m_enemyStates = EnemyStates.PURSUIT;
  45.                 }
  46.                 break;
  47.             case EnemyStates.PATROL:
  48.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  49.                 m_animatorState.isPursuing = false;
  50.  
  51.                 //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
  52.                 if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
  53.                 {
  54.                     m_animatorState.isWalking = false;
  55.                     m_patrolDestPoint = GetNewPatrolDestPoint();
  56.                 }
  57.                 else
  58.                 {
  59.                     m_animatorState.isWalking = true;
  60.                     m_navMeshAgent.destination = m_patrolDestPoint;
  61.                 }
  62.                 break;
  63.             case EnemyStates.PURSUIT:
  64.                 m_navMeshAgent.speed = m_defaultSpeed;
  65.                 m_animatorState.isWalking = false;
  66.                 m_animatorState.isPursuing = true;
  67.                 if ((attackObject = FindPlayer()) != null)
  68.                 {
  69.                     m_animatorState.isFollowing = true;
  70.                     m_navMeshAgent.destination = attackObject.transform.position;
  71.                 }
  72.                 else
  73.                 {
  74.                     m_animatorState.isFollowing = false;
  75.                     m_navMeshAgent.destination = transform.position;
  76.                 }
  77.                 break;
  78.             case EnemyStates.DEATH:
  79.                 break;
  80.         }
  81.     }
  82.  
  83.     Vector3 GetNewPatrolDestPoint()
  84.     {
  85.         float offsetX = Random.Range(-m_patrolRadius, m_patrolRadius);
  86.         float offsetZ = Random.Range(-m_patrolRadius, m_patrolRadius);
  87.  
  88.         Vector3 randomPoint = new Vector3(m_initPosition.x + offsetX, transform.position.y, m_initPosition.z + offsetZ);
  89.  
  90.         return randomPoint;
  91.     }
  92. }

2.1 随机点在不可行走区域

代码清单 2 中的 GetNewPatrolDestPoint() 函数还存在问题:如果生成的随机点在不可行走区域,则敌人永远走不到那里,就会出现卡住的现象。关于区域的概念可以看 《Unity3D RPG Core | 04 智能导航地图烘焙》 重新温习一下。

解决以上问题,可以使用 Unity 的 NavMesh.SamplePosition() 函数。其定义为:

  • bool SamplePosition(Vector3 sourcePosition,
  •                     out NavMeshHit hit,
  •                     float maxDistance,
  •                     int areaMask);

函数的作用是,在 sourcePosition 中心点的 maxDistance 范围内,寻找满足 areaMask 掩码定义区域要求的最近的点。如果找到,则返回 true,hit 中附带点的信息;如果没有相关区域,则返回 false。

视频中说 areaMask 为 1 代表可行走区域,但是文档里没有找到定义。留作问题。

还有一个问题是不可行走区域是否也能寻找。

完善过后的 GetNewPatrolDestPoint() 函数如代码清单 3 所示,如果找不到可行走的点,则将随机点设置为当前位置,等待下一次获取。

代码清单 3 完善随机点逻辑
  1. Vector3 GetNewPatrolDestPoint()
  2. {
  3.     float offsetX = Random.Range(-m_patrolRadius, m_patrolRadius);
  4.     float offsetZ = Random.Range(-m_patrolRadius, m_patrolRadius);
  5.  
  6.     Vector3 randomPoint = new Vector3(m_initPosition.x + offsetX, transform.position.y, m_initPosition.z + offsetZ);
  7.  
  8.     NavMeshHit hit;
  9.     if (NavMesh.SamplePosition(randomPoint, out hit, m_patrolRadius, 1))
  10.         randomPoint = hit.position;
  11.     else
  12.         randomPoint = transform.position;
  13.  
  14.     return randomPoint;
  15. }

2.2 让敌人走走停停

我们继续完善敌人的巡逻逻辑:让敌人走到随机点之后,张望一会,然后再走到下一个巡逻点,模拟真实的巡逻场景。

我们在动画控制器中添加一个张望的动画,并定义相应的转移变量 Sensing。如代码清单 4 所示,添加相应变量的更新(第 9 行)。

定义需要停留张望的时间和已经停留的时间变量(第 3 至 4 行)。当走到随机巡逻点后,在原先逻辑上更新停留时间相关变量,并播放张望动画。

代码清单 4 巡逻张望逻辑
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     public float     m_patrolStoppingTime = 2;
  4.     float            m_patrolStoppingRemainTime;
  5.  
  6.     void SwitchAnimation()
  7.     {
  8.         m_animator.SetBool("Walking", m_animatorState.isWalking);
  9.         m_animator.SetBool("Sensing", m_animatorState.isSensing);
  10.         m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
  11.         m_animator.SetBool("Following", m_animatorState.isFollowing);
  12.     }
  13.  
  14.     void SwitchState()
  15.     {
  16.         GameObject attackObject = null;
  17.  
  18.         switch (m_enemyStates)
  19.         {
  20.             case EnemyStates.GUARD:
  21.                 if (FindPlayer() != null)
  22.                 {
  23.                     Debug.Log("HasFoundPlayer");
  24.                     m_enemyStates = EnemyStates.PURSUIT;
  25.                 }
  26.                 break;
  27.             case EnemyStates.PATROL:
  28.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  29.                 m_animatorState.isPursuing = false;
  30.  
  31.                 //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
  32.                 if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
  33.                 {
  34.                     m_navMeshAgent.destination = transform.position;
  35.                     m_animatorState.isWalking = false;
  36.                     m_animatorState.isSensing = true;
  37.                     if (m_patrolStoppingRemainTime > 0)
  38.                     {
  39.                         m_patrolStoppingRemainTime -= Time.deltaTime;
  40.                     }
  41.                     else
  42.                     {
  43.                         m_patrolStoppingRemainTime = m_patrolStoppingTime;
  44.                         m_patrolDestPoint = GetNewPatrolDestPoint();
  45.                     }
  46.                 }
  47.                 else
  48.                 {
  49.                     m_animatorState.isWalking = true;
  50.                     m_animatorState.isSensing = false;
  51.                     m_navMeshAgent.destination = m_patrolDestPoint;
  52.                 }
  53.                 break;
  54.             case EnemyStates.PURSUIT:
  55.                 m_navMeshAgent.speed = m_defaultSpeed;
  56.                 m_animatorState.isWalking = false;
  57.                 m_animatorState.isPursuing = true;
  58.                 if ((attackObject = FindPlayer()) != null)
  59.                 {
  60.                     m_animatorState.isFollowing = true;
  61.                     m_navMeshAgent.destination = attackObject.transform.position;
  62.                 }
  63.                 else
  64.                 {
  65.                     m_animatorState.isFollowing = false;
  66.                     m_navMeshAgent.destination = transform.position;
  67.                 }
  68.                 break;
  69.             case EnemyStates.DEATH:
  70.                 break;
  71.         }
  72.     }
  73. }

最终巡逻张望的效果如图 2 所示,敌人到巡逻点后会张望一会,再到下一个巡逻点。

图2 巡逻张望

3. 完善转移状态转移逻辑

目前状态转移方面还剩下两个大问题:一是巡逻状态下还没有发现角色的逻辑;二是追击状态下,脱战时敌人需要回到原始的区域。

如代码清单 4 所示,第 17 至 22 行添加巡逻状态下的发现角色逻辑,发现角色后转移到追击状态。第 65 至 77 行添加脱战逻辑,脱战时敌人也停留一段时间,然后切换到原先的守卫或者巡逻状态。

代码清单 4 状态转移
  1. public class EnemyController : MonoBehaviour
  2. {
  3.     void SwitchState()
  4.     {
  5.         GameObject attackObject = null;
  6.  
  7.         switch (m_enemyStates)
  8.         {
  9.             case EnemyStates.GUARD:
  10.                 if (FindPlayer() != null)
  11.                 {
  12.                     Debug.Log("HasFoundPlayer");
  13.                     m_enemyStates = EnemyStates.PURSUIT;
  14.                 }
  15.                 break;
  16.             case EnemyStates.PATROL:
  17.                 if (FindPlayer() != null)
  18.                 {
  19.                     Debug.Log("HasFoundPlayer");
  20.                     m_enemyStates = EnemyStates.PURSUIT;
  21.                     break;
  22.                 }
  23.  
  24.                 m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
  25.                 m_animatorState.isPursuing = false;
  26.  
  27.                 //Debug.Log("dist=" + Vector3.Distance(m_patrolDestPoint, transform.position));
  28.                 if (Vector3.Distance(m_patrolDestPoint, transform.position) <= m_navMeshAgent.stoppingDistance)
  29.                 {
  30.                     m_navMeshAgent.destination = transform.position;
  31.                     m_animatorState.isWalking = false;
  32.                     m_animatorState.isSensing = true;
  33.                     if (m_patrolStoppingRemainTime > 0)
  34.                     {
  35.                         m_patrolStoppingRemainTime -= Time.deltaTime;
  36.                     }
  37.                     else
  38.                     {
  39.                         m_patrolStoppingRemainTime = m_patrolStoppingTime;
  40.                         m_patrolDestPoint = GetNewPatrolDestPoint();
  41.                     }
  42.                 }
  43.                 else
  44.                 {
  45.                     m_animatorState.isWalking = true;
  46.                     m_animatorState.isSensing = false;
  47.                     m_navMeshAgent.destination = m_patrolDestPoint;
  48.                 }
  49.                 break;
  50.             case EnemyStates.PURSUIT:
  51.                 m_navMeshAgent.speed = m_defaultSpeed;
  52.                 m_animatorState.isWalking = false;
  53.                 m_animatorState.isSensing = false;
  54.                 m_animatorState.isPursuing = true;
  55.                 if ((attackObject = FindPlayer()) != null)
  56.                 {
  57.                     m_animatorState.isFollowing = true;
  58.                     m_navMeshAgent.destination = attackObject.transform.position;
  59.                 }
  60.                 else
  61.                 {
  62.                     m_animatorState.isFollowing = false;
  63.                     m_navMeshAgent.destination = transform.position;
  64.  
  65.                     if (m_patrolStoppingRemainTime > 0)
  66.                     {
  67.                         m_patrolStoppingRemainTime -= Time.deltaTime;
  68.                     }
  69.                     else
  70.                     {
  71.                         m_patrolStoppingRemainTime = m_patrolStoppingTime;
  72.  
  73.                         if (m_enemyType == EnemyType.PATROL)
  74.                             m_enemyStates = EnemyStates.PATROL;
  75.                         else
  76.                             m_enemyStates = EnemyStates.GUARD;
  77.                     }
  78.                 }
  79.                 break;
  80.             case EnemyStates.DEATH:
  81.                 break;
  82.         }
  83.     }
  84. }