目录

前言

一、A*(A-Star)简介

二、效果展示

三、实现步骤

1、定义节点类

2、定义地图 

3、初始化地图 

4、寻路算法的实现

5、点击选取初始点与目标点。 

最后


前言

        仅个人学习的记录,旨在分享我的学习笔记和个人见解。


一、A*(A-Star)简介

        在游戏开发的过程中,不免需要涉及到怪物AI的寻路或让玩家自动导航到目标点位置,当在没有障碍的场景中,直接让怪物/玩家向目标点移动即可,但在有障碍物的场景中(如迷宫,房间,围墙等等),寻路算法就变得尤为重要。

        A*是目前主流的寻路算法之一,它的作用就是寻找开始点与目标点之间,避开障碍的最短路径。它结合了Dijkstra算法的最短路径搜索和贪心算法的启发式搜索,通过综合考虑已经搜索到的路径以及预测的路径长度,选择下一个要探索的节点,以加快搜索速度。
                                               A*计算某点代价的核心思路就是 F=G+H   
                                总代价=初始点到该点的最小代价+该点到目标点的预估代价

二、效果展示

        

三、实现步骤

1、定义节点类

       包含空间信息,代价等等,其中parent为当前节点的父节点,当B点的最小G值是通过A点G值+路径长度得到的时候,A为B的父节点。主要用于计算从开始点出发到该节点的最小代价。在后续回溯路径时,终点不断访问父节点即可找到最佳路径。

public class Point
{
    public int X;
    public int Y;
    public float F;//总代价 F=G+H;
    public float G;//开始点到该点的最小代价
    public float H;//该点到目的地的预计代价(曼哈顿街区)
    public Point parent = null;

    public bool isObstacle = false;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void SetParent(Point parent, float g)
    {
        this.parent = parent;
        G = g;
        F = G + H;
    }
}

2、定义地图 

        定义了3个二维数组,其中
                map 为存储节点的数组,主要用于计算。
                sprites 为存储SpriteRenderer的数组,用于方块显示。
                blockLayout 为方块布局数组,用于在脚本中编辑地图。

public GameObject blockPrefab;   //方块预制体
public Point[,] map;
public SpriteRenderer[,] sprites;//生成的方块的二维数组

