Unity学习笔记--快速入门实现A*算法寻路

一、要实现的功能:

  1. 在场景中自动生成cube阵列,并随机20%的生成为“障碍”红色cube;
  2. 建立节点类;
  3. 建立节点管理类(实现A*的主要逻辑)
  4. 进行操作,可以用鼠标点击选择起始点与结束点。
    最终实现以下效果:
    在这里插入图片描述
    二、A算法原理:
    这篇是csdn上点击量很高的一个,而且里面讲解的很详细,算是通俗易懂:
    A*算法详解(个人认为最透彻的一个)
    结合Unity实现的视频(本文的核心代码也是根据此学习的):
    Unity中实现A星寻路算法
    三、代码实现:
    这里首先介绍核心与难点代码,并在最后附上所有代码,A
    节点与管理器类不需要继承MonoBehaviour。
  5. 创建节点类:
    1.1 创建节点的枚举类型,将其归类为可以走的地方:Walk与不可走的地方Stop:
public enum E_Node_Type
{
    
    
    Walk,//可以走的地方
    Stop,//不能走的地方
}

1.2 每个节点都应包含的信息:x、y坐标,f=g+h评价函数,类型(Walk/Stop),节点类中建立构造函数:

 public AStarNode(int x,int y,E_Node_Type type)
    {
    
    
        this.x = x;
        this.y = y;
        this.type = type;
    }
  1. A星管理器:
    2.1 A*管理器命名为AStarManager,首先应建立类的单例:
//定义私有变量用来存储类的实例对象
    private static AStarManager instance;
    //定义公有方法提供一个全局访问点
    public static AStarManager Instance
    {
    
    
        get  //类的实例是只读的,所以只有get没有set
        {
    
    
            if (instance==null)
            {
    
    
                instance = new AStarManager();
            }
            return instance;
        }
    }

上面的单例是为了保证管理类只有一个实例,且为只读的。
2.2 建立一个方法,输入为开始、结束节点,返回的是寻找到的节点列表,方法的架构如下:
//1. 首先判断传入的两个点是否合法(假设传入的点就是整数) 是不是阻挡
//2. 如果不合法,应直接返回null,意味着不能寻路
//3. 应当得到起点和终点对应的节点
//4. 把开始点放入关闭列表中
//5. 从起点开始找周围的点并放入开启列表中
//6. 左上-上-右上-左-右-左下-下-右下
//7. 判断这些点是否是边界,是否是阻挡,是否已经在开启或者关闭别表中,如果都不是,则放进开启列表中;
//8. 选出开启列表中寻路消耗最小的点
//9. 放入关闭列表中,然后从开启列表中删除
//10. 如果这个点已经是终点了,那么得到最终的结果返回出去
//11. 如果这个点不是终点,那么继续寻路。
2.3 节点的搜寻状态:
判断某一个节点是否合法,然后再判断是否已经存在与当前的开启列表与关闭列表中,存储这个节点的父节点,并计算这个节点的代价函数:f=g+h;
其中h用的是欧几里得计算方法:忽略障碍时当前点距离终点的水平+垂直距离:

private void FindNearlyNodeToOPenList(int x, int y,float g,AStarNode father,AStarNode end)
    {
    
    
        if (x<0||x>=mapW||y<0||y>=mapH)
            return;
       
        AStarNode node = nodes[x, y];
        if (node==null||
            node.type==E_Node_Type.Stop||
            closeList.Contains(node)||
            openList.Contains(node))
            return;

        //计算f值
        node.father = father;
        node.g = father.g + g;
        node.h = Mathf.Abs(end.x - node.x) + Mathf.Abs(end.y - node.y);
        node.f = node.g + node.h;
        openList.Add(node);
       
    }

记得最后要把这个点存入开启列表中;
2.4 使用list.sort( )进行降序排列:

    private int SortOpenList(AStarNode a, AStarNode b)
    {
    
    
        if (a.f>b.f)
           return 1;
        else
           return -1;
     }
openList.Sort(SortOpenList);

重新排列后的OpenList[0]就是我们代价最先的点,作为下一个start再次循环;

  1. TestAstar的作用:
    3.1 定义网格(地图)的长宽属性;
    3.2 在场景中生成Cube网格,给每个网格命名,并存储到节点中;
    3.3 获取鼠标事件,选中起点选中终点,改变颜色;
    三、附上完整代码:

