Unity 中的简单A*寻路 (AStar寻路)实现

本文实现的A*算法,未经过大量的优化,后续文章会进一步实现优化

后篇:A*优化讨论

寻路代码实现

结点类:

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

public enum E_Node_Type
{
    /// <summary>
    /// 可行走
    /// </summary>
    Normal,
    /// <summary>
    /// 障碍
    /// </summary>
    Obstacles
}


public class AStarNode 
{
    /// <summary>
    /// x坐标
    /// </summary>
    public int x;
    /// <summary>
    /// y坐标
    /// </summary>
    public int y;

    /// <summary>
    /// 寻路消耗
    /// </summary>
    public float f;
    /// <summary>
    /// 距离起点距离
    /// </summary>
    public float g;
    /// <summary>
    /// 距离终点距离
    /// </summary>
    public float h;

    /// <summary>
    /// 父结点
    /// </summary>
    public AStarNode father;

    /// <summary>
    /// 结点类型
    /// </summary>
    public E_Node_Type type;

    public AStarNode(int xPos, int yPos, E_Node_Type t)
    {
        this.x = xPos;
        this.y = yPos;
        this.type = t;
    }

}

结点管理类:

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

public class AStarManger : BaseManger<AStarManger>
{
    public int mapW;
    public int mapH;

    private AStarNode startNode;
    private AStarNode endNode;

    //private Vector2 endVec = Vector2.right * -1;

    public AStarNode[,] nodes;

    List<AStarNode> openList = new List<AStarNode>();
    List<AStarNode> closeList = new List<AStarNode>();

    /// <summary>
    /// 初始化
    /// </summary>
    public void InitAStarManger(int mapW, int mapH)
    {
        if(mapH < 0 || mapW < 0)
        {
            Debug.LogError("地图尺寸存在问题");
            return;
        }
        this.mapW = mapW;
        this.mapH = mapH;
        
        nodes = new AStarNode[mapW, mapH];

        for(int i = 0; i < this.mapW; i++)
        {
            for(int j = 0; j < this.mapH; j++)
            {
                AStarNode an = new AStarNode(i, j, Random.Range(0, 100) < 20 ? E_Node_Type.Obstacles : E_Node_Type.Normal);
                nodes[i, j] = an;
            }
        }

    }

    /// <summary>
    /// 开始寻径
    /// </summary>
    /// <returns></returns>
    public List<AStarNode> FindPath(Vector2 startPos, Vector2 endPos)
    {
        if(startPos.x < 0 || startPos.y < 0 || endPos.x < 0 || endPos.y < 0 
            || startPos.x >= this.mapW || startPos.y >= this.mapH 
            || endPos.x >= this.mapW || endPos.y >= this.mapH)
        {
            Debug.LogError("坐标超出范围");
            return null;
        }

        startNode = nodes[(int)startPos.x, (int)startPos.y];
        endNode = nodes[(int)endPos.x, (int)endPos.y];

        if(startNode.type is E_Node_Type.Obstacles || endNode.type is E_Node_Type.Obstacles)
        {
            Debug.LogError("起始结点或目标结点为障碍");
            return null;
        }

        startNode.father = null;
        startNode.f = 0;
        startNode.g = 0;
        startNode.h = 0;

        openList.Clear();
        closeList.Clear();

        closeList.Add(startNode);

        while(true)
        {
            //找附近点,并将符合条件的添加进openList
            AddNearlyNode2OpenList(startNode.x - 1, startNode.y -1, 1.4f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x, startNode.y - 1, 1f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x + 1, startNode.y - 1, 1.4f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x - 1, startNode.y, 1f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x + 1, startNode.y, 1f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x - 1, startNode.y + 1, 1.4f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x, startNode.y + 1, 1f, startNode, endPos);
            AddNearlyNode2OpenList(startNode.x + 1, startNode.y + 1, 1.4f, startNode, endPos);

            if(openList.Count == 0)
            {
                Debug.LogWarning("终点不可达");
                return null;
            }

            //排序
            openList.Sort(SortRuler);
            //选择新的父节点
            startNode = openList[0];
            //添加到closeList
            closeList.Add(startNode);
            openList.RemoveAt(0);
            //判断是否到达终点
            if(startNode == endNode)
            {
                List<AStarNode> path = new List<AStarNode>();
                path.Add(endNode);
                while(endNode.father != null)
                {
                    path.Add(endNode.father);
                    endNode = endNode.father;
                }
                path.Reverse();
                return path;
            }
        }
    }

    private int SortRuler(AStarNode a, AStarNode b)
    {
        if (a.f >= b.f) return 1;
        else return -1;
    }

