UnityA星寻路算法获取最短路径

~最后效果

在这里插入图片描述

1. 场景的搭建

在这里插入图片描述

2. 说明

A星寻路公式 F = G + H
F: 寻路消耗(越小说明距离终点越近)
G: 起点距离当前点的代价
H: 当前点距离终点的代价

脚本 说明
Singleton 继承该脚本实现单例
AStarNode 存储每个节点的信息
AStarManager 所有节点的管理器
Test 使用节点管理器实现可视化的寻路流程

3. Singleton脚本

继承该脚本实现单例

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Singleton<T> where T : class
{
    
    
    private static T instance;
    public static T Instance
    {
    
    
        get
        {
    
    
            if(instance == null)
            {
    
    
            	// 通过反射创建实例
                instance = (T)Activator.CreateInstance(typeof(T), true);
            }    

            return instance;
        }
    }
}

4. AStarNode脚本

存储每个节点的信息

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 节点的类型
public enum AStarNodeType
{
    
    
    Walk, // 可以走的
    NotWalk, // 障碍物
    Start, // 起点
    End, // 终点
}

public class AStarNode
{
    
    
    // 节点坐标
    public int x;
    public int y;
    // 寻路消耗
    public float f;
    // 离起点的代价
    public float g;
    // 离终点的代价
    public float h;
    // 父对象
    public AStarNode father;
    // 类型
    public AStarNodeType type;
    // 构造函数
    public AStarNode(int x, int y, AStarNodeType type)
    {
    
    
        this.x = x;
        this.y = y;
        this.type = type;
    }
}

5. AStarManager 脚本

