Unity3D RPG Core | 14 随机巡逻点
这篇内容将完善敌人的巡逻逻辑。
1. 定义巡逻范围
首先和敌人的监视范围类似,我们需要定义敌人的巡逻范围。目前存在一个问题,我们无法感知这些范围,以便进行调整。为了解决这个问题,视频中介绍可以使用 Gizmos 将范围画出来。
如代码清单 1 所示,我们在 OnDrawGizmosSelected() 函数中进行 Gizmos 绘制。'Selected' 表示只有在界面上选择了这个对象,才进行绘制,可以使画面不会太杂乱。
- public class EnemyController : MonoBehaviour
- {
- public float m_sightRadius = 5;
- public float m_patrolRadius = 5;
- void OnDrawGizmosSelected()
- {
- Gizmos.color = Color.blue;
- Gizmos.DrawWireSphere(transform.position, m_patrolRadius);
- }
- }
很好的调试手段。
写好代码后,我们返回到 Unity 界面。选择史莱姆对象,就可以看到如图 1 这样,画出了范围球体线框。

2. 实现巡逻逻辑
巡逻的整体逻辑是:基于敌人的初始坐标上,随机生成巡逻范围之内的偏移,并使敌人走到这个点上。前往这个点的过程中播放行走动画;走到了这个点时,播放待机动画,并计算新的随机点。
我们看到代码清单 2,再次梳理上述逻辑。首先看到随机点生成的 GetNewPatrolDestPoint() 函数,x 和 z 坐标计算基于初始坐标的随机偏移,y 坐标不变,防止所在地形高度不一。
SwitchState() 函数中的 EnemyStates.PATROL 分支是巡逻逻辑的主要实现。巡逻时速度相对于追击时减少,isPursuing 变量设置为 false,让动画层在基础层(第 48 至 49 行)。判断是否走到对应点使用 Vector3.Distance() 函数,注意设置的阈值,调试中发现距离不会到达 0,只会无限接近 0。
注意 Vector3.Distance() 的判断阈值。
如果到达位置点,则播放待机动画,并计算新的随机点(第 54 至 55 行);如果没有到达,则播放行走动画,并设置 NavMeshAgent.destination。
- public enum EnemyType
- {
- GUARD,
- PATROL,
- }
- [RequireComponent(typeof(NavMeshAgent))]
- public class EnemyController : MonoBehaviour
- {
- public EnemyType m_enemyType = EnemyType.PATROL;
- public float m_patrolRadius = 5;
- Vector3 m_patrolDestPoint;
- Vector3 m_initPosition;
- void Awake()
- {
- m_navMeshAgent = GetComponent<NavMeshAgent>();
- m_animator = GetComponent<Animator>();
- m_defaultSpeed = m_navMeshAgent.speed;
- m_initPosition = transform.position;
- }
- // Start is called before the first frame update
- void Start()
- {
- if (m_enemyType == EnemyType.PATROL)
- m_enemyStates = EnemyStates.PATROL;
- else
- m_enemyStates = EnemyStates.GUARD;
- m_patrolDestPoint = GetNewPatrolDestPoint();
- }
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- 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_animatorState.isWalking = false;
- m_patrolDestPoint = GetNewPatrolDestPoint();
- }
- else
- {
- m_animatorState.isWalking = true;
- m_navMeshAgent.destination = m_patrolDestPoint;
- }
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- Vector3 GetNewPatrolDestPoint()
- {
- float offsetX = Random.Range(-m_patrolRadius, m_patrolRadius);
- float offsetZ = Random.Range(-m_patrolRadius, m_patrolRadius);
- Vector3 randomPoint = new Vector3(m_initPosition.x + offsetX, transform.position.y, m_initPosition.z + offsetZ);
- return randomPoint;
- }
- }
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 所示,如果找不到可行走的点,则将随机点设置为当前位置,等待下一次获取。
- Vector3 GetNewPatrolDestPoint()
- {
- float offsetX = Random.Range(-m_patrolRadius, m_patrolRadius);
- float offsetZ = Random.Range(-m_patrolRadius, m_patrolRadius);
- Vector3 randomPoint = new Vector3(m_initPosition.x + offsetX, transform.position.y, m_initPosition.z + offsetZ);
- NavMeshHit hit;
- if (NavMesh.SamplePosition(randomPoint, out hit, m_patrolRadius, 1))
- randomPoint = hit.position;
- else
- randomPoint = transform.position;
- return randomPoint;
- }
2.2 让敌人走走停停
我们继续完善敌人的巡逻逻辑:让敌人走到随机点之后,张望一会,然后再走到下一个巡逻点,模拟真实的巡逻场景。
我们在动画控制器中添加一个张望的动画,并定义相应的转移变量 Sensing。如代码清单 4 所示,添加相应变量的更新(第 9 行)。
定义需要停留张望的时间和已经停留的时间变量(第 3 至 4 行)。当走到随机巡逻点后,在原先逻辑上更新停留时间相关变量,并播放张望动画。
- public class EnemyController : MonoBehaviour
- {
- public float m_patrolStoppingTime = 2;
- float m_patrolStoppingRemainTime;
- 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);
- }
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- 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.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- }
最终巡逻张望的效果如图 2 所示,敌人到巡逻点后会张望一会,再到下一个巡逻点。

3. 完善转移状态转移逻辑
目前状态转移方面还剩下两个大问题:一是巡逻状态下还没有发现角色的逻辑;二是追击状态下,脱战时敌人需要回到原始的区域。
如代码清单 4 所示,第 17 至 22 行添加巡逻状态下的发现角色逻辑,发现角色后转移到追击状态。第 65 至 77 行添加脱战逻辑,脱战时敌人也停留一段时间,然后切换到原先的守卫或者巡逻状态。
- public class EnemyController : MonoBehaviour
- {
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- 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.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isSensing = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- if (m_patrolStoppingRemainTime > 0)
- {
- m_patrolStoppingRemainTime -= Time.deltaTime;
- }
- else
- {
- m_patrolStoppingRemainTime = m_patrolStoppingTime;
- if (m_enemyType == EnemyType.PATROL)
- m_enemyStates = EnemyStates.PATROL;
- else
- m_enemyStates = EnemyStates.GUARD;
- }
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- }