1、效果展示
2、简要说明
绿色表示起点,红色表示终点,黑色表示墙体。
右侧4个按钮,分别是三种算法和一个重置场景。
鼠标左键点击图中的方块,方块会变成黄色,此时在黄色方块内单击鼠标右键,会显示一个面板,该面板内有三个按钮,分别为设置起点,设置终点,设置/取消墙体。
3、代码
3.1 Map.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Map : MonoBehaviour
{
public GameObject mapParent;
public List<List<Transform>> mapList;
static Map _instance; //单例模式
public static Map Instance
{
get
{
return _instance;
}
}
// Start is called before the first frame update
void Awake()
{
if (Instance == null)
{
_instance = this;
}
mapList = new List<List<Transform>>();
int k = 0;
for (int i = 0; i < 5; i++) //获取Map
{
List<Transform> MapLine = new List<Transform>();
for (int j = 0; j < 11; j++)
{
Transform grid = mapParent.transform.GetChild(k);
k += 1;
MapLine.Add(grid);
}
mapList.Add(MapLine);
}
//Debug.Log("行:" + mapList.Count + " 列:" + mapList[0].Count);
}
public void ChangeColor(Vector2Int pos, Color color) //改变颜色
{
mapList[pos.x][pos.y].gameObject.GetComponent<SpriteRenderer>().color = color;
}
public Vector2Int GetGridPos(GameObject gameObject) //获取方块具体的行列值
{
Vector2Int gridPos = new Vector2Int();
for (int i = 0; i < mapList.Count; i++)
{
//List<T>.IndexOf()方法,该方法会返回元素在列表中的索引,如果元素不在列表中则返回-1。
int index = mapList[i].IndexOf(gameObject.transform);
if (index != -1)
{
//Debug.Log(gameObject.name + "在第" + i + "行" + "第" + index + "列");
gridPos.x = i;
gridPos.y = index;
return gridPos;
}
}
return new Vector2Int(-1, -1);
}
}
3.2 PathFinder.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PathFinder : MonoBehaviour
{
private Vector2Int[] offsets;
public float duration = 0.3f;
public Vector2Int start = new Vector2Int(1, 5);
public Vector2Int end = new Vector2Int(2, 10);
static PathFinder _instance;
public static PathFinder Instance
{
get
{
return _instance;
}
}
private void Awake()
{
if (Instance == null)
{
_instance = this;
}
Map.Instance.mapList[start.x][start.y].gameObject.GetComponent<SpriteRenderer>().color = new Color(0, 1, 0);
Map.Instance.mapList[end.x][end.y].gameObject.GetComponent<SpriteRenderer>().color = new Color(1, 0, 0);
offsets = new Vector2Int[] {
new Vector2Int(1, 0), new Vector2Int(0, -1), new Vector2Int(-1, 0), new Vector2Int(0, 1) };
}
public void BreadFirst(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
StartCoroutine(IBreadFirst(map, start, end));
}
/// <summary>
/// 广度优先搜索
/// </summary>
/// <param name="map">地图</param>
/// <param name="start">开始位置</param>
/// <param name="end">目标位置</param>
/// <returns></returns>
public IEnumerator IBreadFirst(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
Queue<Vector2Int> queue = new Queue<Vector2Int>();
Dictionary<Vector2Int, Vector2Int> camefrom = new Dictionary<Vector2Int, Vector2Int>(); //定义一个字典来存储每个节点的前一个节点。
queue.Enqueue(start);
camefrom[start] = new Vector2Int(-1, -1);
bool hasRoute = false;
while (queue.Count > 0)
{
Vector2Int current = queue.Dequeue(); //从队列中移除并返回队列的开头元素
yield return new WaitForSeconds(duration);
if (current == start)
Map.Instance.ChangeColor(current, new Color(0, 1, 0));
else if (current == end)
Map.Instance.ChangeColor(current, new Color(1, 0, 0));
else
Map.Instance.ChangeColor(current, new Color(0, 0, 1));
if (current == end) //找到目标位置就退出循环
{
hasRoute = true;
break;
}
//将当前格子的四个临边格子加入队列
//这段代码创建了一个 Vector2Int 类型的数组,数组中有四个 Vector2Int 元素。
foreach (Vector2Int offset in offsets)
{
Vector2Int newPos = current + offset;
//超出边界
if (newPos.x < 0 || newPos.y < 0 || newPos.x >= map.Count || newPos.y >= map[0].Count)
{
continue;
}
//已经走过
if (camefrom.ContainsKey(newPos))
{
continue;
}
//墙
if (map[newPos.x][newPos.y].tag == "Wall")
{
continue;
}
queue.Enqueue(newPos); //加入队列
camefrom[newPos] = current;
}
}
if (hasRoute) //显示寻找到的路径
{
Stack<Vector2Int> trace = new Stack<Vector2Int>();
Vector2Int pos = end;
while (camefrom.ContainsKey(pos))
{
trace.Push(pos);
pos = camefrom[pos];
}
while (trace.Count > 0)
{
Vector2Int p = trace.Pop();
yield return new WaitForSeconds(duration);
if (p == start || p == end)
{
continue;
}
Map.Instance.ChangeColor(p, new Color(0, 1, 1));
}
}
}
public void Dijkstra(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
StartCoroutine(IDijkstra(map, start, end));
}
/// <summary>
/// 曼哈顿算法是一种常用于寻路算法中的启发式算法,它主要用于计算两点之间的距离。
/// 在二维平面上,曼哈顿距离指两点在网格上沿着垂直方向和水平方向行走的距离之和。
/// 也就是说,从点 A 到点 B 的曼哈顿距离是:|A.x - B.x| + |A.y - B.y|。
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public int Manhattan(Vector2Int a, Vector2Int b)
{
return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
}
/// <summary>
/// 迪杰斯特拉算法
///Dijkstra算法是一种基于贪心策略的算法,它从起点开始,逐步扩大到其他节点,
///每次选择当前距离起点最近的一个节点进行扩展,直到扩展到目标节点为止。它会记录每个节点到起点的距离,
///当遍历到一个节点时,如果经过这个节点可以使得到起点的距离更短,就更新这个节点的距离值。
///因此,Dijkstra算法适用于没有负权边的图。
/// </summary>
/// <param name="map"></param>
/// <param name="start"></param>
/// <param name="end"></param>
/// <returns></returns>
public IEnumerator IDijkstra(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
List<Vector2Int> sortList = new List<Vector2Int>();
Dictionary<Vector2Int, Vector2Int> camefrom = new Dictionary<Vector2Int, Vector2Int>(); //定义一个字典来存储每个节点的前一个节点。
sortList.Add(start);
camefrom[start] = new Vector2Int(-1, -1);
bool hasRoute = false;
while (sortList.Count > 0)
{
//拿到最近的点
sortList.Sort((Vector2Int a, Vector2Int b) =>
{
return Manhattan(start, a) - Manhattan(start, b);
});
Vector2Int current = sortList[0];
sortList.RemoveAt(0);
yield return new WaitForSeconds(duration);
if (current == start)
Map.Instance.ChangeColor(current, new Color(0, 1, 0));
else if (current == end)
Map.Instance.ChangeColor(current, new Color(1, 0, 0));
else
Map.Instance.ChangeColor(current, new Color(0, 0, 1));
if (current == end) //找到目标位置就退出循环
{
hasRoute = true;
break;
}
//将当前格子的四个临边格子加入队列
//这段代码创建了一个 Vector2Int 类型的数组,数组中有四个 Vector2Int 元素。
foreach (Vector2Int offset in offsets)
{
Vector2Int newPos = current + offset;
//超出边界
if (newPos.x < 0 || newPos.y < 0 || newPos.x >= map.Count || newPos.y >= map[0].Count)
{
continue;
}
//已经走过
if (camefrom.ContainsKey(newPos))
{
continue;
}
//墙
if (map[newPos.x][newPos.y].tag == "Wall")
{
continue;
}
sortList.Add(newPos); //加入队列
camefrom[newPos] = current;
}
}
if (hasRoute) //显示寻找到的路径
{
Stack<Vector2Int> trace = new Stack<Vector2Int>();
Vector2Int pos = end;
while (camefrom.ContainsKey(pos))
{
trace.Push(pos);
pos = camefrom[pos];
}
while (trace.Count > 0)
{
Vector2Int p = trace.Pop();
yield return new WaitForSeconds(duration);
if (p == start || p == end)
{
continue;
}
Map.Instance.ChangeColor(p, new Color(0, 1, 1));
}
}
}
public void AStar(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
StartCoroutine(IAStar(map, start, end));
}
/// <summary>
/// A星算法
///A算法在Dijkstra算法的基础上增加了一些启发式搜索的思想,它维护一个估价函数f(n) = g(n) + h(n),
///其中g(n)表示从起点到n节点的实际距离,h(n)表示从n节点到目标节点的估计距离。A算法在每次扩展节点时,
///优先选择f(n)值最小的节点进行扩展,因此它可以更快地找到目标节点。同时,通过合理的估价函数,
///A*算法可以避免对所有节点的遍历,从而更加高效。
/// </summary>
/// <param name="map"></param>
/// <param name="start"></param>
/// <param name="end"></param>
/// <returns></returns>
public IEnumerator IAStar(List<List<Transform>> map, Vector2Int start, Vector2Int end)
{
List<Vector2Int> sortList = new List<Vector2Int>();
Dictionary<Vector2Int, Vector2Int> camefrom = new Dictionary<Vector2Int, Vector2Int>(); //定义一个字典来存储每个节点的前一个节点。
sortList.Add(start);
camefrom[start] = new Vector2Int(-1, -1);
bool hasRoute = false;
while (sortList.Count > 0)
{
//拿到最近的点
sortList.Sort((Vector2Int a, Vector2Int b) =>
{
return (Manhattan(start, a) + Manhattan(end, a)) - (Manhattan(start, b) + Manhattan(end, b));
});
Vector2Int current = sortList[0];
sortList.RemoveAt(0);
yield return new WaitForSeconds(duration);
if (current == start)
Map.Instance.ChangeColor(current, new Color(0, 1, 0));
else if (current == end)
Map.Instance.ChangeColor(current, new Color(1, 0, 0));
else
Map.Instance.ChangeColor(current, new Color(0, 0, 1));
if (current == end) //找到目标位置就退出循环
{
hasRoute = true;
break;
}
//将当前格子的四个临边格子加入队列
//这段代码创建了一个 Vector2Int 类型的数组,数组中有四个 Vector2Int 元素。
foreach (Vector2Int offset in offsets)
{
Vector2Int newPos = current + offset;
//超出边界
if (newPos.x < 0 || newPos.y < 0 || newPos.x >= map.Count || newPos.y >= map[0].Count)
{
continue;
}
//已经走过
if (camefrom.ContainsKey(newPos))
{
continue;
}
//墙
if (map[newPos.x][newPos.y].tag == "Wall")
{
continue;
}
sortList.Add(newPos); //加入队列
camefrom[newPos] = current;
}
}
if (hasRoute) //显示寻找到的路径
{
Stack<Vector2Int> trace = new Stack<Vector2Int>();
Vector2Int pos = end;
while (camefrom.ContainsKey(pos))
{
trace.Push(pos);
pos = camefrom[pos];
}
while (trace.Count > 0)
{
Vector2Int p = trace.Pop();
yield return new WaitForSeconds(duration);
if (p == start || p == end)
{
continue;
}
Map.Instance.ChangeColor(p, new Color(0, 1, 1));
}
}
}
}
3.3 GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
private Ray ray;
private RaycastHit hit;
private GameObject currentObj; //当前点击的物体
private GameObject lastObj; //上一次点击的物体
private Color originalColor; //原本的颜色
public GameObject selectCanvas;
public Button setStartButton;
public Button setEndButton;
public Button setWallButton;
private bool isWall;
// Start is called before the first frame update
void Start()
{
foreach(var wall in GameObject.FindGameObjectsWithTag("Wall"))
{
wall.GetComponent<SpriteRenderer>().color = Color.black;
}
}
// Update is called once per frame
void Update()
{
//检测点击时,先判断鼠标是否在 UI 上,如果是,则不进行物体的检测,否则再进行物体的检测。
if (EventSystem.current.IsPointerOverGameObject())
{
return;
}
if (Input.GetMouseButtonDown(0))
{
if (lastObj)
{
lastObj.GetComponent<SpriteRenderer>().color = originalColor;
}
selectCanvas.SetActive(false);
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
//Debug.Log(hit.collider.name);
Map.Instance.GetGridPos(hit.collider.gameObject);
currentObj = hit.collider.gameObject;
originalColor = currentObj.GetComponent<SpriteRenderer>().color;
currentObj.GetComponent<SpriteRenderer>().color = new Color(1, 1, 0);
lastObj = currentObj;
}
}
else if (Input.GetMouseButtonDown(1))
{
ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.GetComponent<SpriteRenderer>().color == new Color(1, 1, 0))
{
selectCanvas.SetActive(true);
if (Map.Instance.GetGridPos(currentObj).y >= 8)
selectCanvas.transform.position = hit.transform.position - new Vector3(1.5f, 0, 0);
else
selectCanvas.transform.position = hit.transform.position + new Vector3(1.5f, 0, 0);
if (originalColor == new Color(0, 0, 0))
{
setWallButton.transform.GetChild(0).GetComponent<Text>().text = "取消墙体";
isWall = true;
}
else
{
setWallButton.transform.GetChild(0).GetComponent<Text>().text = "设置墙体";
isWall = false;
}
if (originalColor == new Color(0, 1, 0) || originalColor == new Color(1, 0, 0))
{
setStartButton.interactable = false;
setEndButton.interactable = false;
setWallButton.interactable = false;
}
else
{
setStartButton.interactable = true;
setEndButton.interactable = true;
setWallButton.interactable = true;
}
}
}
}
}
public void OnBreadFirstButtonClick()
{
PathFinder.Instance.BreadFirst(Map.Instance.mapList, PathFinder.Instance.start, PathFinder.Instance.end);
}
public void OnDijkstraButtonClick()
{
PathFinder.Instance.Dijkstra(Map.Instance.mapList, PathFinder.Instance.start, PathFinder.Instance.end);
}
public void OnResetSceneButtonClick(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
public void OnAStarButtonClick()
{
PathFinder.Instance.AStar(Map.Instance.mapList, PathFinder.Instance.start, PathFinder.Instance.end);
}
public void OnSetStartButtonClick()
{
Map.Instance.mapList[PathFinder.Instance.start.x][PathFinder.Instance.start.y].gameObject.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1);
currentObj.GetComponent<SpriteRenderer>().color = new Color(0, 1, 0);
originalColor = new Color(0, 1, 0);
Vector2Int gridPos = Map.Instance.GetGridPos(hit.collider.gameObject);
if (gridPos.x >= 0 && gridPos.y >= 0)
{
PathFinder.Instance.start = Map.Instance.GetGridPos(hit.collider.gameObject);
}
selectCanvas.SetActive(false);
}
public void OnSetEndButtonClick()
{
Map.Instance.mapList[PathFinder.Instance.end.x][PathFinder.Instance.end.y].gameObject.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1);
currentObj.GetComponent<SpriteRenderer>().color = new Color(1, 0, 0);
originalColor = new Color(1, 0, 0);
Vector2Int gridPos = Map.Instance.GetGridPos(hit.collider.gameObject);
if (gridPos.x >= 0 && gridPos.y >= 0)
{
PathFinder.Instance.end = Map.Instance.GetGridPos(hit.collider.gameObject);
}
selectCanvas.SetActive(false);
}
public void OnSetWallButtonClick()
{
if(isWall)
{
currentObj.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1);
originalColor = new Color(1, 1, 1);
currentObj.tag = "Untagged";
}
else
{
currentObj.GetComponent<SpriteRenderer>().color = new Color(0, 0, 0);
originalColor = new Color(0, 0, 0);
currentObj.tag = "Wall";
}
selectCanvas.SetActive(false);
}
}
4、项目链接
链接:https://pan.baidu.com/s/1NyD-MgytfVudRKaV-upEzw
提取码:rvff