在游戏中,AI人物的移动往往有许多种实现方法,本文主要列出其中的几种常见的2D寻路方法并附上完整源代码,供读者参考,批评以及指正。
所有的代码均在Unity下完成,并通过测试可以使用。

Depth-First-Search 深度优先搜索
深度优先(DFS)算法,正如他的名字,每次先往深度走,如果此路走到底都没找到,退回到原点,换另一条路找,如果走到底还是没找到,继续换一条路,直到全部路走完。
DFS由于每次向深处搜索然后返回,很容易就让人想到用栈实现,而系统本来就有提供栈,即用递归实现。
由于深度优先算法有可能出现搜索不到目标点的情况,在这里就没有实现,仅仅做简单介绍。
Breadth First Search 广度优先搜索
广度优先搜索算法(BFS),在寻路中广度搜索算法是一种彻彻底底的盲目搜索,也就是以自己为单位,搜索地图中的每一个格子并且和目标比对,如果找到,则返回路径。
BFS的实现需要用队列,大体步骤如下:
-
首先将根节点放入队列中。
-
从队列中取出第一个节点,并检验它是否为目标。
-
如果找到目标,则结束搜寻并回传结果。
-
否则将它所有尚未检验过的直接子节点(邻节点)加入队列中。
-
若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”
简单地说,就是从自身出发,像横竖斜向共八个方向进行搜索,询问自己的邻居们是不是目标点,如果不是,则将他们加入队列中。将自己的邻居遍历完了,发现没有要找的点,那么就从队列中取出之前的邻居,以邻居为起点,进行横竖斜向共八个方向的搜索,再看能不能找到目标,如此反复。
以下是部分代码,完整代码会放在文末,下同。
public const int XLength = MapManager.XLength;
public const int YLength = MapManager.YLength;
public Point[,] Grid = MapManager.Grid;
public bool[,] Book = new bool[XLength, YLength];//用于确定该点是否来过
public Stack<Point> PathPosStack = new Stack<Point>(); //存放最终寻路结果的栈
public int[] StepX = new int[] {
1, -1, 0, 0, 1,-1, 1, -1 };//向横竖以及斜方向八个方向走
public int[] StepY = new int[] {
0, 0, 1, -1,1, 1, -1, -1 };
public Stack<Point> FindPath(Point StartPoint,Point EndPoint)
{
//清除上一次算法留下的节点与节点之间的父子关系
ClearPoint();
Queue<Point> Queue = new Queue<Point>();
Book[StartPoint.PosX, StartPoint.PosY] = true;
Queue.Enqueue(StartPoint);
while(Queue.Count>0)
{
Point CurrentPoint = Queue.Dequeue();
int X = CurrentPoint.PosX;
int Y = CurrentPoint.PosY;
//八个方向都尝试走一下,如果可以走则加入Queue中,否则不加入
for(int i=0;i<8;i++)
{
int NextX = X + StepX[i];
int NextY = Y + StepY[i];
if(NextX<0 || NextX>=XLength || NextY<0 || NextY>=YLength ||
Grid[NextX,NextY].IsObstacle==true || Book[NextX,NextY]==true)
{
continue;
}
Queue.Enqueue(Grid[NextX, NextY]);
Grid[NextX, NextY].ParentPoint = CurrentPoint;
Book[NextX, NextY] = true;
}
//到达终点,返回
if (CurrentPoint == EndPoint)
{
PathPosStack.Clear();
while (CurrentPoint!=StartPoint)
{
PathPosStack.Push(CurrentPoint);
CurrentPoint = CurrentPoint.ParentPoint;
}
PathPosStack.Push(StartPoint);
return PathPosStack;
}
}
return null;
}
BFS没有办法计算代价。这句话是什么意思呢?举个例子,目前我们的地图中只有墙。墙是不可能越过的。但是如果地图中加入了可以走过但是花费时间会比较久的沼泽地,广度搜索就没有办法衡量这个代价了,它只会直接走过去(如下图)。这明显不是我们想要的结果。