    /// <summary>
    /// 将父结点周围符合条件结点,添加进openList
    /// </summary>
    private void AddNearlyNode2OpenList(int x, int y, float g, AStarNode father, Vector2 endPos)
    {
        //判断传入坐标是否超范围
        if(x < 0 || y < 0 || x >= this.mapW || y >= this.mapH)
        {
            return;
        }

        AStarNode an = nodes[x, y];

        //如果该结点是障碍结点 或 ol,cl里有,忽略
        //如果结点为null,或是上下左右都有障碍物,忽略
        if(an.type is E_Node_Type.Obstacles
            || an is null || closeList.Contains(an)
            || nodes[x, father.y].type is E_Node_Type.Obstacles 
            || nodes[father.x, y].type is E_Node_Type.Obstacles)
        {
            return;
        }

        //单独判断是否在ol里,优化。如果结点已经在ol里,并且下一个最小f值的点也可以算到该点,那么该点的父节点不会变为这个最小值点,导致不是最短路径
        if(openList.Contains(an))
        {
            float gThis = father.g + g;
            if(gThis < an.g)
            {
                an.g = gThis;
                an.f = an.g + an.h;
                an.father = father;
            }
            return;
        }

        //父结点距离起点距离,加上自己距离父结点距离
        an.g = father.g + g;
        an.h = Mathf.Abs(endPos.x - x) + Mathf.Abs(endPos.y - y);
        //计算曼哈顿距离
        an.f = an.g + an.h;

        //将结点加入openList
        openList.Add(an);

        //设置父结点
        an.father = father;
    }
}

单例模板:

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

public class BaseManger<T> where T : new() 
{
    private static T instance;

    public static T Getinstance()
    {
        if(instance is null)
        {
            instance = new T();
        }
        return instance;
    }
}

Unity中界面构建

测试脚本:

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

public class Test : MonoBehaviour
{
    public float BeginX;
    public float BeginY;

    public float offsetX;
    public float offsetY;

    public int mapW;
    public int mapH;

    private Vector2 beginPos = Vector2.right * -1;
    private Dictionary<string, GameObject> cubes = new Dictionary<string, GameObject>();
    List<AStarNode> list = new List<AStarNode>();

    private GameObject tmpStart;
    private GameObject tmpEnd;
    // Start is called before the first frame update
    void Start()
    {
        Init();
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit info;
            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]));

                    if (tmpEnd) tmpEnd.GetComponent<MeshRenderer>().material.color = Color.white;
                    if (tmpStart) tmpStart.GetComponent<MeshRenderer>().material.color = Color.white;

                    tmpStart = info.collider.gameObject;
                    tmpStart.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]));
                   
                    tmpEnd = info.collider.gameObject;
                    tmpEnd.GetComponent<MeshRenderer>().material.color = Color.blue;
                    list = AStarManger.Getinstance().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;
                }
            }
        }
    }

    public void Init()
    {
        AStarManger.Getinstance().InitAStarManger(mapW, mapH);
        tmpEnd = null; 
        tmpStart = null;
        list = null;

        if(cubes.Count != 0)
        {            
            foreach (var obj in cubes)
            {
                Destroy(obj.Value.gameObject);
            }
            cubes.Clear();
        }
        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);
                obj.transform.name = i + "_" + j;
                
                cubes.Add(obj.name, obj);

                AStarNode an = AStarManger.Getinstance().nodes[i, j];
                if (an.type == E_Node_Type.Obstacles)
                {
                    MeshRenderer mr = obj.GetComponent<MeshRenderer>();
                    mr.material.color = Color.red;
                }
            }
        }
    }
}

新建一个场景,将测试脚本挂载在任意物体上

新建一个画布,并添加一个按钮。其它ui元素可随意设定

将按钮关联Init方法

实现效果

正常终点可达:

终点不可达情况:

后续优化文章: 

 进一步优化


本文仅为学习阶段的一个总结,按需浏览。存在一定不足的情况

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我会尝试回答你的问题。 首先,要使用Unity脚本实现A*算法,需要在Unity创建一个脚本,并将其添加到场景的游戏对象上。 以下是实现A*算法的Unity脚本示例代码: ```csharp using UnityEngine; using System.Collections; using System.Collections.Generic; public class AStarPathfinding : MonoBehaviour { public Transform seeker, target; //起点和终点 Grid grid; //所需的网格 void Awake() { grid = GetComponent<Grid>(); } void Update() { FindPath(seeker.position, target.position); } void FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = grid.NodeFromWorldPoint(startPos); Node targetNode = grid.NodeFromWorldPoint(targetPos); List<Node> openSet = new List<Node>(); HashSet<Node> closedSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { Node currentNode = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || (openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { RetracePath(startNode, targetNode); return; } foreach (Node neighbour in grid.GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) { continue; } int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour); if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost = newMovementCostToNeighbour; neighbour.hCost = GetDistance(neighbour, targetNode); neighbour.parent = currentNode; if (!openSet.Contains(neighbour)) { openSet.Add(neighbour); } } } } } void RetracePath(Node startNode, Node endNode) { List<Node> path = new List<Node>(); Node currentNode = endNode; while (currentNode != startNode) { path.Add(currentNode); currentNode = currentNode.parent; } path.Reverse(); grid.path = path; } int GetDistance(Node nodeA, Node nodeB) { int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX); int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY); if (dstX > dstY) { return 14 * dstY + 10 * (dstX - dstY); } return 14 * dstX + 10 * (dstY - dstX); } } ``` 该脚本的A*算法会在每次Update()函数调用时找从起点到终点的最短径,并将其保存在网格的实现A*算法需要一个网格,该网格由一系列节点组成。每个节点包含了该节点在网格的位置、该节点到起点的距离(gCost)、

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值