前言
本博客中采用的寻路算法主要为A星算法,A星算法主要实现代码借鉴他人博客(忘记原文地址了,如果原作者刚好看到可以留言,博主将注明原文出处,若涉及侵权请联系博主核实后将立马删除)A星算法具体原理不在本文章描述范围, 若想深入学习算法原理可自行搜索关键字:A星算法。
主要实现功能
- AI 在将以自身为原点,生成自定义大小的正方形为检测范围;
- AI 在自由寻路状态时,随机选取正方形边缘上的一点为目标点,朝目标点移动,到达目标点时将再次选取随机点作为目标点进行移动;
- AI生成的地图中每个地图元素都会在成时发射一条向上2米的射线检测,用于检测障碍物(AI定义为1米身高,超过2米的障碍物并不会影响AI前进);
- 地图元素采用对象池模式,每次敌人切换目标时地图元素并不用重新生成,而是初始化后继续使用;
- AI在追随模式时,地图所有元素将与目标点做距离判定,将距离玩家最近的元素作为目标点,当AI到达目标点时若还为玩家位置将再次作出判定,直到接近玩家为止;
- 鼠标左键为自由寻路模式,右键为追随模式,游戏时可自行控制查看效果。
主要代码分析
- AI控制脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
//内部单例
private static Enemy Instance = null;
//路径
private List<Node> path = new List<Node>();
//起点
private GameObject start;
//目标
private GameObject end;
//开始寻路
public bool startPathfinding = true;
//克隆体
public GameObject cube;
//行
public int row;
//计时器
public float timer = 0;
//地图
private Dictionary<Vector3, Node> map = new Dictionary<Vector3, Node>();
//圆点
private GameObject dot;
//辅助线
private LineRenderer line = null;
//移动点数量
public int pathCount = 0;
/// <summary>
/// 单利访问
/// </summary>
public static Enemy instance
{
get
{
return Instance;
}
}
private void Awake()
{
Instance = this;
//辅助线
line = new GameObject("Line").AddComponent<LineRenderer>();
line.startWidth = 0.1f;
line.endWidth = 0.1f;
dot = new GameObject("Dot");
}
private void Update()
{
//测试
if (Input.GetMouseButtonDown(0)) AutoFindWay();
if (Input.GetMouseButtonDown(1)) PursueTarget();
if (startPathfinding)
{
if (path.Count > 0)
{
transform.position = Vector3.MoveTowards(transform.position, path[pathCount].transform.position, Time.deltaTime);
transform.LookAt(path[pathCount].transform.position);
if (Vector3.Distance(transform.position, path[pathCount].transform.position) <= 0)
{
pathCount--;
}
}
}
else
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit) && hit.collider != null && hit.collider.GetComponent<Node>() != null)
{
path.Clear();
FindTargetPath(hit.collider.gameObject);
startPathfinding = true;
}
}
}
/// <summary>
/// 自动寻路
/// </summary>
public void AutoFindWay()
{
path.Clear();
NewPath();
startPathfinding = true;
}
/// <summary>
/// 追击目标
/// </summary>
public void PursueTarget()
{
path.Clear();
FindTargetPath(FindObjectOfType<Player>().gameObject);
startPathfinding = true;
}
/// <summary>
/// 新路线
/// </summary>
private void NewPath()
{
print("寻找新路径 >>>>>>>>>>");
//原点到两边距离相等
if (row % 2 == 0) row++;
//圆点位置等于自身位置
dot.transform.position = transform.position;
//以自身为圆点形成坐标坐标系
MovingRangeFormationPoint(map,row);
//设置地图
NodeManager.Instance.getMap(dot.GetComponentsInChildren<Node>());
//起点
start = map[FindRecently(map, transform.transform)].gameObject;
//目标点
end = map[EdgeGetTarget(start.transform)].gameObject;
//延迟0.5s寻路
Invoke("FindWay", 0.5f);
}
/// <summary>
/// 找到目标
/// </summary>
public void FindTargetPath(GameObject target)
{
print("以距离目标最近的点作为路径 >>>>>>");
//原点到两边距离相等
if (row % 2 == 0) row++;
//圆点位置等于自身位置
dot.transform.position = transform.position;
//以自身为圆点形成坐标坐标系
MovingRangeFormationPoint(map,row);
//设置地图
NodeManager.Instance.getMap(dot.GetComponentsInChildren<Node>());
//起点
start = map[FindRecently(map, transform.transform)].gameObject;
//目标点
end = map[FindRecently(map,target.transform)].transform.gameObject;
//延迟1s寻路
Invoke("FindWay", 0.5f);
}
/// <summary>
/// 以自身为圆点形成坐标坐标系
/// </summary>
private void MovingRangeFormationPoint(Dictionary<Vector3, Node> map, int row)
{
print("以自身为圆点形成坐标坐标系");
//以自身为原点
Vector3 centerPos = dot.transform.position - new Vector3(row * 0.5f - 0.5f, 0, row * 0.5f - 0.5f);
//复用之前的对象
if (map.Count > 0)
{
//清空障碍物
NodeManager.Instance.CloseObstacle();
//下标
int index = 0;
//地图上的点(重复利用)
List<Node> points = new List<Node>();
//找出之前的点
foreach (Vector3 i in map.Keys)
{
//坐标初始化
map[i].Init();
//加入对象池列表
points.Add(map[i]);
}
//清空地图
map.Clear();
for (int i = 0; i < row; i++)
{
for (int j = 0; j < row; j++)
{
//正方形坐标
Vector3 pos = new Vector3(centerPos.x + i, 0, centerPos.z + j);
//点的位置重新赋值
points[index].transform.position = pos;
//加入地图
map.Add(pos, points[index]);
//下一个点
index++;
}
}
}
else
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < row; j++)
{
Vector3 pos = new Vector3(centerPos.x + i, 0, centerPos.z + j);
//正方形坐标
GameObject game = Instantiate(cube, pos, Quaternion.identity);
//将地图设置为圆点的之物体
game.transform.SetParent(dot.transform);
//点坐标位置
game.name = pos.ToString();
//添加至地图
map.Add(pos, game.GetComponent<Node>());
//坐标初始化
map[pos].Init();
}
}
}
}
/// <summary>
/// 寻找路线
/// </summary>
public void FindWay()
{
if (end.GetComponent<Node>().nObstacle)
{
NewPath();
Debug.LogError("目标为障碍物,重新寻路");
return;
}
//路径
path = AStar.FindPath(start.GetComponent<Node>(), end.GetComponent<Node>());
//画辅助线
line.positionCount = path.Count;
for (int i = 0; i < path.Count; i++)
{
line.SetPosition(i, path[i].gameObject.transform.position);
}
//移动路径大小
pathCount = path.Count - 1;
}
/// <summary>
/// 边缘获取目标位置
/// </summary>
private Vector3 EdgeGetTarget(Transform dot)
{
List<Vector3> squarePoints = new List<Vector3>();
int width = (row - 1) / 2;
for (int j = row - 1; j >= 0; j--)
{
squarePoints.Add(dot.position + new Vector3(width - j, 0, width));
}
for (int j = 1; j < row; j++)
{
squarePoints.Add(dot.position + new Vector3(width, 0, width - j));
}
for (int j = 1; j < row; j++)
{
squarePoints.Add(dot.position + new Vector3(width - j, 0, -width));
}
for (int j = row - 2; j > 0; j--)
{
squarePoints.Add(dot.position + new Vector3(-width, 0, width - j));
}
int randomTarget = Random.Range(0, squarePoints.Count - 1);
return squarePoints[randomTarget];
}
/// <summary>
/// 寻找地图最近的点
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
private Vector3 FindRecently(Dictionary<Vector3, Node> map, Transform target)
{
List<Vector3> dos = new List<Vector3>();
//找出地图上的点
foreach (Vector3 i in map.Keys)
{
dos.Add(i);
}
//寻找距离玩家最近的点
Vector3 miniDis = dos[0];
for (int i = 1; i < dos.Count; i++)
{ //找出距离玩家最近位置点
if (Vector3.Distance(miniDis, target.position) > Vector3.Distance(dos[i], target.position))
{
miniDis = dos[i];
}
}
return map[miniDis].transform.position;
}
}
- 地图元素管理类
using System.Collections.Generic;
using UnityEngine;
public class NodeManager : MonoBehaviour
{
private static NodeManager instance = null;
public static NodeManager Instance
{
get
{
if (instance == null)
instance = FindObjectOfType(typeof(NodeManager)) as NodeManager;
if (instance == null)
Debug.LogError("Could not find a NodeManager. Please add one NodeManager in the scense");
return instance;
}
}
//地图坐标
public Node[,] nodes;
/// <summary>
/// 障碍物列表
/// </summary>
public List<GameObject> obstacleList = new List<GameObject>();
//设置为障碍物
public void SetObstacle(GameObject obs)
{
if (obstacleList.Contains(obs))
return;
//障碍物列表
obstacleList.Add(obs);
//设置障碍物
obs.GetComponent<Node>().MarkAsObstacle();
}
/// <summary>
/// 清理障碍物
/// </summary>
public void CloseObstacle()
{
foreach (GameObject i in obstacleList)
{
i.GetComponent<Renderer>().material.color = Color.white;
}
obstacleList.Clear();
print("清理障碍物");
}
//得到地图
public void getMap(Node[] games)
{
int index = -1;
nodes = new Node[Enemy.instance.row, Enemy.instance.row];
for (int i = 0; i < Enemy.instance.row; i++)
{
for (int j = 0; j < Enemy.instance.row; j++)
{
index++;
nodes[i, j] = games[index];
}
}
}
/// <summary>
/// 得到附近节点
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public List<Node> GetNeighbours(Node node)
{
//附近节点
List<Node> neighbours = new List<Node>();
for (int i = 0; i < Enemy.instance.row; i++)
{
for (int j = 0; j < Enemy.instance.row; j++)
{
if(nodes[i,j] == node)
{
neighbours = AssignNeighbours(i, j + 1, neighbours);//up
//neighbours = AssignNeighbours(i + 1, j + 1, neighbours);//up + right
neighbours = AssignNeighbours(i + 1, j, neighbours);//right
//neighbours = AssignNeighbours(i + 1, j - 1, neighbours);//right + down
neighbours = AssignNeighbours(i, j - 1, neighbours);//down
//neighbours = AssignNeighbours(i - 1, j - 1, neighbours);//down + left
neighbours = AssignNeighbours(i - 1, j, neighbours);//left
//neighbours = AssignNeighbours(i - 1, j + 1, neighbours);//left + up
}
}
}
return neighbours;
}
/// <summary>
/// 分配的临近点
/// </summary>
/// <param name="row"></param>
/// <param name="col"></param>
/// <param name="neighbours"></param>
/// <returns></returns>
private List<Node> AssignNeighbours(int row, int col, List<Node> neighbours)
{
//
if (row >= 0 && col >= 0 && row < Enemy.instance.row && col < Enemy.instance.row)
{
//不属于障碍物
if(!obstacleList.Contains(nodes[row, col].gameObject))
{
neighbours.Add(nodes[row, col]);
}
}
return neighbours;
}
}
- 地图元素类
using System;
using UnityEngine;
using UnityEngine.UI;
public class Node : MonoBehaviour, IComparable
{
/// <summary>
/// 节点到总成本 G
/// </summary>
public float nodeTotalCost = 1.0f;
/// <summary>
/// 估计成本 F
/// </summary>
public float estimateCost = 0.0f;
/// <summary>
/// 路径的父节点
/// </summary>
public Node nParent;
/// <summary>
/// 是否障碍物
/// </summary>
public bool nObstacle = false;
/// <summary>
/// 初始化
/// </summary>
public void Init()
{
nodeTotalCost = 1.0f;
estimateCost = 0.0f;
nObstacle = false;
nParent = null;
//向上2米检测是否有障碍物
if (Physics.Raycast(transform.position, transform.up, out RaycastHit hit, 2))
{
//加入障碍物列表
NodeManager.Instance.SetObstacle(gameObject);
//画出辅助线
Debug.DrawRay(transform.position, transform.up * 2, Color.red, 10);
}
}
/// <summary>
/// 设置为障碍物
/// </summary>
public void MarkAsObstacle()
{
nObstacle = true;
}
/// <summary>
/// 对比路径成本
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int CompareTo(object obj)
{
Node node = (Node)obj;
if(estimateCost < node.estimateCost)
return -1;
else if(estimateCost > node.estimateCost)
return 1;
else
return 0;
}
public override string ToString()
{
return transform.position.x + "," + transform.position.y;
}
/// <summary>
/// 显示大小
/// </summary>
public void ShowCost()
{
var text = GetComponentInChildren<Text>();
if (nObstacle)
{
text.text = "障碍物";
text.color = Color.red;
}
else {
text.text = "F: " + estimateCost.ToString("F2") + "\n" +
"G: " + nodeTotalCost.ToString("F2") + "\n";
}
}
}
- A星算法主要实现类
using System.Collections.Generic;
using UnityEngine;
public class AStar : MonoBehaviour
{
//开启队列
public static PriorityQueue openQueue;
//关闭队列
public static PriorityQueue closeQueue;
//触发式评估大小
private static float HeuristicEstimateCost(Node currentNode, Node goalNode)
{
//返回两个节点的距离
return Vector3.Distance(currentNode.transform.position, goalNode.transform.position);
}
/// <summary>
/// 寻找路径
/// </summary>
/// <param name="start">开始位置</param>
/// <param name="goal">目标位置</param>
/// <returns></returns>
public static List<Node> FindPath(Node start, Node goal)
{
openQueue = new PriorityQueue();
//天际节点
openQueue.Add(start);
//初始化成本为0
start.nodeTotalCost = 0;
//估计成本等于自身到目标的位置
start.estimateCost = HeuristicEstimateCost(start, goal);
closeQueue = new PriorityQueue();
//起始节点
Node node = null;
//打开队列长度大于0
while (openQueue.Count != 0)
{
//得到起点
node = openQueue.First();
//判断起点是否等于目标点
if(node == goal)
{
//node.GetComponentInChildren<Text>().text = "目标点";
print("寻路完毕");
break;
}
//得到周围的位置
List<Node> neighbours = NodeManager.Instance.GetNeighbours(node);
foreach(Node neighbour in neighbours)
{
//不在关闭队列
if (!closeQueue.Contains(neighbour))
{
//节点总成本=节点初始节点 + 附近点到起点距离
neighbour.nodeTotalCost = node.nodeTotalCost + HeuristicEstimateCost(neighbour,node);
//估计成本 = 附近点到目标点距离 + 节点总成本
neighbour.estimateCost = HeuristicEstimateCost(neighbour, goal) + neighbour.nodeTotalCost;
//附近节点的父物体等于起点
neighbour.nParent = node;
//不在开启队列
if (!openQueue.Contains(neighbour))
{
//加入开启队列
openQueue.Add(neighbour);
}
}
}
//起点加入关闭队列
closeQueue.Add(node);
//起点从打开队列移除
openQueue.Remove(node);
}
if (node != goal)
{
Debug.LogError("找不到路径");
}
return CalculatePath(node);
}
/// <summary>
/// 计算路径
/// </summary>
/// <param name="node"></param>
/// <returns>计算后得到的路径</returns>
private static List<Node> CalculatePath(Node node)
{
//路径列表
List<Node> path = new List<Node>();
while (node != null)
{
//节点加入路径列表
path.Add(node);
//设置父物体
node = node.nParent;
}
return path;
}
}
游戏运行效果
场景配置图
游戏运行效果
动手操作
打开unity,新建一个3D项目,将 AI自动寻路+绕路+追随.unitypackage文件导入项目,打开Game场景,点击开始运行就可以看到效果了!
AI自动寻路+绕路+追随.unitypackage
资源提取码: 7t1p
结语
如果你觉得文章还不错就点点关注吧!
如果你有更好的提议欢迎评论区留言或者私信!
如果你想在第一时间里了解更多关于我的作品,或者想了解编程界其他技术,请扫码关注个人微信公众号,公众号留言将持续带来更新!