Dijkstra 算法
迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。
我们不管这玩意叫什么,只要知道,这玩意能解决刚才说的沼泽地难题。
Dijkstra算法就是通过在每一个地图格子中加入一个权值G,这个G值表示的是上一个格子到当前格子所需要花费的代价。Dijkstra在每次搜索的时候都会计算周围格子的G值,然后每次都选择G值最小的格子作为下一个拓展格子。搜索到最后,Dijkstra能返回代价最小的路径,也就是最优路径。
这比BFS智能多了,BFS会将每一个周围的格子都拓展搜索一遍,相当盲目。
部分代码如下:
public const int XLength = MapManager.XLength;
public const int YLength = MapManager.YLength;
public Point[,] Grid = MapManager.Grid;
public Stack<Point> PathPosStack = new Stack<Point>();
public Stack<Point> FindPath(Point StartPoint, Point EndPoint)
{
//清除上一次算法留下的节点与节点之间的父子关系
ClearPoint();
//初始化Open表和Close表
List<Point> OpenList = new List<Point>();
List<Point> CloseList = new List<Point>();
//开始时将起点加入Open表
OpenList.Add(StartPoint);
while (OpenList.Count > 0)
{
//寻找Open表中G值最小的节点
Point MinPoint = FindMinPoint(OpenList);
OpenList.Remove(MinPoint);
CloseList.Add(MinPoint);
//寻找MinPoint周围的点(边界和障碍物不会算在内)
List<Point> SurroundPoints = FindSurroundPoints(MinPoint);
//如果SurroundPoints中的点在Close表中出现过,则移除这些点
foreach (Point ClosePoint in CloseList)
{
if (SurroundPoints.Contains(ClosePoint))
{
SurroundPoints.Remove(ClosePoint);
}
}
//遍历SurroundPoints中的点
foreach (Point Son in SurroundPoints)
{
//若该点在Open表中出现过,则检查这条路径是否更优,
//也就是说经由当前方格(我们选中的方格) 到达那个方格是否具有更小的 G 值。
if (OpenList.Contains(Son))
{
float NewPathG = CalcG(Son, MinPoint);
//如果 G 值更小,则把那个方格的父亲设为当前方格 ( 我们选中的方格 ) ,
//然后重新计算那个方格的G 值
if (NewPathG < Son.G)
{
Son.ParentPoint = MinPoint;
Son.G = NewPathG;
}
//如果没有,不做任何操作。
}
else
{
//若该点没有在Open表中出现过,则直接计算G值存入点内,且将该点的父亲设置为minPoint
Son.G=CalcG(Son, MinPoint);
Son.ParentPoint = MinPoint;
OpenList.Add(Son);
}
}
//若已经到达终点,则退出循环
if (OpenList.IndexOf(EndPoint) > -1)
{
break;
}
}
//返回寻路结果
return GetPathWay(StartPoint, EndPoint);
}
Dijkstra算法虽然可以计算出最优的路径,但是它也存在盲目搜索的问题。因为虽然Dijkstra每次选择的格子都是代价最小的那一个,但不会考虑这个格子是否离终点更近一步。有可能出现终点在右边,但Dijkstra选择的下一个格子往左边走的情况。
Best-Frist-Search贪婪最佳优先搜索
贪婪最佳优先搜索就可以解决这个问题。
贪婪最佳优先搜索(BestFrist)是一种启发式算法,其也会在地图格子中添加权值,但这次添加的权值H表示的是离终点的代价。也就是采用每个格子到目标格子的距离进行排序。每次搜索都选择离终点最近的那一个格子。
public const int XLength = MapManager.XLength;
public const int YLength = MapManager.YLength;
public Point[,] Grid = MapManager.Grid;
public Stack<Point> PathPosStack = new Stack<Point>();public Stack<Point> FindPath(Point StartPoint, Point EndPoint)
{
//清除上一次算法留下的节点与节点之间的父子关系
ClearPoint();
//初始化Open表和Close表
List<Point> OpenList = new List<Point>();
List<Point> CloseList = new List<Point>();
//开始时将起点加入Open表
OpenList.Add(StartPoint);
while (OpenList.Count > 0)
{
//寻找Open表中H值最小的节点
Point MinPoint = FindMinPoint(OpenList);
OpenList.Remove(MinPoint);
CloseList.Add(MinPoint);
//寻找MinPoint周围的点(边界和障碍物不会算在内)
List<Point> SurroundPoints = FindSurroundPoints(MinPoint);
//如果SurroundPoints中的点在Close表中出现过,则移除这些点
foreach (Point ClosePoint in CloseList)
{
if (SurroundPoints.Contains(ClosePoint))
{
SurroundPoints.Remove(ClosePoint);
}
}
//遍历SurroundPoints中的点
foreach (Point Son in SurroundPoints)
{
//若该点没有在Open表中出现过,则直接计算G值存入点内,且将该点的父亲设置为minPoint
Son.H = CalcH(Son, EndPoint);
Son.ParentPoint = MinPoint;
OpenList.Add(Son);
}
//若已经到达终点,则退出循环
if (OpenList.IndexOf(EndPoint) > -1)
{
break;
}
}
//返回寻路结果
return GetPathWay(StartPoint, EndPoint);
}
这么一来就很明显了,BestFirst算法理论上是最快的,毕竟你每次都在向终点靠近。但是这样也牺牲了准确性,BestFirst没有办法计算出最优路径,只能给出一个相对最优的路径,最终的搜索路径就会显得有点怪怪的。