所有节点的管理器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarManager : Singleton<AStarManager>
{
    
    
    private AStarManager() 
    {
    
    
        // 实例化开始列表和关闭列表
        openList = new List<AStarNode>();
        closeList = new List<AStarNode>();
    }

    // 地图宽高
    private int w;
    private int h;
    // 存储所有节点的二维数组
    public AStarNode[,] nodes;
    // 开始列表
    private List<AStarNode> openList;
    // 关闭列表
    private List<AStarNode> closeList;

    // 初始化地图
    public void InitMap(int w, int h)
    {
    
    
        // 获取地图宽高
        this.w = w;
        this.h = h;
        // 实例化数组
        nodes = new AStarNode[w, h];
        // 遍历存储所有节点,并有百分之20几率是障碍物类型的节点
        for (int i = 0; i < w; i++)
        {
    
    
            for(int j = 0; j < h; j++)
            {
    
    
                nodes[i, j] = new AStarNode(i, j, Random.Range(0, 100) < 20 ? AStarNodeType.NotWalk : AStarNodeType.Walk);
            }
        }

        int x = 0;
        int y = 0;
        bool isDone = false; // 判断起点或者终点有没有生成完成;
        // 生成起点,如果没有完成生成
        while (!isDone)
        {
    
    
            // 随机生成x,y;
            x = Random.Range(0, w);
            y = Random.Range(0, h);
            // 如果不是障碍物类型,就将这个节点变为起点
            if (nodes[x, y].type != AStarNodeType.NotWalk)
            {
    
    
                nodes[x, y].type = AStarNodeType.Start;
                // 生成起点完成,结束循环
                isDone = true;
            }
        }

        // 生成终点
        isDone = false;
        while(!isDone)
        {
    
    
            x = Random.Range(0, w);
            y = Random.Range(0, h);
            // 如果不是障碍物也不是起点, 那么这个节点就可以成为终点
            if(nodes[x,y].type != AStarNodeType.NotWalk && nodes[x, y].type != AStarNodeType.Start)
            {
    
    
                nodes[x, y].type = AStarNodeType.End;
                // 生成终点完成,结束循环
                isDone = true;
            }
        }
        
    }

    public List<AStarNode> FindPath(Vector2 startPos, Vector2 endPos)
    {
    
    
        // 判断起点和终点有没有在地图范围内
        if (startPos.x < 0 || startPos.y < 0 || startPos.x >= w || startPos.y >= h ||
            endPos.x < 0 || endPos.y < 0 || endPos.x >= w || endPos.y >= h)
        {
    
    
            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 == AStarNodeType.NotWalk || end.type == AStarNodeType.NotWalk)
        {
    
    
            Debug.Log("起点或终点是障碍物");
            return null;
        }

        // 清空上一轮的开始和关闭列表
        openList.Clear();
        closeList.Clear();

        // 起点的值确保为初始化状态
        start.f = 0;
        start.g = 0;
        start.h = 0;
        start.father = null;

        // 将起点添加到关闭列表
        closeList.Add(start);

        // 循环找附近的8个点,直到死路或者找到终点
        while (true)
        {
    
    
            // 找到起点附近的8个点
            for (int i = -1; i <= 1; i++)
            {
    
    
                for (int j = -1; j <= 1; j++)
                {
    
    
                    float g = 1;
                    // 如果是自己,就跳过
                    if (i == 0 && j == 0) continue;
                    // 如果是斜着的节点, g 就是 1.4
                    if ((i < 0 ? -i : i) == (j < 0 ? -j : j))
                    {
    
    
                        g = 1.4f;
                    }
                    // 找到附近的节点并判断能不能添加到开始列表
                    FindNearPos(start.x + i, start.y + j, g, 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;
        }
    }

    private void FindNearPos(int x, int y, float g, AStarNode father, AStarNode end)
    {
    
    
        // 判断是不是在地图范围内
        if (x < 0 || x >= w || y < 0 || y >= h) return;
        // 获取该节点
        AStarNode node = nodes[x, y];

        // 如果在开始列表中有该节点
        if (openList.Contains(node))
        {
    
    
            // 算出该节点距离起点的距离 g
            float gCrt = father.g + g; // 父对象离起点的距离 + 我离父对象的距离 = 我离起点的距离
            // 如果该节点距离起点的距离小于我离父对象的距离
            if (gCrt < node.g)
            {
    
    
                // 那么该点的父节点要获取这个最小值点
                node.g = gCrt;
                // 算出寻路消耗
                node.f = node.g + node.h;
                // 父对象赋值
                node.father = father;
                return;
            }
            else
            {
    
    
                return;
            }
        }

        // 如果节点
        // 1. 不是为空
        // 2. 不是障碍物
        // 3. 不在开始列表中
        // 4. 不在关闭列表中
        if (node == null ||
            node.type == AStarNodeType.NotWalk ||
            closeList.Contains(node) ||
            openList.Contains(node)) 
            return;

        // 父对象赋值
        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);
    }
}

6. Test脚本

使用节点管理器实现可视化的寻路流程

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    
    
    // 地图宽高
    private int w;
    private int h;
    // 要生成的方块,作为A星格子
    private GameObject cube;
    // 存储方块的字典
    private Dictionary<string, GameObject> dic;

    private void Awake()
    {
    
    
        // 加载方法资源
        cube = Resources.Load<GameObject>("Prefabs/Cube");
        // 实例化字典
        dic = new Dictionary<string, GameObject>();
    }

    private void Start()
    {
    
    
        // 绑定按钮点击事件
        GameObject.Find("Start").GetComponent<Button>().onClick.AddListener(StartFindPath);
        GameObject.Find("ReStart").GetComponent<Button>().onClick.AddListener(ReStart);
        // 获取地图宽高
        w = (int)transform.localScale.x * 10;
        h = (int)transform.localScale.z * 10;
        // 初始化
        Init();
    }

    // 开始寻路
    private void StartFindPath()
    {
    
    
        // 获取起点和终点的坐标
        Vector2 startPos = Vector2.zero;
        Vector2 endPos = Vector2.zero;
        foreach (AStarNode item in AStarManager.Instance.nodes)
        {
    
    
            if(item.type == AStarNodeType.Start)
            {
    
    
                startPos = new Vector2(item.x, item.y);
            }
            if(item.type == AStarNodeType.End)
            {
    
    
                endPos = new Vector2(item.x, item.y);
            }
        }

        // 获取寻路结果
        List<AStarNode> list = AStarManager.Instance.FindPath(startPos, endPos);

        // 寻路结果不等于null 说明找到路了
        if (list != null)
        {
    
    
            // 遍历
            foreach (AStarNode item in list)
            {
    
    
                // 如果是起点或者终点就跳过
                if (item.type == AStarNodeType.Start || item.type == AStarNodeType.End) continue;
                // 将路径变为绿色
                dic[item.x + "_" + item.y].GetComponent<MeshRenderer>().material.color = Color.green;
            }
        }
    }

    private void ReStart()
    {
    
    
        // 清空字典
        dic.Clear();
        // 清空子对象
        for(int i = 0; i < transform.childCount; i++)
        {
    
    
            Destroy(transform.GetChild(i).gameObject);
        }
        // 初始化
        Init(); 
    }

    private void Init()
    {
    
    
        // 初始化地图
        AStarManager.Instance.InitMap(w, h);

        for (int i = 0; i < w; i++)
        {
    
    
            for (int j = 0; j < h; j++)
            {
    
    
                // 生成方块
                GameObject go = Instantiate(cube, transform);
                // 修改方块的位置(铺满整个地板)
                go.transform.localPosition = new Vector3(i - w * 0.5f + 0.5f, 0.5f, j - h * 0.5f + 0.5f);
                // 更改方块的名字,为了获取的方便
                go.name = i + "_" + j;
                // 将方块添加到字典
                dic.Add(go.name, go);
                // 获取节点
                AStarNode node = AStarManager.Instance.nodes[i, j];
                // 判断节点的类型
                switch (node.type)
                {
    
    
                    // 如果是障碍物就变成红色
                    case AStarNodeType.NotWalk:
                        go.GetComponent<MeshRenderer>().material.color = Color.red;
                        break;
                    // 如果是起点就变成黄色
                    case AStarNodeType.Start:
                        go.GetComponent<MeshRenderer>().material.color = Color.yellow;
                        break;
                    // 如果是终点就变成蓝色
                    case AStarNodeType.End:
                        go.GetComponent<MeshRenderer>().material.color = Color.blue;
                        break;
                }
            }
        }
    }

}

7. 结束

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/My_csdnQMDX/article/details/126475491