宽度优先搜索(BFS)在Unity中实现
引用
什么是宽度优先搜索(BFS)
从根节点开始,沿着树型的宽度(也就是这一行),遍历树下每一个节点。
从图中可以看到,如果我们所需要的节点是8,那么BFS的搜索顺序从1开始遍历,接着遍历234,之后开始遍历567,最后遍历8。
最后得到的最短路径就是1-4-7-8。
BFS会系统的展开,并判断整棵树中所有的节点,直到我们找到了我们想要的目标点为止。
宽度优先搜索(BFS)在网格中的抽象
一般选用BFS进行搜索的游戏类型一般为战旗或者塔防类的小游戏(也就是地图为离散型形式的地图),一般都会以网格的形式的形式呈现,如下图:
在这中 离散型的地图中 ,要想抽象成树类型的形式,我们需要给它订一套规范,以上右下左的形式进行移动。
比如说,我们现在起点是(0,0),终点是(2,2),按照我们这套规范该如何遍历,从(0,0)开始遍历(1,0)和(0,1),然后以(2,0),(1,1)和(0,2),等等以此类推,知道找到你所需要的点。
抽象成树就是这样,如下图(每个节点的子节点就是它的上右下左方向):
BFS和NavMesh的比较
- BFS
- 适合 离散式的网格布局 的地图游戏;人物的行走都是纵向分明的。
- 查找的速度不如NavMesh
- 参数可自行调整
- 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有一个致命的缺点就是无法像迪杰斯特拉那样实时获取最短路径。
最后我们看一下,把原本的地图扩大四倍并制作简易的障碍,所得的结果: