游戏开发重要算法之A*算法

一、算法用处

学习算法最重要的就是要用它。A* 算法在游戏中的应用常用于寻路计算。这里借用百度百科的一句话。

A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。

说白了,就是一种搜索算法。

二、重要名词

在学习A* 算法之前,我们要对这个算法的很多重要名词做一个解释。要学习这个算法,这些名词是必须要理解的。

1.结点
为了方便理解,我将称以下所有的结点为格子。这么称呼会更形象,当然它不一定是格子,也可能是一个坐标,一个方格,一个六边形格子,甚至其他的东西。这也是A*算法的一个基本单位。

2.列表
开启列表(OpenList):保存待检查的格子的列表。
关闭列表(CloseList):保存检查完的格子的列表。

3.F=G+H
这是整个算法中的核心公式,可以说是整个算法的灵魂。对于整个地图来说,每一个格子,都在不同时候有不同的F、G、H。
专业术语版:
G:在状态空间中从初始状态到状态n的实际代价。
H:从状态n到目标状态的最佳路径的估计代价。
F:从初始状态经由状态n到目标状态的代价估计。
简单理解版:
G:起始格子到该格子走过的距离
H:该格子到目标格子估计的距离
F:G+H
查找了很多资料或者教学之后,多数的A* 算法都是这么算的:规定斜着一格距离为14,直着距离为10。首先原因是因为计算机处理浮点数效率远比整数低。因此首先这个数值一定要尽可能取整。如果设置两个格子的距离为1,那斜向根据勾股定理会取到1.414,因此多数人都把这个值乘以10。取10和14。

4.父节点
就是当前格子是从哪个格子走过来的。毕竟寻路是要返回路径的,而不是单纯判断能不能去。

三、算法思想

其实A*算法的思想并不难。只不过我当时学习的时候被那一大堆专业术语搞得糊涂了,才让我好久都无法理解。

1.确定起点和终点(当然提前要检查这两个点是不是可到达的)
2.把起点添加到开启列表
3.把起点周围所有可到达的点以父节点为起点加入开启列表
4.把起点放入关闭列表
5.计算每一个开启列表里结点的F、G、H
6.找寻开启列表里F最低的一个格子。把当前格子设为这个格子。
7.将当前格子周围所有可到达的格子加入开启列表
8.如果遇到已经加入过的格子,需计算从当前格子到这个加入过的格子的新G。如果G比之前要低,就把这个加入过的格子的父节点设为当前格子。然后赋新的F、G、H。
9.如果是没加入过的,直接把父节点设为当前格子
10.把当前格子放到关闭列表。然后再开启列表里寻找F最低的格子
7-10循环往复
最后一直循环之后会发现某个格子的周围格子正好是终点。这时将终点的父节点设置为这个格子,然后一直寻找父节点,就返回了一条寻路路径了。

这里引用一个大佬的教程。因为这里面有一个图解。会让人更加直观的了解这个算法(喜欢看图理解的可以看看!)unity A*算法简单实现

不必担心什么计算量特别大或者什么这样真的会找到终点之类的问题吗。我第一次学的时候也是这么想的,最后发现真的可以。我之前还担心他的计算会影响游戏运行效率,后来试着做一个6* 6终点的遍历A* 寻路,发现也是没有卡顿的感觉就计算完了。是我太低估了CPU。

四、实现过程

我当时写这段代码时参考了一个大佬的A*算法教程,但是我找不到这篇教程了。所以原大佬如果看到这篇教程感觉这个代码结构很像你的,别纠结,可能就是你的(捂脸)。当然我在这里吸收精华之后有修改了一些自己的理解。因此来学习的也不必担心我并不是讲一个不属于自己的代码。

首先定义了一个Grid类。这个类是每一个结点的类

public class Grid
{
    public Grid Parent; //父节点
    public int F;
    public int G;
    public int H;

    public int X; //位置X
    public int Y; //位置Y
    public bool CantWalk; //是否能通行

    public Grid(int x, int y) //构造函数
    {
        X = x;
        Y = y;
        CantWalk = false;
        Parent = null;
    }

    public void ChangeParent(Grid parent, int g) //修改父节点
    {
        Parent = parent;
        G = g;
        F = G + H;//修改G之后需要重新计算
    }
}

接下来是主类的定义部分我只留了一个地图,和一个单例。

    public static AStar Instance; //单例
    public Map map; //地图

