前言
在使用Unity開發游戲項目時,經常會遇到一些角色的導航需求,然而Unity提供給我們的NavMesh+NavMeshAgent并不能很好幫我們實現,且尋路網格的烘焙,以及在導航過程中尋路網格的檢測,都是非常消耗性能的,因此在很多企業項目中,會自己寫一下高效的尋路算法來完成需求,其中有一種就是A*尋路算法。A*尋路算法是一種啟發式算法,從所有可以行走的路徑中找出估量代價最小的,遞歸每步這樣的操作,最終到達目標點。
-
A尋路算法的估量代價*
在A*算法中核心的尋路依據就是估量代價,在A*中通常用 F 表示。
F = G + H
其中G表示當前點到起始點的估量代價,H表示當前點到終點的代價。
-
G的計算方式:最開始,以起點為中心開始計算周邊的八個格子,然后在以這算出來的八個格子為中心,繼續計算他們周圍的八個格子的G值。以此類推,直到找到終端,或遍歷完所有的格子。G值的計算結果為:中心點的G值+距離值【10 or 14】
FGH示意圖圖中格子左下角為G值,右下角為H值,左上角為F值
因此從當前格子周邊的八個格子中找到下一步所走的格子,依據F值,當F值相同時隨機選擇。
當然在尋路過程中,會有障礙,不能通過,通過可以行走的格子走到終點。下圖中綠色為起點,紅色為終點,藍色是障礙,淺藍邊框是參與計算的格子,A*就是通過這樣的一系列計算完成的最優尋路。
-
AStar尋路步驟
- 設置開放列表
OpenList
和關閉列表CloseList
- 將起點放置到OpenList
- 開啟循環
While(OpenList.count > 0)
3.1 將OpenList排序【F值從小到大】
3.2 OpenList[0]必定是F值最小的,命名為Center
3.2.1 發現Center就是終點,回溯找到導航路徑
3.3 以這個點為中心,去發現它周圍的8個格子
3.4 計算發現的每個格子G H F三個值
3.5 如果這個格子沒有被計算過或原來G值,比這次計算出來的G要大
3.5.1 此時,設置新的FGH值給這個格子,并設置該格子的發現者是Center
3.6 如果這個格子被計算過,且原G值,比這次計算出來的G要小
3.6.1 此時,就不能替換原來FGH值
3.7 將發現的每個格子放入到OpenList
3.7.1 放入的時候要做檢測【該格子不在OpenList、該格子不在CloseList】
3.8 將此次循環的發現者Center放入到CloseList
3.8 判斷OpenList空了
3.8.1 說明所有的可發現的格子都被遍歷過了,始終沒有找到中,說明無法到達終點
- 設置開放列表
下面我寫了一個小例子,方便大家學習。
-
簡單效果
簡單效果 首先需要創建一個格子類Grid
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
/// <summary>
/// 格子類型
/// </summary>
public enum GridType
{
//正常類型
Normal,
//障礙物類型
Obstacle,
//起點類型
Start,
//終點類型
End
}
/// <summary>
/// 格子類(實現IComparable方便排序)
/// </summary>
public class Grid : IComparable
{
//格子坐標x-y
public int x;
public int y;
//格子A*三屬性f-g-h
public int f;
public int g;
public int h;
//格子類型
public GridType type;
//格子的歸屬(父格子)
public Grid parent;
//構造賦值
public Grid (int x, int y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// 實現排序接口方法
/// </summary>
/// <returns>The to.</returns>
/// <param name="obj">Object.</param>
public int CompareTo (object obj)
{
Grid grid = (Grid)obj;
if (this.f < grid.f) {
//升序
return -1;
}
if (this.f > grid.f) {
//降序
return 1;
}
return 0;
}
}
- 然后主邏輯AStar類
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class MyAStar : MonoBehaviour
{
/// <summary>
/// 單例腳本
/// </summary>
public static MyAStar instance;
//參考物體預設體
public GameObject reference;
//格子數組
public Grid[,] grids;
//格子數組對應的參考物(方塊)對象
public GameObject[,] objs;
//開啟列表
public ArrayList openList;
//關閉列表
public ArrayList closeList;
//目標點坐標
public int targetX;
public int targetY;
//起始點坐標
public int startX;
public int startY;
//格子行列數
private int row;
private int colomn;
//結果棧
private Stack<string> parentList;
//基礎物體
private Transform plane;
private Transform start;
private Transform end;
private Transform obstacle;
//流顏色參數
private float alpha = 0;
private float incrementPer = 0;
void Awake ()
{
instance = this;
plane = GameObject.Find ("Plane").transform;
start = GameObject.Find ("Start").transform;
end = GameObject.Find ("End").transform;
obstacle = GameObject.Find ("Obstacle").transform;
parentList = new Stack<string> ();
openList = new ArrayList ();
closeList = new ArrayList ();
}
/// <summary>
/// 初始化操作
/// </summary>
void Init ()
{
//計算行列數
int x = (int)(plane.localScale.x * 20);
int y = (int)(plane.localScale.z * 20);
row = x;
colomn = y;
grids = new Grid[x, y];
objs = new GameObject[x, y];
//起始坐標
Vector3 startPos =
new Vector3 (plane.localScale.x * -5, 0, plane.localScale.z * -5);
//生成參考物體(Cube)
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
grids [i, j] = new Grid (i, j);
GameObject item = (GameObject)Instantiate (reference,
new Vector3 (i * 0.5f, 0, j * 0.5f) + startPos,
Quaternion.identity);
item.transform.GetChild (0).GetComponent<Reference> ().x = i;
item.transform.GetChild (0).GetComponent<Reference> ().y = j;
objs [i, j] = item;
}
}
}
/// <summary>
/// A*計算
/// </summary>
IEnumerator Count ()
{
//等待前面操作完成
yield return new WaitForSeconds (0.1f);
//添加起始點
openList.Add (grids [startX, startY]);
//聲明當前格子變量,并賦初值
Grid currentGrid = openList [0] as Grid;
//循環遍歷路徑最小F的點
while (openList.Count > 0 && currentGrid.type != GridType.End) {
//獲取此時最小F點
currentGrid = openList [0] as Grid;
//如果當前點就是目標
if (currentGrid.type == GridType.End) {
Debug.Log ("Find");
//生成結果
GenerateResult (currentGrid);
}
//上下左右,左上左下,右上右下,遍歷
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i != 0 || j != 0) {
//計算坐標
int x = currentGrid.x + i;
int y = currentGrid.y + j;
//如果未超出所有格子范圍,不是障礙物,不是重復點
if (x >= 0 && y >= 0 && x < row && y < colomn
&& grids [x, y].type != GridType.Obstacle
&& !closeList.Contains (grids [x, y])) {
//計算G值
int g = currentGrid.g + (int)(Mathf.Sqrt ((Mathf.Abs (i) + Mathf.Abs (j))) * 10);
//與原G值對照
if (grids [x, y].g == 0 || grids [x, y].g > g) {
//更新G值
grids [x, y].g = g;
//更新父格子
grids [x, y].parent = currentGrid;
}
//計算H值
grids [x, y].h = Manhattan (x, y);
//計算F值
grids [x, y].f = grids [x, y].g + grids [x, y].h;
//如果未添加到開啟列表
if (!openList.Contains (grids [x, y])) {
//添加
openList.Add (grids [x, y]);
}
//重新排序
openList.Sort ();
}
}
}
}
//完成遍歷添加該點到關閉列表
closeList.Add (currentGrid);
//從開啟列表中移除
openList.Remove (currentGrid);
//如果開啟列表空,未能找到路徑
if (openList.Count == 0) {
Debug.Log ("Can not Find");
}
}
}
/// <summary>
/// 生成結果
/// </summary>
/// <param name="currentGrid">Current grid.</param>
void GenerateResult (Grid currentGrid)
{
//如果當前格子有父格子
if (currentGrid.parent != null) {
//添加到父對象棧(即結果棧)
parentList.Push (currentGrid.x + "|" + currentGrid.y);
//遞歸獲取
GenerateResult (currentGrid.parent);
}
}
/// <summary>
/// 顯示結果
/// </summary>
/// <returns>The result.</returns>
IEnumerator ShowResult ()
{
//等待前面計算完成
yield return new WaitForSeconds (0.3f);
//計算每幀顏色值增量
incrementPer = 1 / (float)parentList.Count;
//展示結果
while (parentList.Count != 0) {
//出棧
string str = parentList.Pop ();
//等0.3秒
yield return new WaitForSeconds (0.3f);
//拆分獲取坐標
string[] xy = str.Split (new char[]{ '|' });
int x = int.Parse (xy [0]);
int y = int.Parse (xy [1]);
//當前顏色值
alpha += incrementPer;
//以顏色方式繪制路徑
objs [x, y].transform.GetChild (0).GetComponent<MeshRenderer> ().material.color
= new Color (1 - alpha, alpha, 0, 1);
}
}
/// <summary>
/// 曼哈頓方式計算H值
/// </summary>
/// <param name="x">The x coordinate.</param>
/// <param name="y">The y coordinate.</param>
int Manhattan (int x, int y)
{
return (int)(Mathf.Abs (targetX - x) + Mathf.Abs (targetY - y)) * 10;
}
void Start ()
{
Init ();
StartCoroutine (Count ());
StartCoroutine (ShowResult ());
}
}
- 最后是參考預設體方塊的簡單實現
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class Reference : MonoBehaviour
{
//顏色材質區分
public Material startMat;
public Material endMat;
public Material obstacleMat;
//顯示信息Text
private Text text;
//當前格子坐標
public int x;
public int y;
void Awake ()
{
text = GameObject.Find ("Text").GetComponent<Text> ();
}
//判斷當前格子的類型
void OnTriggerEnter (Collider other)
{
if (other.name == "Start") {
GetComponent<MeshRenderer> ().material = startMat;
MyAStar.instance.grids [x, y].type = GridType.Start;
MyAStar.instance.openList.Add (MyAStar.instance.grids [x, y]);
MyAStar.instance.startX = x;
MyAStar.instance.startY = y;
} else if (other.name == "End") {
GetComponent<MeshRenderer> ().material = endMat;
MyAStar.instance.grids [x, y].type = GridType.End;
MyAStar.instance.targetX = x;
MyAStar.instance.targetY = y;
} else if (other.name == "Obstacle") {
GetComponent<MeshRenderer> ().material = obstacleMat;
MyAStar.instance.grids [x, y].type = GridType.Obstacle;
}
}
/// <summary>
/// 鼠標點擊顯示當前格子基礎信息
/// </summary>
void OnMouseDown ()
{
text.text = "XY(" + x + "," + y + ")" + "\n" +
"FGH(" + MyAStar.instance.grids [x, y].f + "," +
MyAStar.instance.grids [x, y].g + "," +
MyAStar.instance.grids [x, y].h + ")";
text.color = GetComponent<MeshRenderer> ().material.color;
}
}
-
多障礙效果圖
多障礙效果圖 -
遍歷流程監測
其實A*遍歷的格子還是蠻多的,因為曼哈頓計算的H值是不考慮障礙物的,所以會有很多格子的F值會很小,但不一定此時很小的F值格子就是要走的路徑,最終的最優路徑是通過,終點格子反推回來的,就如代碼中GenerateResult遞歸方法所示,為了方便大家理解,我做了一個小動畫,方便大家對A*的理解。(粉色是此時F值最小的格子,藍色是最小F值格子周邊正在遍歷的格子,黃色格子是從未設置父物體,第一次被遍歷的格子)
遍歷流程監測
慢放版
結束語
A*只是游戲算法中的鳳毛麟角,其中的邏輯也相對簡單,所以想要提升編碼質量,想要寫出高效的游戲邏輯,還需要更多的算法學習。還是那個道理,編程 = 數據結構 + 算法,不帶班的這段時間我會盡量分享一些東西給大家,同仁們加油。本文項目下載鏈接:http://pan.baidu.com/s/1hs13F8K 密碼: rbs1