Loading

宽度优先搜索(BFS)在Unity中实现

引用

广度优先搜索算法在Unity网格地图中寻找最短路径

推荐路径算法学习网站

广度优先搜索算法在Unity网格地图中实现最短路径

什么是宽度优先搜索(BFS)

根节点开始,沿着树型的宽度(也就是这一行),遍历树下每一个节点

image

从图中可以看到,如果我们所需要的节点是8,那么BFS的搜索顺序从1开始遍历,接着遍历234,之后开始遍历567,最后遍历8。

最后得到的最短路径就是1-4-7-8。

BFS会系统的展开,并判断整棵树中所有的节点,直到我们找到了我们想要的目标点为止。

宽度优先搜索(BFS)在网格中的抽象

一般选用BFS进行搜索的游戏类型一般为战旗或者塔防类的小游戏(也就是地图为离散型形式的地图),一般都会以网格的形式的形式呈现,如下图:

image

在这中 离散型的地图中 ,要想抽象成树类型的形式,我们需要给它订一套规范,以上右下左的形式进行移动。

比如说,我们现在起点是(0,0),终点是(2,2),按照我们这套规范该如何遍历,从(0,0)开始遍历(1,0)和(0,1),然后以(2,0),(1,1)和(0,2),等等以此类推,知道找到你所需要的点。

抽象成树就是这样,如下图(每个节点的子节点就是它的上右下左方向):

image

BFS和NavMesh的比较

  1. BFS
    • 适合 离散式的网格布局 的地图游戏;人物的行走都是纵向分明的。
    • 查找的速度不如NavMesh
    • 参数可自行调整
  2. NavMseh
    • 自由探索类型的游戏;以两点之间直线最短为基础,以斜线的方式进行行走。
    • 查找速度快
    • 都在NavAgent组件中

代码实现

成果演示:以(0,0)点为初始点,(3,3)点为终点进行最短路径的搜索。所得结果如下:

首先是WayPoint脚本,对格子,也就是地图进行抽象,把我们所需要的属性抽象出来。

using UnityEngine;

public class WayPoint : MonoBehaviour
{
    /// <summary>
    /// 标记已经搜索过的点
    /// </summary>
    public bool isExplored;

    /// <summary>
    /// 父节点
    /// </summary>
    public WayPoint exploredFrom;

    /// <summary>
    /// 转换坐标,把物体在游戏中的坐标转换为text上所写的的坐标
    /// 也就是我们在游戏中自定义的坐标系
    /// </summary>
    /// <returns></returns>
    public Vector2Int GetPosition() {

        return new Vector2Int(
            Mathf.RoundToInt(transform.position.x / 1.5F),
            Mathf.RoundToInt(transform.position.z / 1.5F)
            );
    }
}

接着是SphereMovement脚本,就是控制小球移动的脚本

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

public class SphereMovement : MonoBehaviour
{
    private void Start() {
        Pathfinding pf = FindObjectOfType<Pathfinding>();
        var path = pf.GetPath();
        StartCoroutine(FindWayPoint(path));
    }

    /// <summary>
    /// 遍历路径
    /// </summary>
    /// <returns></returns>
    IEnumerator FindWayPoint(List<WayPoint> pathWayPoints) {
        foreach(var wayPoint in pathWayPoints) {
            transform.position = wayPoint.transform.position + new Vector3(0, 1, 0);
            yield return new WaitForSeconds(0.5F);
        }
    }

}

最后就是核心Pathfinding脚本,节点的查找已经最短路径的录入。

using System.Collections.Generic;
using UnityEngine;

public class Pathfinding : MonoBehaviour
{
    [SerializeField] private GameObject startPoint, endPoint;
    /// <summary>
    /// 通过字典的键找到相应的值;
    /// 在这个实例中也就是通过具体的Vector2Int类型的坐标位置,找到相应的每个游戏中GameObject类型的格子游戏对象。
    /// </summary>
    public Dictionary<Vector2Int, WayPoint> wayPointDict = new Dictionary<Vector2Int, WayPoint>();

    Queue<WayPoint> queue = new Queue<WayPoint>();

    /// <summary>
    /// BFS算法是否在运行
    /// </summary>
    [SerializeField] private bool isRuning = true;

    /// <summary>
    /// 当前正在搜索的节点
    /// </summary>
    private WayPoint searchCenter;

