本文将向大家介绍一套简单的敌人AI巡逻与追击的算法,简称漫游算法。
主要就是利用协程来编写一个脚本,让敌人在地图上随机漫游。如果敌人发现玩家在附近,敌人会追击玩家,直到玩家逃跑出敌人的攻击范围。
漫游算法可能听起来很复杂,但是当我们逐步分解它时,就会发现这一切都是非常容易实现的。下图就是蛮有算法的示意图。我们将分阶段实现每个部分,并在前进的过程中逐一解释,这样就不会感到不知所措。
接下来,我将以简单的2D游戏来实现和说明这个算法。
1.准备工作
首先给我们“敌人”的预制上添加CircleCollider2D组件,作为玩家感知的视野。因为是当做触发器来用所以我们需要勾选IsTrigger。(当然大家也可以用射线检测或者距离判断来编写“敌人”的视野范围,这里用这个触发器只是更加方便的说明)
2.创建漫游脚本
完成了准备工作,我们开始创建漫游的控制脚本取名为Wander.cs,将脚本拖到我们“敌人”的预制上。并声明一些我们需要的变量,还有一些组件的初始化逻辑。
每个变量的意义都有注释,就不一一解释了,看代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(Animator))]
public class Wander : MonoBehaviour
{
public float pursuitSpeed;//追击速度
public float wanderSpeed;//漫游速度
float currentSpeed;//当前速度
public float directionChangeInterval;//改变漫游方向的频率
public bool followPlayer;//是否有追击目标功能
Coroutine moveCoroutine;//用于保存当前正在运动的协程
Transform targetTransform = null;//追击目标
Vector3 endPosition;//漫游目标
float currentAngle = 0;//重新转向的角度
CircleCollider2D circleCollider;//追击范围的碰撞盒
Rigidbody2D rb2d;
Animator animator;
private void Start()
{
animator = GetComponent<Animator>();
currentSpeed = wanderSpeed;
rb2d = GetComponent<Rigidbody2D>();
circleCollider = GetComponent<CircleCollider2D>();
}
}
这些带[中括号]的函数主要作用是用来确保我们挂载脚本的预制必须有以上三个组件。
3.移动及漫游逻辑
/// <summary>
/// 漫游算法入口
/// </summary>
/// <returns></returns>
public IEnumerator WanderRoutine()
{
while (true)
{
ChooseNewEndpoint();
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
yield return new WaitForSeconds(directionChangeInterval);
}
}
其中ChooseNewEndpoint()方法的作用是选择一个新的目标点,并不会去处理移动的逻辑。
Move(rb2d, currentSpeed)的这个协程主要是用来负责移动。
下面将贴出ChooseNewEndpoint()方法的逻辑实现:(还是一样看注释就完了~)
/// <summary>
/// 选择新的目标点
/// (负责随机选择一个新的端点,供敌人前往)
/// </summary>
private void ChooseNewEndpoint()
{
//随机选择新的方向,以角度来表示
currentAngle += Random.Range(0, 360);
//限制角度在360°之内
currentAngle = Mathf.Repeat(currentAngle, 360);
endPosition += Vector3FromAngle(currentAngle);
}
/// <summary>
/// 将角度转换为弧度并以向量Vector3来返回
/// </summary>
/// <param name="inputAngleDegrees"></param>
/// <returns></returns>
private Vector3 Vector3FromAngle(float inputAngleDegrees)
{
//乘以度数到弧度的转换常数,将角度从读书装换为弧度
float inputAngleRadians = inputAngleDegrees * Mathf.Deg2Rad;
//使用以弧度表示的输入角度,为敌人方向创建标准化的方向向量
return new Vector3(Mathf.Cos(inputAngleRadians), Mathf.Sin(inputAngleRadians), 0);
}
接下来是Move()方法的逻辑代码:(一样没什么难度,看注释就好~)
/// <summary>
/// 移动
/// </summary>
/// <param name="rigidbodyToMove"></param>
/// <param name="speed"></param>
/// <returns></returns>
private IEnumerator Move(Rigidbody2D rigidbodyToMove, float speed)
{
//求得敌人当前位置和目的地之间的粗略距离
float remainingDistance = (transform.position - endPosition).sqrMagnitude;
while (remainingDistance > float.Epsilon)
{
//当targetTransform不为null的时候为追击,目标为玩家
if (targetTransform != null)
{
endPosition = targetTransform.position;
}
if (rigidbodyToMove != null)
{
animator.SetBool("isWalking", true);
//用于计算Rigidbody2D的移动
Vector3 newPosition = Vector3.MoveTowards(rigidbodyToMove.position, endPosition, speed * Time.deltaTime);
//移动到上面计算好的位置
rb2d.MovePosition(newPosition);
//更新剩余距离
remainingDistance = (transform.position - endPosition).sqrMagnitude;
}
yield return new WaitForEndOfFrame();
}
animator.SetBool("isWalking", false);
}
以上我们就基本完成了漫游算法的基本逻辑,其实“敌人”移动的实现方法有很多种,这里面只是简单的举例说明,也可以替换成各位自己习惯实现移动的方法或者根据项目需要去编写移动逻辑,只要了解我们漫游算法的原理,很多东西都是可以替换的。
4.追逐逻辑触发
上面的脚本已经实现了除了追逐逻辑之外的几乎所有的漫游算法了,现在我们将开始编写追逐玩家的逻辑。因为我们使用触发器来判断的,所以就要用的OnTriggerEnter2D()和OnTriggerExit2D()这两个触发器的方法。上代码:
private void OnTriggerEnter2D(Collider2D collision)
{
//检测玩家进入攻击范围,切换为追击状态
if (collision.gameObject.CompareTag("Player") && followPlayer)
{
//转换为追击的移动速度
currentSpeed = pursuitSpeed;
//设置追击目标
targetTransform = collision.transform;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
//切换移动
moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
}
}
private void OnTriggerExit2D(Collider2D collision)
{
//当玩家离开攻击范围,切换为漫游状态
if (collision.gameObject.CompareTag("Player"))
{
animator.SetBool("isWalking", false);
//转换为漫游的移动速度
currentSpeed = wanderSpeed;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
targetTransform = null;
}
}
至此,我们漫游算法的主要逻辑已经完成,只需要在Start方法中调用WanderRoutine蛮有算法的入口协程就可以开启漫游算法的逻辑。
5.辅助线
我们将利用Gizmo来做一些辅助线来帮助我们观察怪物的视野范围以及移动路径,这些都只是在Editor的状态下才能看到,发布出来的项目并不影响。直接上代码:
private void OnDrawGizmos()
{
if (circleCollider != null)
{
//显示追击范围
Gizmos.DrawWireSphere(transform.position, circleCollider.radius);
}
}
#if UNITY_EDITOR
private void Update()
{
//显示移动路径
Debug.DrawLine(rb2d.position, endPosition, Color.red);
}
#endif
6.最后的最后
大家是不是觉得说的啰啰嗦嗦的,下面我将把这个脚本的完整代码直接贴出来,方便大家搬砖:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(Animator))]
public class Wander : MonoBehaviour
{
public float pursuitSpeed;//追击速度
public float wanderSpeed;//漫游速度
float currentSpeed;//当前速度
public float directionChangeInterval;//改变漫游方向的频率
public bool followPlayer;//是否有追击目标功能
Coroutine moveCoroutine;//用于保存当前正在运动的携程
Rigidbody2D rb2d;
Animator animator;
Transform targetTransform = null;//追击目标
Vector3 endPosition;//漫游目标
float currentAngle = 0;//重新转向的角度
CircleCollider2D circleCollider;//追击范围的碰撞盒
private void Start()
{
animator = GetComponent<Animator>();
currentSpeed = wanderSpeed;
rb2d = GetComponent<Rigidbody2D>();
StartCoroutine(WanderRoutine());
circleCollider = GetComponent<CircleCollider2D>();
}
private void OnDrawGizmos()
{
if (circleCollider != null)
{
//显示追击范围
Gizmos.DrawWireSphere(transform.position, circleCollider.radius);
}
}
#if UNITY_EDITOR
private void Update()
{
//显示移动路径
Debug.DrawLine(rb2d.position, endPosition, Color.red);
}
#endif
private void OnTriggerEnter2D(Collider2D collision)
{
//检测玩家进入攻击范围,切换为追击状态
if (collision.gameObject.CompareTag("Player") && followPlayer)
{
//转换为追击的移动速度
currentSpeed = pursuitSpeed;
//设置追击目标
targetTransform = collision.transform;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
//切换移动
moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
}
}
private void OnTriggerExit2D(Collider2D collision)
{
//当玩家离开攻击范围,切换为漫游状态
if (collision.gameObject.CompareTag("Player"))
{
animator.SetBool("isWalking", false);
//转换为漫游的移动速度
currentSpeed = wanderSpeed;
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
targetTransform = null;
}
}
/// <summary>
/// 漫游算法入口
/// </summary>
/// <returns></returns>
public IEnumerator WanderRoutine()
{
while (true)
{
ChooseNewEndpoint();
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
yield return new WaitForSeconds(directionChangeInterval);
}
}
/// <summary>
/// 选择新的目标点
/// (负责随机选择一个新的端点,供敌人前往)
/// </summary>
private void ChooseNewEndpoint()
{
//随机选择新的方向,以角度来表示
currentAngle += Random.Range(0, 360);
//限制角度在360°之内
currentAngle = Mathf.Repeat(currentAngle, 360);
endPosition += Vector3FromAngle(currentAngle);
}
/// <summary>
/// 将角度转换为弧度并以向量Vector3来返回
/// </summary>
/// <param name="inputAngleDegrees"></param>
/// <returns></returns>
private Vector3 Vector3FromAngle(float inputAngleDegrees)
{
//乘以度数到弧度的转换常数,将角度从读书装换为弧度
float inputAngleRadians = inputAngleDegrees * Mathf.Deg2Rad;
//使用以弧度表示的输入角度,为敌人方向创建标准化的方向向量
return new Vector3(Mathf.Cos(inputAngleRadians), Mathf.Sin(inputAngleRadians), 0);
}
/// <summary>
/// 移动
/// </summary>
/// <param name="rigidbodyToMove"></param>
/// <param name="speed"></param>
/// <returns></returns>
private IEnumerator Move(Rigidbody2D rigidbodyToMove, float speed)
{
//求得敌人当前位置和目的地之间的粗略距离
float remainingDistance = (transform.position - endPosition).sqrMagnitude;
while (remainingDistance > float.Epsilon)
{
//当targetTransform不为null的时候为追击,目标为玩家
if (targetTransform != null)
{
endPosition = targetTransform.position;
}
if (rigidbodyToMove != null)
{
animator.SetBool("isWalking", true);
//用于计算Rigidbody2D的移动
Vector3 newPosition = Vector3.MoveTowards(rigidbodyToMove.position, endPosition, speed * Time.deltaTime);
//移动到上面计算好的位置
rb2d.MovePosition(newPosition);
//更新剩余距离
remainingDistance = (transform.position - endPosition).sqrMagnitude;
}
yield return new WaitForEndOfFrame();
}
animator.SetBool("isWalking", false);
}
}
其实漫游算法并算不上什么高端的算法,比起A星等寻路逻辑来说还是比较简单的,这里我们可以去学习去写敌人AI时的思想方法。首先给自己列出一个功能流程图,然后去逐步完成我们的功能逻辑,这样可能是一个比较健康的写逻辑方法。