在这里插入图片描述
在这里插入图片描述
使用中记得将最后的TestAstar赋给我们的摄像机(也可其他物体),要是看的不全,摄像机调整下位置即可;

  1. AStarNode类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 定义枚举类型对节点进行划分:能走/不能走
/// </summary>
public enum E_Node_Type
{
    
    
    Walk,//可以走的地方
    Stop,//不能走的地方
}
/// <summary>
/// A星格子类
/// </summary>
public class AStarNode 
{
    
     
    //节点的x、y坐标
    public int x;  
    public int y;

    //每个节点都有评价函数f = g + h;
    public float f;
    public float g; //已经消耗的代价
    public float h;  //评估此点到终点的代价

    //每个节点都有自己的父节点
    public AStarNode father; 
    public E_Node_Type type;

    //构造函数
    /// <summary>
    /// 传入坐标与节点类型
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="type"></param>
    public AStarNode(int x,int y,E_Node_Type type)
    {
    
    
        this.x = x;
        this.y = y;
        this.type = type;
    }

 }

  1. AStarManager类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarManager
{
    
    
    //定义类的单例:

    //定义私有变量用来存储类的实例对象
    private static AStarManager instance;
    //定义公有方法提供一个全局访问点
    public static AStarManager Instance
    {
    
    
        get  //类的实例是只读的,所以只有get没有set
        {
    
    
            if (instance==null)
            {
    
    
                instance = new AStarManager();
            }
            return instance;
        }
    }
    //地图的宽、高尺寸:
    public int mapW;
    public int mapH;
    //二维数组作为存储地图相关所有节点的对象容器
    public AStarNode[,] nodes;
    //开启列表
    private List<AStarNode> openList=new List<AStarNode>();
    //关闭列表
    private List<AStarNode> closeList=new List<AStarNode>();
    public void InitMapInfo(int w, int h)
    {
    
    
        //声明容器可以装多少个节点
        nodes = new AStarNode[w, h];
        //根据宽高,创建节点
        //声明节点,装进去
       this.mapW = w;
       this.mapH = h;
        for (int i = 0; i < w; i++)
        {
    
    
            for (int j = 0; j < h; j++)
            {
    
    
                AStarNode node = new AStarNode(i, j, Random.Range(0, 100) <25 ? E_Node_Type.Stop : E_Node_Type.Walk);
                nodes[i, j] = node;
            }
        }
    }
    public List<AStarNode> FindPath(Vector2 startPos,Vector2 endPos)
    {
    
    
        //1. 首先判断传入的两个点是否合法(假设传入的点就是整数) 是不是阻挡
        //2. 如果不合法,应直接返回null,意味着不能寻路
        //3. 应当得到起点和终点对应的节点
        //4. 把开始点放入关闭列表中
        //5. 从起点开始找周围的点并放入开启列表中
        //6. 左上-上-右上-左-右-左下-下-右下
        //7. 判断这些点是否是边界,是否是阻挡,是否已经在开启或者关闭别表中,如果都不是,则放进开启列表中;
        //8. 选出开启列表中寻路消耗最小的点
        //9. 放入关闭列表中,然后从开启列表中删除
        //10. 如果这个点已经是终点了,那么得到最终的结果返回出去
        //11. 如果这个点不是终点,那么继续寻路。
       
             //首先判断起始点是否超出边界
            if (startPos.x < 0 || startPos.x >= mapW ||
                startPos.y < 0 || startPos.y >= mapH ||
                endPos.x < 0 || endPos.x >= mapW ||
                endPos.y < 0 || endPos.y >= mapH)
            {
    
    
                Debug.Log("开始或结束点在地图外");
                return null;
            }
            AStarNode start = nodes[(int)startPos.x, (int)startPos.y];
            AStarNode end = nodes[(int)endPos.x, (int)endPos.y];

            if (start.type == E_Node_Type.Stop || end.type == E_Node_Type.Stop)
            {
    
    
                Debug.Log("开始或结束点为阻挡");
                return null;
            }
            closeList.Clear();
            openList.Clear();

            start.father = null;
            start.f = 0;
            start.g = 0;
            start.h = 0;
            closeList.Add(start);
        while (true)
        {
    
    
            FindNearlyNodeToOPenList(start.x - 1, start.y - 1, 1.4f, start, end);
            FindNearlyNodeToOPenList(start.x, start.y - 1, 1f, start, end);
            FindNearlyNodeToOPenList(start.x + 1, start.y - 1, 1.4f, start, end);
            FindNearlyNodeToOPenList(start.x - 1, start.y, 1f, start, end);
            FindNearlyNodeToOPenList(start.x + 1, start.y, 1f, start, end);
            FindNearlyNodeToOPenList(start.x - 1, start.y + 1, 1.4f, start, end);
            FindNearlyNodeToOPenList(start.x, start.y + 1, 1f, start, end);
            FindNearlyNodeToOPenList(start.x + 1, start.y + 1, 1.4f, start, end);

            if (openList.Count == 0)
            {
    
    
                Debug.Log("死路");
                return null;
            }
            openList.Sort(SortOpenList);

            closeList.Add(openList[0]);
            //找到的这个点又变成新的起点
            start = openList[0];
            openList.RemoveAt(0);
            if (start == end)
            {
    
    
                List<AStarNode> path = new List<AStarNode>();
                path.Add(end);
                while (end.father != null)
                {
    
    
                    path.Add(end.father);
                    end = end.father;
                }
                path.Reverse();//顺序反转
                return path;
            }
        }
    }
    private int SortOpenList(AStarNode a, AStarNode b)
    {
    
    
        if (a.f>b.f)
           return 1;
        else
           return -1;
     }
    /// <summary>
    /// 把临近的点放入开启列表中
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    private void FindNearlyNodeToOPenList(int x, int y,float g,AStarNode father,AStarNode end)
    {
    
    
        if (x<0||x>=mapW||y<0||y>=mapH)
            return;
       
        AStarNode node = nodes[x, y];
        if (node==null||
            node.type==E_Node_Type.Stop||
            closeList.Contains(node)||
            openList.Contains(node))
            return;

        //计算f值
        node.father = father;
        node.g = father.g + g;
        node.h = Mathf.Abs(end.x - node.x) + Mathf.Abs(end.y - node.y);
        node.f = node.g + node.h;
        openList.Add(node);
       
    }

}

  1. TestAStar类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestAStar : MonoBehaviour
{
    
    
    public int beginX=3;
    public int beginY=5;
    //偏移位置
    public int offsetX=2;
    public int offsetY=-2;
    //地图格子的宽与高
    public int mapW=5;
    public int mapH=5;

    //开始点,给他一个为负的坐标点
    private Vector2 beginPos = Vector2.right * -1;

    List<AStarNode> list;

    private Dictionary<string, GameObject> cubes = new Dictionary<string, GameObject>();

    // Start is called before the first frame update
    void Start()
    {
    
    
        AStarManager.Instance.InitMapInfo(mapW, mapH);
        for (int i = 0; i < mapW; ++i)
        {
    
    
            for (int j = 0; j < mapH; ++j)
            {
    
    
                GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                obj.transform.position = new Vector3(beginX + i * offsetX, beginY + j * offsetY, 0);
                obj.name = i + "_" + j;//起个名字

                cubes.Add(obj.name,obj);

                AStarNode node = AStarManager.Instance.nodes[i, j];
                if (node.type==E_Node_Type.Stop)
                {
    
    
                    obj.GetComponent<MeshRenderer>().material.color = Color.red;
                }
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
    
    
        if (Input.GetMouseButtonDown(0))
        {
    
    
            RaycastHit info;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray,out info,1000))
            {
    
    
                if (beginPos==Vector2.right*-1)
                {
    
    

                    //清理上一次路径
                    if (list != null)
                    {
    
    
                        for (int i = 0; i < list.Count; ++i)
                        {
    
    
                            cubes[list[i].x + "_" + list[i].y].GetComponent<MeshRenderer>().material.color = Color.white;
                        }
                    }

                    string[] strs = info.collider.gameObject.name.Split('_');
                    //得到行列位置,就是开始点的位置
                    beginPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
                    //将起点改为黄色
                    info.collider.gameObject.GetComponent<MeshRenderer>().material.color = Color.yellow;

                }
                else
                {
    
    
                    //得到终点
                  string[] strs = info.collider.gameObject.name.Split('_');
                  Vector2 endPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
                    //寻路
                     list = AStarManager.Instance.FindPath(beginPos,endPos);
                    if (list!=null)
                    {
    
    
                        for (int i = 0; i < list.Count; ++i)
                        {
    
    
                            cubes[list[i].x + "_" + list[i].y].GetComponent<MeshRenderer>().material.color = Color.green;
                        }
                    }
                    beginPos = Vector2.right * -1;
                }
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_42434073/article/details/113385266