那么有没有折中的方法,既可以保证搜索的最优性,又可以保证搜索的快速性呢?
A*算法
终于到A算法了。A(念做:A Star)算法是一种很常用的路径查找和图形遍历算法。它有较好的性能和准确度。其于1968年,由Stanford研究院的Peter Hart, Nils Nilsson和Bertram Raphael发表。它可以被认为是Dijkstra算法的扩展。
A* 算法是一种启发式算法,它利用启发信息寻找最优路径。A* 算法需要在地图中搜索节点,并设定适合的启发函数进行指导。通过评价各个节点的代价值,获取下一需要拓展的最佳节点,直至到达最终目标点位置。A* 算法优点在于对环境反应迅速,搜索路径直接,是一种直接的搜索算法,因 此被广泛应用于路径规划问题。其缺点是实时性差,每一节点计算量大、运算时间长,而且随着节点数的增多,算法搜索效率降低,而且A* 算法并没有完全遍历所有可行解,所得到的结果不一定是最优解。
A算法结合了Dijkstra和Best-First算法的优点,在每个格子中赋予的权值F=G+H,也就是结合了前两个算法的权值进行判断。
A算法可以参考我转载的这篇文章,非常浅显易懂
AStar算法的超详细介绍
部分代码:
public const int XLength = MapManager.XLength;
public const int YLength = MapManager.YLength;
public Point[,] Grid = MapManager.Grid;
public Stack<Point> PathPosStack = new Stack<Point>();
public Stack<Point> FindPath(Point StartPoint, Point EndPoint)
{
//清除上一次算法留下的节点与节点之间的父子关系
ClearPoint();
//初始化Open表和Close表
List<Point> OpenList = new List<Point>();
List<Point> CloseList = new List<Point>();
//开始时将起点加入Open表
OpenList.Add(StartPoint);
while (OpenList.Count > 0)
{
//寻找Open表中F值最小的节点
Point MinPoint = FindMinPoint(OpenList);
OpenList.Remove(MinPoint);
CloseList.Add(MinPoint);
//寻找MinPoint周围的点(边界和障碍物不会算在内)
List<Point> SurroundPoints = FindSurroundPoints(MinPoint);
//如果SurroundPoints中的点在Close表中出现过,则移除这些点
foreach (Point ClosePoint in CloseList)
{
if (SurroundPoints.Contains(ClosePoint))
{
SurroundPoints.Remove(ClosePoint);
}
}
//遍历SurroundPoints中的点
foreach (Point Son in SurroundPoints)
{
//若该点在Open表中出现过,则检查这条路径是否更优,
//也就是说经由当前方格(我们选中的方格) 到达那个方格是否具有更小的 G 值。
if (OpenList.Contains(Son))
{
float newPathG = CalcG(Son, MinPoint);
//如果 G 值更小,则把那个方格的父亲设为当前方格 ( 我们选中的方格 ) ,
//然后重新计算那个方格的 F 值和 G 值
if (newPathG < Son.G)
{
Son.ParentPoint = MinPoint;
Son.G = newPathG;
Son.F = Son.G + Son.H;
}
//如果没有,不做任何操作。
}
else
{
//若该点没有在Open表中出现过,则直接计算F值存入点内,且将该点的父亲设置为minPoint
CalcF(Son, EndPoint);
Son.ParentPoint = MinPoint;
OpenList.Add(Son);
}
}
//若已经到达终点,则退出循环
if (OpenList.IndexOf(EndPoint) > -1)
{
break;
}
}
//返回寻路结果
return GetPathWay(StartPoint, EndPoint);
}
A*算法的优化方法:
1.动态修改权值的方法。在之前的A算法中我们说F=G+H,实际上在H的位置还有一个权重系数W,也就是F=G+WH (W>=1)。 W是可以影响评估值的系数。在搜索过程中,我们可以通过动态修改W来影响搜索过程 H 对A星算法的影响。可以推理出,W 越大,越趋近于Best-First算法,而 W 相对越小,则相对于趋近于Dijkstra算法。
2.分级寻径:把搜索过程拆分开了,如查找空间A中的p1点到空间B中的p2点最短路径,那么可以分为两部分,先查找p1点到空间B的路径,再搜索到p2的路径,整个过程分为了两步,甚至是将计算一次的消耗,拆分成了两次,计算压力也变小了
到这里,游戏中常见的算法就算是介绍完了(其实还有B*,WayPoint,NavMesh之类的算法,我之后再继续补充),下边开始上代码
完整代码