// 定义二维数组来表示方块的布局
private int[,] blockLayout = new int[,]
    {
        { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        { 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1},
        { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1},
        { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
        { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    };

方块预制体的结构为 
G、H用于显示初始点到该点的代价与该点到目标点的预估代价。

3、初始化地图 

         通过地图布局来对map与sprites数组赋值。

        需要注意的是unity世界坐标是x正向为右,y正向为上,而在代码中定义的二维数组,x正向为下,y正向为右。如果想要生成与二维数组形态、方向一致的墙壁,需要做一点转换。


即 二维数组的X=世界坐标的-Y; 二维数组的Y=世界坐标的X;
世界坐标的X=二维数组的Y;世界坐标Y=二维数组-X;

所以在生成方块时需要注意生成的坐标点与二维数组下标的转换。

public void InitMap()//初始化地图
    {
        bool isFirst = false;
        height = blockLayout.GetLength(0);
        width = blockLayout.GetLength(1);
        start = null;
        end = null;
        map = new Point[height, width];
        if(sprites == null)
        {
            sprites = new SpriteRenderer[height, width];
            isFirst = true;
        }
        else
        {
            isFirst = false;
        }
        
        for (int x = 0; x < height; x++)
        {
            for (int y = 0; y < width; y++)
            {
                Vector3 position = new Vector3(y, -x, 0);
                if (isFirst)
                {
                    sprites[x, y] = Instantiate(blockPrefab, position, Quaternion.identity).GetComponent<SpriteRenderer>();
                }
                
                sprites[x, y].transform.Find("Canvas/G").GetComponent<Text>().text = "0";
                sprites[x, y].transform.Find("Canvas/H").GetComponent<Text>().text = "0";
                if (blockLayout[x, y] == 1)
                {
                    map[x, y] = new Point(x, y);
                    map[x, y].isObstacle = true;
                    sprites[x, y].color = Color.black;
                }
                else
                {
                    map[x, y] = new Point(x, y);
                    sprites[x, y].color = Color.white;
                }
            }
        }
    }

4、寻路算法的实现

         为了演示寻路的过程所以用了协程。
主要思路:定义2个集合,openList为未确定最小代价点的集合,close为已确定最小代价点的集合。在开始时,添加start点到openList中,当openList中有点时保持循环。将openList中最小的点取出并放入close集合中,获取该点8领域的点并加入openList中,并计算更新周围点的总代价。直到获取到终点时,结束循环并回溯路径。

        具体已在代码中写明注释

 IEnumerator FindPath()
    {
        List<Point> openList = new List<Point>();//未确定最小代价点的集合
        List<Point> closeList = new List<Point>();//已确定最小代价点的集合
        openList.Add(start);
        //还有未确定代价点时 一直循环
        while (openList.Count > 0)
        {
            //选出open集合中代价最小的点
            Point point = GetMinFPoint(openList);

            //该点已经确定最小代价,从open中移出并放入close中。
            openList.Remove(point);
            closeList.Add(point);
            sprites[point.X, point.Y].color = Color.gray;
            
            //获取该点周围的点
            List<Point> SurroundPoints = GetSurroundPoint(point.X, point.Y);

            //剔除周围已经确定最小代价的点
            foreach (Point p in closeList)
            {
                if (SurroundPoints.Contains(p))
                {
                    SurroundPoints.Remove(p);
                }
            }
            //遍历周围的点
            //若已被访问过,则对比G代价,更新初始点到该点的最小代价。
            //若未被访问过,则基于当前点计算周围点的代价,并更新G代价。

            foreach (Point p in SurroundPoints)
            {
                if (openList.Contains(p))
                {
                    float _G = 0;
                    //若不处于斜角,则距离+1,若处于斜角则距离+1.4(根号2)
                    if (point.X==p.X || point.Y == p.Y)
                    {
                        _G = 1f + point.G;
                    }
                    else
                    {
                        _G = 1.4f + point.G;
                    }
                   
                    if (_G < p.G)
                    {
                        p.SetParent(point, _G);
                    }
                }
                else
                {
                    //基于当前点计算周围点的代价
                    SetPoint(p, point);
                    openList.Add(p);
                }

                //在方块上显示G与H
                sprites[p.X, p.Y].transform.Find("Canvas/G").GetComponent<Text>().text = (Mathf.Round(p.G * 10.0f) / 10.0f).ToString();
                sprites[p.X, p.Y].transform.Find("Canvas/H").GetComponent<Text>().text = p.H.ToString();
            }
            //找到终点后,回溯路径
            if (openList.Contains(end))
            {
                StartCoroutine(ShowPath());
                break;
            }
            yield return new WaitForSeconds(0.05f);
        }
    }

 主方法中用到的方法

 /// <summary>
    /// 获取map[x,y]的八邻域点的集合
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <returns></returns>
    public List<Point> GetSurroundPoint(int x, int y)
    {
        List<Point> PointList = new List<Point>();

        int oriX;
        int oriY;
        int[] dx = new int[8] { 0, 1, 1, 1, 0, -1, -1, -1 };
        int[] dy = new int[8] { -1, -1, 0, 1, 1, 1, 0, -1 };
        for (int i = 0; i < 8; i++)
        {
            oriX = x + dx[i];
            oriY = y + dy[i];
            if (oriX > 0 && oriX < height && oriY > 0 && oriY < width && map[oriX, oriY].isObstacle == false)
            {
                PointList.Add(map[oriX, oriY]);
            }
        }
    
        return PointList;
    }

    /// <summary>
    /// 基于parentPoint 计算point的代价。
    /// </summary>
    /// <param name="point"></param>
    /// <param name="parentPoint"></param>
    public void SetPoint(Point point,Point parentPoint)
    {
        point.parent = parentPoint;
        float G = 0;
        float H = Mathf.Abs(end.X - point.X) + Mathf.Abs(end.Y - point.Y);
        if (point.parent != null)
        {
            if (point.parent.X == point.X || point.parent.Y == point.Y)
            {
                G = 1f + point.parent.G;
            }
            else
            {
                G = 1.4f + point.parent.G;
            }
        }
        
        float F = H + G;
        point.H = H;
        point.G = G;
        point.F = F;
    }

    /// <summary>
    /// 获取list中最小总代价的点
    /// </summary>
    /// <param name="list"></param>
    /// <returns></returns>
    public Point GetMinFPoint(List<Point> list)
    {
        float min = float.MaxValue;
        Point point = null;
        foreach (Point p in list)
        {
            if (p.F < min)
            {
                min = p.F;
                point = p;
            }
        }
        return point;
    }

 路径回溯方法

不断访问end点的父节点,直到回到start点。 

    IEnumerator ShowPath()//显示路径
    {
        Point temp = end.parent;
        while (temp != start)
        {
            sprites[temp.X, temp.Y].color = Color.blue;
            temp = temp.parent;
            yield return new WaitForSeconds(0.1f);
        }
    }

5、点击选取初始点与目标点。 

按C重置地图,按D+鼠标左键,选取起点,按D+鼠标右键,设置终点并寻路。

private void Update()
    {
        if (Input.GetKeyDown(KeyCode.C))
        {
            InitMap();
        }
        if (Input.GetKey(KeyCode.D) && Input.GetMouseButtonDown(0))
        {
            Vector2 vector2 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            int MapX = -Mathf.FloorToInt(vector2.y);
            int MapY = Mathf.FloorToInt(vector2.x);
            if(0<MapX && MapX < height && 0< MapY &&MapY<width && !map[MapX, MapY].isObstacle)
            {
                start = map[MapX, MapY];
                sprites[MapX, MapY].color = Color.green;
                Debug.Log("设置起点");
            }
        }
        if (Input.GetKey(KeyCode.D) && Input.GetMouseButtonDown(1))
        {
            Vector2 vector2 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            int MapX = -Mathf.FloorToInt(vector2.y);
            int MapY = Mathf.FloorToInt(vector2.x);
            if (0 < MapX && MapX < height && 0 < MapY && MapY < width && !map[MapX, MapY].isObstacle)
            {
                end = map[MapX, MapY];
                sprites[MapX, MapY].color = Color.red;
                Debug.Log("设置终点");
                if (start != null)
                {
                    StartCoroutine(FindPath());
                }
            }
        }
    }

最后

        文章内容仅为个人学习记录。好记性不如烂笔头,为了能更好的回顾和总结,开始记录与分享自己学到的Unity知识。若文章内容错误,麻烦指点。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