    /// <summary>
    /// 记录最短路径
    /// </summary>
    public List<WayPoint> path = new List<WayPoint>();

    /// <summary>
    /// 通过上右下左四个方向进行索引
    /// </summary>
    private Vector2Int[] directions = {
        Vector2Int.up,
        Vector2Int.right,
        Vector2Int.down,
        Vector2Int.left,
    };

    /// <summary>
    /// 获取最短路径
    /// </summary>
    /// <returns></returns>
    public List<WayPoint> GetPath() {
        startPoint.GetComponent<MeshRenderer>().material.color = Color.blue;
        endPoint.GetComponent<MeshRenderer>().material.color = Color.red;
        LoadAllWayPoints();
        BFS();
        CreatPath();
        return path;
    }

    /// <summary>
    /// 搜索目标点的子节点
    /// 四个方向的子节点
    /// </summary>
    private void ExploreAround() {
        if (!isRuning) return;
        foreach (var direction in directions) {
            var pos = searchCenter.GetPosition() + direction;

            //判断字典中有没有相应的key
            try {
                var neighbor = wayPointDict[pos];

                //枝减,把重复判断的树枝砍去
                if (!neighbor.isExplored) {
                    //四周的相邻子节点变色
                    //neighbor.GetComponent<MeshRenderer>().material.color = Color.green;
                    queue.Enqueue(neighbor);
                    //记录他们的出处,存储seaarchCenter,表示该节点的父节点是哪位
                    neighbor.exploredFrom = searchCenter;
                }
            }
            catch {
                continue;
            }
        }
    }

    /// <summary>
    /// 把游戏对象存入字典中
    /// </summary>
    private void LoadAllWayPoints() {
        //查找游戏中所有添加WayPoint脚本的游戏对象
        var wayPoints = FindObjectsOfType<WayPoint>();
        foreach (var wayPoint in wayPoints) {
            var tempWaypoint = wayPoint.GetPosition();
            //如果字典已经包含了这个键,则跳过
            if (wayPointDict.ContainsKey(tempWaypoint)) {
                continue;
            }
            else {
                wayPointDict.Add(tempWaypoint, wayPoint);
            }
        }
    }

    /// <summary>
    /// 宽度优先搜索
    /// </summary>
    private void BFS() {
        //[初始点]加入到队列中
        // while语句:判断队列是否为空
        //第一个元素也就是初始点移除队列:变量V
        //判断变量V是否是目标点
            //如果是:那么return,中止算法
            //如果不是遍历变量V相邻的子节点,将它的子节点加入到队列中
        //移除队列的,也就是搜索完的节点变量V,标记为已搜索。
        queue.Enqueue(startPoint.GetComponent<WayPoint>());
        while(queue.Count > 0 && isRuning) {
            searchCenter = queue.Dequeue();//对应变量V

            StopIfSearchEnd();

            ExploreAround();
            searchCenter.isExplored = true;//标记为已搜索
        }
    }

    /// <summary>
    /// 判断是否叫停BFS
    /// </summary>
    private void StopIfSearchEnd() {
        //如果找到了终点,则中止算法
        if(searchCenter == endPoint.GetComponent<WayPoint>()) {
            isRuning = false;
        }
    }

    /// <summary>
    /// 创建最短路径
    /// 反过来,以最后的节点为主找它的父节点
    /// </summary>
    private void CreatPath() {
        path.Add(endPoint.GetComponent<WayPoint>());//第一个节点

        WayPoint prePoint = endPoint.GetComponent<WayPoint>().exploredFrom;//父节点

        //接着查找判断
        while(prePoint != startPoint.GetComponent<WayPoint>()) {
            //将搜索路径存入到List中
            //高亮路径点
            prePoint.GetComponent<MeshRenderer>().material.color = Color.yellow;
            path.Add(prePoint);
            prePoint = prePoint.exploredFrom;//赋值父节点的父节点
        }

        //添加初始点
        path.Add(startPoint.GetComponent<WayPoint>());
        //反转集合
        path.Reverse();
    }
}

真实的地图可能往往比较复杂,不会像上述这么简单,而且BFS有一个致命的缺点就是无法像迪杰斯特拉那样实时获取最短路径。
最后我们看一下,把原本的地图扩大四倍并制作简易的障碍,所得的结果:

posted @ 2022-08-28 16:36  数学天才琪露诺  阅读(236)  评论(0编辑  收藏  举报