那些游戏中的寻路算法

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

在游戏中,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视图
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++)
            {
   
   </
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值