然后是算法的核心函数

 public List<Grid> FindPath(Grid start, Grid end)
    {
    
        List<Grid> OpenList = new List<Grid>();       //开放列表
        List<Grid> CloseList = new List<Grid>();      //关闭列表
		//在这里重新new的原因是因为这个算法并不是一次性的。所以多次调用时就需要多次new。
		
        OpenList.Add(start); //首先将起点加入开启列表
        
        while (OpenList.Count > 0) //只要开启列表不空,就循环。如果循环没了,就说明没找到路径。
        {
            Grid grid= FindMinFOfGrid(OpenList); //从开启列表中寻找F最小的格子
            OpenList.Remove(grid); //从开启列表移除F最小的格子
            CloseList.Add(grid); //把这个格子加入到关闭列表
            List<Grid> SurroundGrid = GetSurroundGrid(grid); //查找周围的格子
            GridFilter(SurroundGrid, CloseList, OpenList); //将在关闭列表中的格子过滤掉
			ChangeParentInSurround(SurroundGrid, gird); //修改父节点
			
			//如果查找到了就返回路径
            if (OpenList.IndexOf(end) > -1)
            {
                return Path(start, end);
            }
        }
        return null;
    } 

接下来是其他函数

   public Point FindMinFOfGrid(List<Grid> OpenList)
    {
        int f = int.MaxValue; //默认f为最大值
        Grid temp = null; //定义一个临时节点
        foreach (Grid grid in OpenList)
        {
            if (grid.F < f) //如果f值小于临时f
            {
                temp = grid; //临时节点=这个格子
                f = grid.F; //临时f=这个格子的f
            }
        }
        return temp; //最后返回temp
    }
   public List<Grid> GetSurroundGrid(Grid grid)
    {
        Grid up = null, down = null, left = null, right = null;
        if (grid.Y < map.vertical - 1) //判断是否超界,下同
        {
            up = map.grid[grid.X, grid.Y + 1];
        }
        if (grid.Y > 0)
        {
            down = map.grid[grid.X, grid.Y - 1];
        }
        if (grid.X > 0)
        {
            left = map.grid[grid.X - 1, grid.Y];
        }
        if (grid.X < map.horizontal - 1)
        {
            right = map.grid[grid.X + 1, grid.Y];
        }
        
        List<Grid> list = new List<Grid>(); //临时变量存储所有周围的格子
        if (down != null && down.CantWalk == false)
        {
            list.Add(down);
        }
        if (up != null && up.CantWalk == false)
        {
            list.Add(up);
        }
        if (left != null && left.CantWalk == false)
        {
            list.Add(left);
        }
        if (right != null && right.CantWalk == false)
        {
            list.Add(right);
        }
        return list;
    } 

这里需要注意的是,因为我这段代码用到是时战棋游戏。多数战棋游戏都是没有斜向移动的。因此我只选择了上下左右四个方向。如果你需要8方向的寻路移动的时候,额外再加上另外四个方向即可。

    public void GridFilter(List<Grid> src, List<Grid> CloseList)
    {
        foreach (Grid grid in CloseList)
        {
            if (src.IndexOf(grid) > -1)
            {
                src.Remove(grid);
            }
        }
    }
	public void ChangeParentInSurround(List<Grid> SurroundGrid, Grid gird, List<Grid> OpenList)
	{
		foreach (Grid surroundGrid in SurroundGrid)
        {
             if (OpenList.IndexOf(surroundGrid) > -1) //如果开放列表中存在当前遍历到的格子
             {
                 int nowG = CalcG(surroundGrid, grid); //当前格子的G
                 if (nowG < surroundGrid.G)  //如果新的G小于原来的G,就更新这个格子的G和父节点
                 {
                     surroundGrid.ChangeParent(grid, nowG);
                 }
             }
             else //如果开放列表里面没有
             {
                 surroundGrid.Parent = gird; //设置父节点
                 CalcF(surroundGrid, end); //计算F
                 OpenList.Add(surroundGrid); //添加到开放列表中
             }
        }
	}

至于计算G和F这两个函数,不同的思路有不同的算法。我就不详细说了,也当作一个个人思考。我这里提供一个思路,计算G就是计算两个点之间方向。斜向计算14,纵横计算10,再加上父节点的G值。F的计算可以算出终点和当前节点的距离,再加上G即可。
最后return的Path函数我也不写在这里了。思路就是从终点一路寻找父节点寻回起点,然后把这些节点都记录到一个list里传回去。
最后调用的时候只需要AStar.Instance.FindPath(start,end);即可。返回值就是一个List < Grid >(没有空格这个我不加空格打不出来……)类型的路径。

五、实际应用

我就拿我自己的demo做例子。为了保证小于5M的图片只能压缩质量了……

当然这种游戏其实也不一定非要做A*。这些短距离寻路用一些深度广度优先算法也可以实现。只不过我就是为了A*算法才做的这个游戏23333.

你可能感兴趣的:(unity)