如图所示,其中黑色的是墙壁,绿色的为寻路的物体(这里命名为Enemy),红色的为寻路的终点。
地图由80x45的正方形格子组成,统一由MapMamager类动态生成,可添加障碍物。

Scene视图,其中以算法名字+Enemy的物体都是绿色的方块,是用来寻路的物体。

类视图,其中Point为地图格子的数据结构。
先从基础的类开始:
Point类,这个类是每一个地图格子都保存的数据结构,里边主要用于存储权值,父亲节点,对应的游戏格子GameObject等数据。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Point
{
public Point ParentPoint {
get; set; }//父节点
public GameObject GameObject {
get; set; }//节点的游戏物体
public Renderer PointRenderer {
get; set; }
//F,G,H值
public float F {
get; set; }
public float G {
get; set; }
public float H {
get; set; }
public Vector2 Position {
get; set; }//当前节点所处于的位置
public int PosX {
get; set; }
public int PosY {
get; set; }
public bool IsObstacle {
get; set; }//是否是障碍物
/// <summary>
/// 构造函数
/// </summary>
/// <param name="X">该节点的X坐标</param>
/// <param name="Y">该节点的Y坐标</param>
public Point(int X, int Y)
{
PosX = X;
PosY = Y;
Position = new Vector2(PosX, PosY);
ParentPoint = null;
GameObject = GameObject.Find(X + "," + Y);//根据坐标绑定到场景中的游戏物体
PointRenderer = GameObject.GetComponent<Renderer>();
}
}
MapManager类,主要负责管理整个地图的生成以及设置障碍物。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapManager : MonoBehaviour
{
//生成地图
public GameObject MapPoint;
public const int XLength=80;
public const int YLength=45;
public static Point[,] Grid = new Point[XLength, YLength];
public static void InitPoint()
{
for (int i = 0; i < XLength; i++)
{
for (int j = 0; j < YLength; j++)
{
</

本文详细介绍了2D游戏中的四种常见寻路算法:深度优先搜索(DFS)、广度优先搜索(BFS)、Dijkstra算法和A*算法,并提供了Unity环境下的完整源代码。每种算法的特点、优缺点以及适用场景都有所阐述,其中A*算法综合了Dijkstra和Best-First的优点,能够在寻找最优路径的同时保持较快的搜索速度。
最低0.47元/天 解锁文章
667





