被生活++了一个多月,都没时间上来吹比了。
因为破游戏准备设计敌人了,苦于Unity自带的导航系统迟迟不适配2D项目,即便用最新的NavMeshSurface把3D当成2D来用,也和我的想法背道而驰,资源商店里的又动不动几百块╮( ̄▽ ̄")╭,还没有试用功能,鬼知道能不能融入我这破游戏的框架里。
那就只能自己写一个寻路了,久闻A*算法大名,网上教程也挺多的,看了几篇之后,大致了解了基本内容,最后还上油管看了个视频,东拼西凑设计出来自己的A*,虽然性能甚至比不上官方的NavMesh,180个寻路单位在普通情况下每次寻路需要10ms~20ms,不过至少能在2D上用了。为了追求性能,我甚至研究了ECS框架和Job System,不过鉴于使用限制太大(只能操作值类型,反复开辟和回收空间对性能消耗来说太蛋疼了- . -),以我的水平写出来的寻路Job性能还没不用的好,最后放弃,不过还好学会怎么在Unity上用ECS框架管理游戏对象和用Jobs优化性能了。
言归正传,A*的基本思路我也不扯了,浪费篇幅,随便度娘一下就可以了。
A*是基于网格的,所以最先是弄一个AStarNode类来存储点信息,实现如下:
public class AStarNode : IHeapItem<AStarNode>
{
public readonly Vector3 worldPosition;
public readonly Vector2Int gridPosition;
public readonly float height;
public bool walkable;
public AStarNode parent;
public int connectionLabel;
public int GCost { get; set; }
public int HCost { get; set; }
public int FCost { get { return GCost + HCost; } }
public int HeapIndex { get; set; }
public AStarNode(Vector3 position, int gridX, int gridY, float height)
{
worldPosition = position;
gridPosition = new Vector2Int(gridX, gridY);
this.height = height;
walkable = true;
}
public int CalculateHCostTo(AStarNode other)
{
//使用曼哈顿距离
int disX = Mathf.Abs(gridPosition.x - other.gridPosition.x);
int disY = Mathf.Abs(gridPosition.y - other.gridPosition.y);
if (disX > disY)
return 14 * disY + 10 * (disX - disY) + Mathf.RoundToInt(Mathf.Abs(height - other.height));
else return 14 * disX + 10 * (disY - disX) + Mathf.RoundToInt(Mathf.Abs(height - other.height));
}
public bool CanReachTo(AStarNode other)
{
return connectionLabel > 0 && connectionLabel == other.connectionLabel;
}
public int CompareTo(AStarNode other)
{
if (FCost < other.FCost || (FCost == other.FCost && HCost < other.HCost) || (FCost == other.FCost && HCost == other.HCost && height < other.height))
return -1;
else if (FCost == other.FCost && HCost == other.HCost && height == other.height) return 0;
else return 1;
}
public static implicit operator Vector3(AStarNode self)
{
return self.worldPosition;
}
public static implicit operator Vector2(AStarNode self)
{
return self.worldPosition;
}
public static implicit operator bool(AStarNode self)
{
return self != null;
}
}
其中,IHeapItem是栈元素接口,因为A*每轮需要从Open表里选一个距离最小的点出来,用堆排性能较好,接口内容如下:
public interface IHeapItem<T> : IComparable<T>
{
int HeapIndex { get; set; }
}
相应的,用于排序的堆实现如下:
public class Heap<T> where T : class, IHeapItem<T>
{
private readonly T[] items;
private readonly int maxSize;
private readonly HeapType heapType;
public int Count { get; private set; }
public Heap(int size, HeapType heapType = HeapType.MinHeap)
{
items = new T[size];
maxSize = size;
this.heapType = heapType;
}
public void Add(T item)
{
if (Count >= maxSize) return;
item.HeapIndex = Count;
items[Count] = item;
Count++;
SortUpForm(item);
}
public T RemoveRoot()
{
if (Count < 1) return default;
T root = items[0];
root.HeapIndex = -1;
Count--;
if (Count > 0)
{
items[0] = items[Count];
items[0].HeapIndex = 0;
SortDownFrom(items[0]);
}
return root;
}
public bool Contains(T item)
{
if (item == default || item.HeapIndex < 0 || item.HeapIndex > Count - 1) return false;
return Equals(items[item.HeapIndex], item);//用items.Contains()就等着哭吧
}
public void Clear()
{
Count = 0;
}
private void SortUpForm(T item)
{
int parentIndex = (item.HeapIndex - 1) / 2;
while (true)
{
T parent = items[parentIndex];
if (Equals(parent, item)) return;
if (heapType == HeapType.MinHeap ? item.CompareTo(parent) < 0 : item.CompareTo(parent) > 0)
{
if (!Swap(item, parent))
return;//交换不成功则退出,防止死循环
}
else return;
parentIndex = (item.HeapIndex - 1) / 2;
}
}
private void SortDownFrom(T item)
{
while (true)
{
int leftChildIndex = item.HeapIndex * 2 + 1;
int rightChildIndex = item.HeapIndex * 2 + 2;
if (leftChildIndex < Count)
{
int swapIndex = leftChildIndex;
if (rightChildIndex < Count && (heapType == HeapType.MinHeap ?
items[rightChildIndex].CompareTo(items[leftChildIndex]) < 0 : items[rightChildIndex].CompareTo(items[leftChildIndex]) > 0))
swapIndex = rightChildIndex;
if (heapType == HeapType.MinHeap ? items[swapIndex].CompareTo(item) < 0 : items[swapIndex].CompareTo(item) > 0)
{
if (!Swap(item, items[swapIndex]))
return;//交换不成功则退出,防止死循环
}
else return;
}
else return;
}
}
public void Update()
{
if (Count < 1) return;
SortDownFrom(items[0]);
SortUpForm(items[Count - 1]);
}
private bool Swap(T item1, T item2)
{
if (!Contains(item1) || !Contains(item2)) return false;
items[item1.HeapIndex] = item2;
items[item2.HeapIndex] = item1;
int item1Index = item1.HeapIndex;
item1.HeapIndex = item2.HeapIndex;
item2.HeapIndex = item1Index;
return true;
}
public static implicit operator bool(Heap<T> self)
{
return self != null;
}
public enum HeapType
{
MinHeap,
MaxHeap
}
}
可以看到,因为堆需要排序更新Item的HeapIndex,所以值类型绝逼是不能用了,每次传递都是复制一份,在堆里改完了数据,怎么才能写回没加入堆前的原来的对象里?除非用指针,基于指针的用于管理结构体的Heap我也实现了,不过与此文无关,不贴上来了。
接下来是算法的核心,我单独用一个AStar类来实现:
public class AStar
{
public AStar(Vector2 worldSize, float cellSize, CastCheckType castCheckType, float castRadiusMultiple,
LayerMask unwalkableLayer, LayerMask groundLayer, bool threeD, float worldHeight, int cellHeight, GoalSelectMode goalSelectMode)
{
this.worldSize = worldSize;
this.cellSize = cellSize;
this.castCheckType = castCheckType;
this.castRadiusMultiple = castRadiusMultiple;
this.unwalkableLayer = unwalkableLayer;
this.groundLayer = groundLayer;
this.threeD = threeD;
this.worldHeight = worldHeight;
this.cellHeight = cellHeight;
this.goalSelectMode = goalSelectMode;
GridSize = Vector2Int.RoundToInt(worldSize / cellSize);
Grid = new AStarNode[Mathf.RoundToInt(GridSize.x), Mathf.RoundToInt(GridSize.y)];
openCache = new Heap<AStarNode>(GridSize.x * GridSize.y);
closedCache = new HashSet<AStarNode>();
pathCache = new List<AStarNode>();
}
private readonly bool threeD;
private readonly Vector2 worldSize;
private readonly float worldHeight;
private readonly LayerMask groundLayer;
private readonly int cellHeight;
private readonly float cellSize;
public Vector2Int GridSize { get; private set; }
public AStarNode[,] Grid { get; private set; }
private readonly LayerMask unwalkableLayer;
private readonly CastCheckType castCheckType;
private readonly float castRadiusMultiple;
private readonly GoalSelectMode goalSelectMode;
#region 网格相关
public void CreateGrid(Vector3 axisOrigin)
{
for (int i = 0; i < GridSize.x; i++)
for (int j = 0; j < GridSize.y; j++)
CreateNode(axisOrigin, i, j);
RefreshGrid();
}
private void CreateNode(Vector3 axisOrigin, int gridX, int gridY)
{
//Debug.Log(gridX + ":" + gridY);
Vector3 nodeWorldPos;
if (!threeD)
{
nodeWorldPos = axisOrigin + Vector3.right * (gridX + 0.5f) * cellSize + Vector3.up * (gridY + 0.5f) * cellSize;
}
else
{
nodeWorldPos = axisOrigin + Vector3.right * (gridX + 0.5f) * cellSize + Vector3.forward * (gridY + 0.5f) * cellSize;
float height;
if (Physics.Raycast(nodeWorldPos + Vector3.up * (worldHeight + 0.01f), Vector3.down, out RaycastHit hit, worldHeight + 0.01f, groundLayer))
height = hit.point.y;
else height = worldHeight + 0.01f;
nodeWorldPos += Vector3.up * height;
}
Grid[gridX, gridY] = new AStarNode(nodeWorldPos, gridX, gridY, threeD ? nodeWorldPos.y : 0);
}
public void RefreshGrid()
{
if (Grid == null) return;
for (int i = 0; i < GridSize.x; i++)
for (int j = 0; j < GridSize.y; j++)
CheckNodeWalkable(Grid[i, j]);
CalculateConnections();
}
public void RefreshGrid(Vector3 fromPoint, Vector3 toPoint)
{
if (Grid == null) return;
AStarNode fromNode = WorldPointToNode(fromPoint);
AStarNode toNode = WorldPointToNode(toPoint);
if (fromNode == toNode)
{
CheckNodeWalkable(fromNode);
return;
}
AStarNode min = fromNode.gridPosition.x <= toNode.gridPosition.x && fromNode.gridPosition.y <= toNode.gridPosition.y ? fromNode : toNode;
AStarNode max = fromNode.gridPosition.x > toNode.gridPosition.x && fromNode.gridPosition.y > toNode.gridPosition.y ? fromNode : toNode;
fromNode = min;
toNode = max;
//Debug.Log(string.Format("From {0} to {1}", fromNode.GridPosition, toNode.GridPosition));
if (toNode.gridPosition.x - fromNode.gridPosition.x <= toNode.gridPosition.y - fromNode.gridPosition.y)
for (int i = fromNode.gridPosition.x; i <= toNode.gridPosition.x; i++)
for (int j = fromNode.gridPosition.y; j <= toNode.gridPosition.y; j++)
CheckNodeWalkable(Grid[i, j]);
else for (int i = fromNode.gridPosition.y; i <= toNode.gridPosition.y; i++)
for (int j = fromNode.gridPosition.x; j <= toNode.gridPosition.x; j++)
CheckNodeWalkable(Grid[i, j]);
CalculateConnections();
}
public AStarNode WorldPointToNode(Vector3 position)
{
if (Grid == null || (threeD && position.y > worldHeight)) return null;
int gX = Mathf.RoundToInt((GridSize.x - 1) * Mathf.Clamp01((position.x + worldSize.x / 2) / worldSize.x));
//gX怎么算:首先利用坐标系原点的 x 坐标来修正该点的 x 轴坐标,然后除以世界宽度,获得该点的 x 坐标在网格坐标系上所处的区域,用不大于 1 分数来表示,
//然后获得相同区域的网格的 x 坐标即 gX,举个例子:
//假设 x 坐标为 -2,而世界起点的 x 坐标为 -24, 即实际坐标原点 x 坐标 0 减去世界宽度的一半,则修正 x 坐标为 x + 24 = 22,这就是它在A*坐标系上虚拟的修正了的位置 x',
//以上得知世界宽度为48,那么 22 / 48 = 11/24,说明 x' 在世界宽度轴 11/24 的位置上,所以,该位置相应的格子的 x 坐标也在网格宽度轴的11/24位置上,
//假设网格宽度为也为48,则 gX = 48 * 11/24 = 22,看似正确,其实,假设上面算得的 x' 是48,那么 48 * 48/48 = 48,而网格坐标最多到47,因为数组下标从0开始,
//所以这时就会发生越界错误,反而用 gX = (48 - 1) * 48/48 = 47 * 1 = 47 来代替就对了,回过头来,x' 是 22,则 47 * 11/24 = 21.54 ≈ 22,位于 x' 轴左数第 23 个格子上,
//再假设算出的 x' 是 30,则 gX = 47 * 15/24 = 29.38 ≈ 29,完全符合逻辑,为什么不用 48 算完再减去 1 ?如果 x' 是 0, 48 * 0/48 - 1 = -1,又越界了
int gY;
if (!threeD) gY = Mathf.RoundToInt((GridSize.y - 1) * Mathf.Clamp01((position.y + worldSize.y / 2) / worldSize.y));
else gY = Mathf.RoundToInt((GridSize.y - 1) * Mathf.Clamp01((position.z + worldSize.y / 2) / worldSize.y));
return Grid[gX, gY];
}
private bool CheckNodeWalkable(AStarNode node)
{
if (!node) return false;
if (!threeD)
{
RaycastHit2D[] hit2Ds = new RaycastHit2D[0];
switch (castCheckType)
{
case CastCheckType.Box:
hit2Ds = Physics2D.BoxCastAll(node.worldPosition,
Vector2.one * cellSize * castRadiusMultiple * 2, 0, Vector2.zero, Mathf.Infinity, unwalkableLayer);
break;
case CastCheckType.Sphere:
hit2Ds = Physics2D.CircleCastAll(node.worldPosition,
cellSize * castRadiusMultiple, Vector2.zero, Mathf.Infinity, unwalkableLayer);
break;
}
node.walkable = hit2Ds.Length < 1 || hit2Ds.Where(h => !h.collider.isTrigger && h.collider.tag != "Player").Count() < 1;
}
else
{
RaycastHit[] hits = new RaycastHit[0];
switch (castCheckType)
{
case CastCheckType.Box:
hits = Physics.BoxCastAll(node.worldPosition, Vector3.one * cellSize * castRadiusMultiple,
Vector3.up, Quaternion.identity, (cellHeight - 1) * cellSize, unwalkableLayer, QueryTriggerInteraction.Ignore);
break;
case CastCheckType.Sphere:
hits = Physics.SphereCastAll(node.worldPosition, cellSize * castRadiusMultiple,
Vector3.up, (cellHeight - 1) * cellSize, unwalkableLayer, QueryTriggerInteraction.Ignore);
break;
}
node.walkable = node.height < worldHeight && (hits.Length < 1 || hits.Where(h => h.collider.tag != "Player").Count() < 1);
}
return node.walkable;
}
public bool WorldPointWalkable(Vector3 point)
{
return CheckNodeWalkable(WorldPointToNode(point));
}
private AStarNode GetClosestSurroundingNode(AStarNode node, AStarNode closestTo, int ringCount = 1)
{
var neighbours = GetSurroundingNodes(node, ringCount);
if (ringCount >= Mathf.Max(GridSize.x, GridSize.y)) return null;//突破递归
AStarNode closest = neighbours.FirstOrDefault(x => x.walkable);
if (closest)
using (var neighbourEnum = neighbours.GetEnumerator())
{
while (neighbourEnum.MoveNext())
if (neighbourEnum.Current == closest) break;
while (neighbourEnum.MoveNext())
{
AStarNode neighbour = neighbourEnum.Current;
if (Vector3.Distance(closestTo.worldPosition, neighbour.worldPosition) < Vector3.Distance(closestTo.worldPosition, closest.worldPosition))
if (neighbour.walkable) closest = neighbour;
}
}
if (!closest) return GetClosestSurroundingNode(node, closestTo, ringCount + 1);
else return closest;
}
private readonly List<AStarNode> neighbours = new List<AStarNode>();
private List<AStarNode> GetSurroundingNodes(AStarNode node, int ringCount/*圈数*/)
{
neighbours.Clear();
if (node != null && ringCount > 0)
{
int neiborX;
int neiborY;
for (int x = -ringCount; x <= ringCount; x++)
for (int y = -ringCount; y <= ringCount; y++)
{
if (Mathf.Abs(x) < ringCount && Mathf.Abs(y) < ringCount) continue;//对于圈内的结点,总有其x和y都小于圈数,所以依此跳过
neiborX = node.gridPosition.x + x;
neiborY = node.gridPosition.y + y;
if (neiborX >= 0 && neiborX < GridSize.x && neiborY >= 0 && neiborY < GridSize.y)
neighbours.Add(Grid[neiborX, neiborY]);
}
}
return neighbours;
}
private List<AStarNode> GetReachableNeighbours(AStarNode node)
{
neighbours.Clear();
if (node != null)
{
int neiborX;
int neiborY;
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
{
if (x == 0 && y == 0) continue;
neiborX = node.gridPosition.x + x;
neiborY = node.gridPosition.y + y;
if (neiborX >= 0 && neiborX < GridSize.x && neiborY >= 0 && neiborY < GridSize.y)
if (Reachable(Grid[neiborX, neiborY]))
neighbours.Add(Grid[neiborX, neiborY]);
}
}
return neighbours;
bool Reachable(AStarNode neighbour)
{
if (neighbour.gridPosition.x == node.gridPosition.x || neighbour.gridPosition.y == node.gridPosition.y) return neighbour.walkable;
if (!neighbour.walkable) return false;
if (neighbour.gridPosition.x > node.gridPosition.x && neighbour.gridPosition.y > node.gridPosition.y)//右上角
{
int leftX = node.gridPosition.x;
int leftY = neighbour.gridPosition.y;
AStarNode leftNode = Grid[leftX, leftY];
if (leftNode.walkable)
{
int downX = neighbour.gridPosition.x;
int downY = node.gridPosition.y;
AStarNode downNode = Grid[downX, downY];
if (!downNode.walkable) return false;
else return true;
}
else return false;
}
else if (neighbour.gridPosition.x > node.gridPosition.x && neighbour.gridPosition.y < node.gridPosition.y)//右下角
{
int leftX = node.gridPosition.x;
int leftY = neighbour.gridPosition.y;
AStarNode leftNode = Grid[leftX, leftY];
if (leftNode.walkable)
{
int upX = neighbour.gridPosition.x;
int upY = node.gridPosition.y;
AStarNode upNode = Grid[upX, upY];
if (!upNode.walkable) return false;
else return true;
}
else return false;
}
else if (neighbour.gridPosition.x < node.gridPosition.x && neighbour.gridPosition.y > node.gridPosition.y)//左上角
{
int rightX = node.gridPosition.x;
int rightY = neighbour.gridPosition.y;
AStarNode rightNode = Grid[rightX, rightY];
if (rightNode.walkable)
{
int downX = neighbour.gridPosition.x;
int downY = node.gridPosition.y;
AStarNode downNode = Grid[downX, downY];
if (!downNode.walkable) return false;
else return true;
}
else return false;
}
else if (neighbour.gridPosition.x < node.gridPosition.x && neighbour.gridPosition.y < node.gridPosition.y)//左下角
{
int rightX = node.gridPosition.x;
int rightY = neighbour.gridPosition.y;
AStarNode rightNode = Grid[rightX, rightY];
if (rightNode.walkable)
{
int upX = neighbour.gridPosition.x;
int upY = node.gridPosition.y;
AStarNode upNode = Grid[upX, upY];
if (!upNode.walkable) return false;
else return true;
}
else return false;
}
else return true;
}
}
private bool CanGoStraight(Vector3 from, Vector3 to)
{
Vector3 dir = (to - from).normalized;
float dis = Vector3.Distance(from, to);
float checkRadius = cellSize * castRadiusMultiple;
float radiusMultiple = 1;
if (castCheckType == CastCheckType.Box)//根据角度确定两个端点的偏移量
{
float x, y, angle;
if (!threeD)
{
if (from.x < to.x)
angle = Vector2.Angle(Vector2.right.normalized, dir);
else
angle = Vector2.Angle(Vector2.left.normalized, dir);
}
else
{
if (from.x < to.x)
angle = Vector3.Angle(Vector3.right.normalized, new Vector3(dir.x, 0, dir.z));
else
angle = Vector3.Angle(Vector3.left.normalized, new Vector3(dir.x, 0, dir.z));
}
if (angle < 45)
{
x = 1;
y = Mathf.Tan(angle * Mathf.Deg2Rad);
}
else if (angle == 90)
{
x = 0;
y = 1;
}
else
{
y = 1;
x = 1 / Mathf.Tan(angle * Mathf.Deg2Rad);
}
radiusMultiple = Mathf.Sqrt(x * x + y * y);
}
if (!threeD)
{
bool hit = Physics2D.Raycast(from, dir, dis, unwalkableLayer);
if (!hit)//射不中,则进行第二次检测
{
float x1 = -dir.y / dir.x;
Vector3 point1 = from + new Vector3(x1, 1).normalized * checkRadius * radiusMultiple;
bool hit1 = Physics2D.Raycast(point1, dir, dis, unwalkableLayer);
if (!hit1)//射不中,进行第三次检测
{
float x2 = dir.y / dir.x;
Vector3 point2 = from + new Vector3(x2, -1).normalized * checkRadius * radiusMultiple;
bool hit2 = Physics2D.Raycast(point2, dir, dis, unwalkableLayer);
if (!hit2) return true;
else return false;
}
else return false;
}
else return false;
}
else
{
bool hit = Physics.Raycast(from, dir, dis, unwalkableLayer, QueryTriggerInteraction.Ignore);
if (!hit)
{
float x1 = -dir.z / dir.x;
Vector3 point1 = from + new Vector3(x1, 0, 1).normalized * checkRadius * radiusMultiple;//左边
bool hit1 = Physics.Raycast(point1, dir, dis, unwalkableLayer, QueryTriggerInteraction.Ignore);
if (!hit1)
{
float x2 = dir.z / dir.x;
Vector3 point2 = from + new Vector3(x2, 0, -1).normalized * checkRadius * radiusMultiple;//右边
bool hit2 = Physics.Raycast(point2, dir, dis, unwalkableLayer, QueryTriggerInteraction.Ignore);
if (!hit2)
{
float x3 = -dir.y / dir.x;
Vector3 point3 = from + new Vector3(x3, 1, 0).normalized * checkRadius;//底部
bool hit3 = Physics.Raycast(point3, dir, dis, unwalkableLayer, QueryTriggerInteraction.Ignore);
if (!hit3)
{
for (int i = 1; i <= cellHeight; i++)//上部
{
float x4 = -dir.y / dir.x;
Vector3 point4 = from + new Vector3(x4, -1, 0).normalized * (checkRadius * (1 + 2 * (i - 1)));
bool hit4 = Physics.Raycast(point4, dir, dis, unwalkableLayer, QueryTriggerInteraction.Ignore);
if (hit4) return false;
}
return true;
}
else return false;
}
else return false;
}
else return false;
}
else return false;
}
}
private AStarNode GetEffectiveGoal(AStarNode startNode, AStarNode goalNode)
{
switch (goalSelectMode)
{
case GoalSelectMode.ByRing:
return GetEffectiveGoalByRing(startNode, goalNode);
case GoalSelectMode.ByLine:
return GetEffectiveGoalByLine(startNode, goalNode);
case GoalSelectMode.None:
default:
return goalNode;
}
}
private AStarNode GetEffectiveGoalByLine(AStarNode startNode, AStarNode goalNode)
{
AStarNode newGoalNode = null;
int startNodeNodeX = startNode.gridPosition.x, startNodeY = startNode.gridPosition.y;
int goalNodeX = goalNode.gridPosition.x, goalNodeY = goalNode.gridPosition.y;
int xDistn = Mathf.Abs(goalNode.gridPosition.x - startNode.gridPosition.x);
int yDistn = Mathf.Abs(goalNode.gridPosition.y - startNode.gridPosition.y);
float deltaX, deltaY;
if (xDistn >= yDistn)
{
deltaX = 1;
deltaY = yDistn / (xDistn * 1.0f);
}
else
{
deltaY = 1;
deltaX = xDistn / (yDistn * 1.0f);
}
bool CheckNodeReachable(float fX, float fY)
{
int gX = Mathf.RoundToInt(fX);
int gY = Mathf.RoundToInt(fY);
if (Grid[gX, gY].CanReachTo(startNode))
{
newGoalNode = Grid[gX, gY];
return true;
}
else return false;
}
if (startNodeNodeX >= goalNodeX && startNodeY >= goalNodeY)//起点位于终点右上角
for (float x = goalNodeX + deltaX, y = goalNodeY + deltaY; x <= startNodeNodeX && y <= startNodeY; x += deltaX, y += deltaY)
{
if (CheckNodeReachable(x, y)) break;
}
else if (startNodeNodeX >= goalNodeX && startNodeY <= goalNodeY)//起点位于终点右下角
for (float x = goalNodeX + deltaX, y = goalNodeY - deltaY; x <= startNodeNodeX && y >= startNodeY; x += deltaX, y -= deltaY)
{
if (CheckNodeReachable(x, y)) break;
}
else if (startNodeNodeX <= goalNodeX && startNodeY >= goalNodeY)//起点位于终点左上角
for (float x = goalNodeX - deltaX, y = goalNodeY + deltaY; x >= startNodeNodeX && y <= startNodeY; x -= deltaX, y += deltaY)
{
if (CheckNodeReachable(x, y)) break;
}
else if (startNodeNodeX <= goalNodeX && startNodeY <= goalNodeY)//起点位于终点左下角
for (float x = goalNodeX - deltaX, y = goalNodeY - deltaY; x >= startNodeNodeX && y >= startNodeY; x -= deltaX, y -= deltaY)
{
if (CheckNodeReachable(x, y)) break;
}
return newGoalNode;
}
private AStarNode GetEffectiveGoalByRing(AStarNode startNode, AStarNode goalNode, int ringCount = 1)
{
var neighbours = GetSurroundingNodes(goalNode, ringCount);
if (ringCount >= Mathf.Max(GridSize.x, GridSize.y)) return null;//突破递归
AStarNode newGoalNode = neighbours.FirstOrDefault(x => x.CanReachTo(startNode));
if (newGoalNode)
using (var neighbourEnum = neighbours.GetEnumerator())
{
while (neighbourEnum.MoveNext())
if (neighbourEnum.Current == newGoalNode) break;
while (neighbourEnum.MoveNext())
{
AStarNode neighbour = neighbourEnum.Current;
if (Vector3.Distance(goalNode.worldPosition, neighbour.worldPosition) < Vector3.Distance(goalNode.worldPosition, newGoalNode.worldPosition))
if (neighbour.CanReachTo(startNode)) newGoalNode = neighbour;
}
}
if (!newGoalNode) return GetEffectiveGoalByRing(startNode, goalNode, ringCount + 1);
else return newGoalNode;
}
#endregion
#region 路径相关
private readonly Heap<AStarNode> openCache;
private readonly HashSet<AStarNode> closedCache;
private readonly List<AStarNode> pathCache;
public void FindPath(PathRequest request, Action<PathResult> callback)
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
AStarNode startNode = WorldPointToNode(request.start);
AStarNode goalNode = WorldPointToNode(request.goal);
if (startNode == null || goalNode == null)
{
stopwatch.Stop();
callback(new PathResult(null, false, request.callback));
return;
}
IEnumerable<Vector3> pathResult = null;
bool findSuccessfully = false;
if (!goalNode.walkable)
{
goalNode = GetClosestSurroundingNode(goalNode, startNode);
if (goalNode == default)
{
stopwatch.Stop();
callback(new PathResult(null, false, request.callback));
//Debug.Log("找不到合适的终点" + request.goal);
return;
}
}
if (!startNode.walkable)
{
startNode = GetClosestSurroundingNode(startNode, goalNode);
if (startNode == default)
{
stopwatch.Stop();
callback(new PathResult(null, false, request.callback));
//Debug.Log("找不到合适的起点" + request.start);
return;
}
}
if (startNode == goalNode)
{
stopwatch.Stop();
callback(new PathResult(null, false, request.callback));
//Debug.Log("起始相同");
return;
}
if (!startNode.CanReachTo(goalNode))
{
goalNode = GetEffectiveGoal(startNode, goalNode);
if (!goalNode || !startNode.CanReachTo(goalNode))
{
stopwatch.Stop();
//Debug.Log("检测到目的地不可到达");
callback(new PathResult(pathResult, false, request.callback));
return;
}
}
if (CanGoStraight(startNode, goalNode))
{
findSuccessfully = true;
pathResult = new Vector3[] { goalNode };
stopwatch.Stop();
//Debug.Log("耗时 " + stopwatch.ElapsedMilliseconds + "ms,通过直走找到路径");
}
if (!findSuccessfully)
{
openCache.Clear();
closedCache.Clear();
openCache.Add(startNode);
while (openCache.Count > 0)
{
AStarNode current = openCache.RemoveRoot();
if (current == default || current == null)
{
callback(new PathResult(null, false, request.callback));
return;
}
closedCache.Add(current);
if (current == goalNode)
{
findSuccessfully = true;
stopwatch.Stop();
//Debug.Log("耗时 " + stopwatch.ElapsedMilliseconds + "ms,通过搜索 " + closedList.Count + " 个结点找到路径");
break;
}
using (var nodeEnum = GetReachableNeighbours(current).GetEnumerator())
while (nodeEnum.MoveNext())
{
AStarNode neighbour = nodeEnum.Current;
if (!neighbour.walkable || closedCache.Contains(neighbour)) continue;
int costStartToNeighbour = current.GCost + current.CalculateHCostTo(neighbour);
if (costStartToNeighbour < neighbour.GCost || !openCache.Contains(neighbour))
{
neighbour.GCost = costStartToNeighbour;
neighbour.HCost = neighbour.CalculateHCostTo(goalNode);
neighbour.parent = current;
if (!openCache.Contains(neighbour))
openCache.Add(neighbour);
else openCache.Update();
}
}
}
if (!findSuccessfully)
{
stopwatch.Stop();
//Debug.Log("耗时 " + stopwatch.ElapsedMilliseconds + "ms,搜索 " + closedList.Count + " 个结点后找不到路径");
}
else
{
stopwatch.Start();
AStarNode pathNode = goalNode;
pathCache.Clear();
while (pathNode != startNode)
{
pathCache.Add(pathNode);
AStarNode temp = pathNode;
pathNode = pathNode.parent;
temp.parent = null;
temp.HeapIndex = -1;
}
pathResult = GetWaypoints(pathCache);
stopwatch.Stop();
//Debug.Log("耗时 " + stopwatch.ElapsedMilliseconds + "ms,通过搜索 " + closedList.Count + " 个结点后取得实际路径");
}
}
callback(new PathResult(pathResult, findSuccessfully, request.callback));
}
public bool TryGetPath(Vector3 start, Vector3 goal, out IEnumerable<Vector3> pathResult)
{
//略
}
public bool TryGetPath(Vector3 start, Vector3 goal)
{
//略
}
private List<Vector3> GetWaypoints(List<AStarNode> path)
{
List<Vector3> waypoints = new List<Vector3>();
if (path.Count < 1) return waypoints;
PathToWaypoints();
StraightenPath();
waypoints.Reverse();
return waypoints;
void PathToWaypoints(bool simplify = true)
{
if (simplify)
{
Vector2 oldDir = Vector3.zero;
for (int i = 1; i < path.Count; i++)
{
Vector2 newDir = path[i - 1].gridPosition - path[i].gridPosition;
if (newDir != oldDir)//方向不一样时才使用前面的点
waypoints.Add(path[i - 1]);
else if (i == path.Count - 1) waypoints.Add(path[i]);//即使方向一样,也强制把起点也加进去
oldDir = newDir;
}
}
else foreach (AStarNode node in path)
waypoints.Add(node);
}
void StraightenPath()
{
if (waypoints.Count < 1) return;
//System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
//stopwatch.Start();
List<Vector3> toRemove = new List<Vector3>();
Vector3 from = waypoints[0];
for (int i = 2; i < waypoints.Count; i++)
if (CanGoStraight(from, waypoints[i]))
toRemove.Add(waypoints[i - 1]);
else from = waypoints[i - 1];
foreach (Vector3 point in toRemove)
waypoints.Remove(point);
//stopwatch.Stop();
//Debug.Log("移除 " + toRemove.Count + " 个导航点完成路径直化");
}
}
#endregion
#region 四邻域连通检测算法
private readonly Dictionary<int, HashSet<AStarNode>> Connections = new Dictionary<int, HashSet<AStarNode>>();
private void CalculateConnections()
{
Connections.Clear();//重置连通域字典
int[,] gridData = new int[GridSize.x, GridSize.y];
for (int x = 0; x < GridSize.x; x++)
for (int y = 0; y < GridSize.y; y++)
if (Grid[x, y].walkable) gridData[x, y] = 1;//大于0表示可行走
else gridData[x, y] = 0;//0表示有障碍
int label = 1;
for (int y = 0; y < GridSize.y; y++)//从下往上扫
{
for (int x = 0; x < GridSize.x; x++)//从左往右扫
{
//若该数据的标记不为0,即该数据表示的位置没有障碍
if (gridData[x, y] != 0)
{
int labelNeedToChange = 0;//记录需要更改的标记
if (y == 0)//第一行,只用看当前数据点的左边
{
//若该点是第一行第一个,前面已判断不为0,直接标上标记
if (x == 0)
{
gridData[x, y] = label;
label++;
}
else if (gridData[x - 1, y] != 0)//若该点的左侧数据的标记不为0,那么当前数据的标记标为左侧的标记,表示同属一个连通域
gridData[x, y] = gridData[x - 1, y];
else//否则,标上新标记
{
gridData[x, y] = label;
label++;
}
}
else if (x == 0)//网格最左边,不可能出现与左侧形成衔接的情况
{
if (gridData[x, y - 1] != 0) gridData[x, y] = gridData[x, y - 1]; //若下方数据不为0,则当前数据标上下方标记的标记
else
{
gridData[x, y] = label;
label++;
}
}
else if (gridData[x, y - 1] != 0)//若下方标记不为0
{
gridData[x, y] = gridData[x, y - 1];//则用下方标记来标记当前数据
if (gridData[x - 1, y] != 0) labelNeedToChange = gridData[x - 1, y]; //若左方数据不为0,则被左方标记所标记的数据都要更改
}
else if (gridData[x - 1, y] != 0)//若左侧不为0
gridData[x, y] = gridData[x - 1, y];//则用左侧标记来标记当前数据
else
{
gridData[x, y] = label;
label++;
}
if (!Connections.ContainsKey(gridData[x, y])) Connections.Add(gridData[x, y], new HashSet<AStarNode>());
Connections[gridData[x, y]].Add(Grid[x, y]);
//将对应网格结点的连通域标记标为当前标记
Grid[x, y].connectionLabel = gridData[x, y];
//Struct.Grid.CoverOrInsert(x, y, Grid[x, y].Structed);//更新结构体
//如果有需要更改的标记,且其与当前标记不同
if (labelNeedToChange > 0 && labelNeedToChange != gridData[x, y])
{
foreach (AStarNode node in Connections[labelNeedToChange])//把对应连通域合并到当前连通域
{
gridData[node.gridPosition.x, node.gridPosition.y] = gridData[x, y];
Connections[gridData[x, y]].Add(node);
node.connectionLabel = gridData[x, y];
//Struct.Grid.CoverOrInsert(x, y, node.Structed);
}
Connections[labelNeedToChange].Clear();
Connections.Remove(labelNeedToChange);
}
}
}
}
}
private bool ACanReachB(AStarNode nodeA, AStarNode nodeB)
{
if (!nodeA || !nodeB) return false;
var first = Connections.FirstOrDefault(x => x.Value.Contains(nodeA));
if (default(KeyValuePair<string, AStarNode>).Equals(first)) return false;
var second = Connections.FirstOrDefault(x => x.Value.Contains(nodeB));
if (default(KeyValuePair<string, AStarNode>).Equals(second)) return false;
return first.Key == second.Key;
}
#endregion
public static implicit operator bool(AStar self)
{
return self != null;
}
}
该类的核心是FindPath()算法,通过传入一个寻路请求和寻路回调来计算和反馈路径,而拓展方法TryGetPath()则可有可无,我也不贴上来了,复制粘贴前者稍改一下就行了。FindPath()开头,首先检查起点和终点是否都是可行走的,如果不行就都换个有效的。由于基本A*算法存在一个致命性的弱点,那就是当终点被封闭空间包围时,算法会搜索所有结点来寻找路径,却找不到可用路径,极其浪费资源,为了解决这个情况,我想到了用连通域来检查起点和终点是否连通,如果不连通,说明起点或终点是被包围起来了的,所以用到了四邻域联通检测算法CalculateConnections(),给同个连通域的结点标识一样的连通域编号,所以当两个结点的连通域编号一样时,互相可达,反之就不行。所以,在寻路算法的接下来一步,用一个CanReachTo()来检查连通性,如果不能连通,则根据自己的选择,退出算法或再另找一个有效可达的终点。再下去,检查起点可否直走到终点,如果可以就不用浪费时间去计算路径了,直接过去就行了。起点和终点的处理就到此为止了,接下来就是A*算法的基本内容了,没什么好解释的。
其中,寻路请求和回馈都是结构体,如下:
public struct PathRequest
{
public readonly Vector3 start;
public readonly Vector3 goal;
public readonly Vector2Int unitSize;
public readonly Action<IEnumerable<Vector3>, bool> callback;
public PathRequest(Vector3 start, Vector3 goal, Vector2Int unitSize, Action<IEnumerable<Vector3>, bool> callback)
{
this.start = start;
this.goal = goal;
this.unitSize = unitSize;
this.callback = callback;
}
}
public struct PathResult
{
public readonly IEnumerable<Vector3> waypoints;
public readonly bool findSuccessfully;
public readonly Action<IEnumerable<Vector3>, bool> callback;
public PathResult(IEnumerable<Vector3> waypoints, bool findSuccessfully, Action<IEnumerable<Vector3>, bool> callback)
{
this.waypoints = waypoints;
this.findSuccessfully = findSuccessfully;
this.callback = callback;
}
}
到此,已经完成大半内容了,这时候就要用一个mono来处理请求和反馈,我管这玩意儿叫AStarManager,实现如下:
public class AStarManager : MonoBehaviour
{
private static AStarManager instance;
public static AStarManager Instance
{
get
{
if (!instance || !instance.gameObject)
instance = FindObjectOfType<AStarManager>();
return instance;
}
}
#region Gizmos相关
[SerializeField]
private bool gizmosEdge = true;
[SerializeField]
private Color edgeColor = Color.white;
[SerializeField]
private bool gizmosGrid = true;
[SerializeField]
private Color gridColor = new Color(Color.white.r, Color.white.g, Color.white.b, 0.15f);
[SerializeField]
private bool gizmosCast = true;
[SerializeField]
private Color castColor = new Color(Color.white.r, Color.white.g, Color.white.b, 0.1f);
#endregion
[SerializeField, Tooltip("长和宽都推荐使用2的幂数")]
private Vector2 worldSize = new Vector2(48, 48);
[SerializeField]
private bool threeD;
public bool ThreeD
{
get
{
return threeD;
}
}
[SerializeField, Range(0.2f, 2f)]
private float baseCellSize = 1;
public float BaseCellSize
{
get
{
return baseCellSize;
}
}
[SerializeField]
private float worldHeight = 20.0f;
[SerializeField]
private LayerMask groundLayer = ~0;
[SerializeField, Tooltip("以单元格倍数为单位,至少是 1 倍,2D空间下 Y 数值无效")]
private Vector2Int[] unitSizes;
[SerializeField]
private LayerMask unwalkableLayer = ~0;
public LayerMask UnwalkableLayer
{
get
{
return unwalkableLayer;
}
}
[SerializeField, Tooltip("以单元格倍数为单位"), Range(0.25f, 0.5f)]
private float castRadiusMultiple = 0.5f;
[SerializeField]
#if UNITY_EDITOR
[EnumMemberNames("方形[精度较低]", "球形[性能较低]")]
#endif
private CastCheckType castCheckType = CastCheckType.Box;
[SerializeField, Tooltip("当终点无法到达时,如何选取近似有效终点?")]
#if UNITY_EDITOR
[EnumMemberNames("不选取", "按圈选取", "直线选取")]
#endif
private GoalSelectMode goalSelectMode = GoalSelectMode.ByRing;
#region 实时变量
public Dictionary<Vector2Int, AStar> AStars { get; private set; } = new Dictionary<Vector2Int, AStar>();
private readonly Queue<PathResult> results = new Queue<PathResult>();
#endregion
#region 网格相关
public AStar GetAStar(Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1) return null;
if (!AStars.ContainsKey(unitSize))
{
CreateAStar(unitSize);
}
AStars[unitSize].RefreshGrid();
return AStars[unitSize];
}
private void CreateAStars()
{
foreach (Vector2Int unitSize in unitSizes)
CreateAStar(unitSize);
}
public void CreateAStar(Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1 || AStars.ContainsKey(unitSize)) return;
//System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
//stopwatch.Start();
Vector3 axisOrigin;
if (!ThreeD)
axisOrigin = new Vector3(Mathf.RoundToInt(transform.position.x), Mathf.RoundToInt(transform.position.y), 0)
- Vector3.right * (worldSize.x / 2) - Vector3.up * (worldSize.y / 2);
else
axisOrigin = new Vector3Int(Mathf.RoundToInt(transform.position.x), 0, Mathf.RoundToInt(transform.position.z))
- Vector3.right * (worldSize.x / 2) - Vector3.forward * (worldSize.y / 2);
AStar AStar = new AStar(worldSize, BaseCellSize * unitSize.x, castCheckType, castRadiusMultiple, UnwalkableLayer, groundLayer,
ThreeD, worldHeight, unitSize.y, goalSelectMode);
AStar.CreateGrid(axisOrigin);
AStars.Add(unitSize, AStar);
//stopwatch.Stop();
//Debug.Log("为规格为 " + unitSize + " 的单位建立寻路基础,耗时 " + stopwatch.ElapsedMilliseconds + "ms");
}
public void UpdateAStars()
{
foreach (AStar AStar in AStars.Values)
{
AStar.RefreshGrid();
}
}
public void UpdateAStars(Vector3 fromPoint, Vector3 toPoint)
{
foreach (AStar AStar in AStars.Values)
{
AStar.RefreshGrid(fromPoint, toPoint);
}
}
public void UpdateAStar(Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1 || !AStars.ContainsKey(unitSize)) return;
AStars[unitSize].RefreshGrid();
}
public void UpdateAStar(Vector3 fromPoint, Vector3 toPoint, Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1 || !AStars.ContainsKey(unitSize)) return;
AStars[unitSize].RefreshGrid(fromPoint, toPoint);
}
public AStarNode WorldPointToNode(Vector3 position, Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1) return null;
if (!AStars.ContainsKey(unitSize))
{
CreateAStar(unitSize);
}
return AStars[unitSize].WorldPointToNode(position);
}
public bool WorldPointWalkable(Vector3 point, Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1) return false;
if (!AStars.ContainsKey(unitSize))
{
CreateAStar(unitSize);
}
return AStars[unitSize].WorldPointWalkable(point);
}
#endregion
#region 路径相关
public bool TryGetPath(Vector3 startPos, Vector3 goalPos, Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1)
{
return false;
}
if (!AStars.ContainsKey(unitSize))
{
CreateAStar(unitSize);
}
return AStars[unitSize].TryGetPath(startPos, goalPos);
}
public bool TryGetPath(Vector3 startPos, Vector3 goalPos, out IEnumerable<Vector3> pathResult, Vector2Int unitSize)
{
if (unitSize.x < 1 || unitSize.y < 1)
{
pathResult = null;
return false;
}
if (!AStars.ContainsKey(unitSize))
{
CreateAStar(unitSize);
}
return AStars[unitSize].TryGetPath(startPos, goalPos, out pathResult);
}
public void RequestPath(PathRequest request)
{
if (!AStars.ContainsKey(request.unitSize))
{
CreateAStar(request.unitSize);
}
lock (AStars[request.unitSize])
((ThreadStart)delegate
{
AStars[request.unitSize].FindPath(request, GetResult);
}).Invoke();
}
public void GetResult(PathResult result)
{
lock (results)
{
results.Enqueue(result);
}
}
#endregion
#region MonoBehaviour
private void Awake()
{
CreateAStars();
StarCoroutine(HandlingResults());
}
private IEnumerator HandlingResults()
{
while(true)
{
if (results.Count > 0)
{
int resultsCount = results.Count;
lock (results)
for (int i = 0; i < resultsCount; i++)
{
PathResult result = results.Dequeue();
result.callback(result.waypoints, result.findSuccessfully);
}
}
yield return null;
}
}
private void OnDrawGizmos()
{
if (gizmosGrid)
{
if (AStars != null && AStars.ContainsKey(Vector2Int.one))
{
AStarNode[,] Grid = AStars[Vector2Int.one].Grid;
for (int x = 0; x < AStars[Vector2Int.one].GridSize.x; x++)
for (int y = 0; y < AStars[Vector2Int.one].GridSize.y; y++)
{
Gizmos.color = gridColor;
if (!Grid[x, y].walkable)
{
Gizmos.color = new Color(Color.red.r, Color.red.g, Color.red.b, gridColor.a);
Gizmos.DrawCube(Grid[x, y], ThreeD ? Vector3.one : new Vector3(1, 1, 0) * BaseCellSize * 0.95f);
}
else if (!ThreeD) Gizmos.DrawWireCube(Grid[x, y], new Vector3(1, 1, 0) * BaseCellSize);
else Gizmos.DrawCube(Grid[x, y], Vector3.one * BaseCellSize * 0.95f);
if (gizmosCast)
{
Gizmos.color = castColor;
if (castCheckType == CastCheckType.Sphere) Gizmos.DrawWireSphere(Grid[x, y], BaseCellSize * castRadiusMultiple);
else Gizmos.DrawWireCube(Grid[x, y], Vector3.one * BaseCellSize * castRadiusMultiple * 2);
}
}
}
else
{
Vector2Int gridSize = Vector2Int.RoundToInt(worldSize / BaseCellSize);
Vector3 nodeWorldPos;
Vector3 axisOrigin;
if (!ThreeD)
axisOrigin = new Vector3(Mathf.RoundToInt(transform.position.x), Mathf.RoundToInt(transform.position.y), 0)
- Vector3.right * (worldSize.x / 2) - Vector3.up * (worldSize.y / 2);
else
axisOrigin = new Vector3Int(Mathf.RoundToInt(transform.position.x), 0, Mathf.RoundToInt(transform.position.z))
- Vector3.right * (worldSize.x / 2) - Vector3.forward * (worldSize.y / 2);
for (int x = 0; x < gridSize.x; x++)
for (int y = 0; y < gridSize.y; y++)
{
Gizmos.color = gridColor;
if (!ThreeD)
{
nodeWorldPos = axisOrigin + Vector3.right * (x + 0.5f) * BaseCellSize + Vector3.up * (y + 0.5f) * BaseCellSize;
Gizmos.DrawWireCube(nodeWorldPos, new Vector3(1, 1, 0) * BaseCellSize);
}
else
{
nodeWorldPos = axisOrigin + Vector3.right * (x + 0.5f) * BaseCellSize + Vector3.forward * (y + 0.5f) * BaseCellSize;
float height;
if (Physics.Raycast(nodeWorldPos + Vector3.up * (worldHeight + 0.01f), Vector3.down, out RaycastHit hit, worldHeight + 0.01f, groundLayer))
height = hit.point.y;
else height = worldHeight + 0.01f;
if (height > worldHeight) Gizmos.color = new Color(Color.red.r, Color.red.g, Color.red.b, gridColor.a);
nodeWorldPos += Vector3.up * height;
Gizmos.DrawCube(nodeWorldPos, Vector3.one * BaseCellSize * 0.95f);
}
if (gizmosCast)
{
Gizmos.color = castColor;
if (castCheckType == CastCheckType.Sphere) Gizmos.DrawWireSphere(nodeWorldPos, BaseCellSize * castRadiusMultiple);
else Gizmos.DrawWireCube(nodeWorldPos, Vector3.one * BaseCellSize * castRadiusMultiple * 2);
}
}
}
}
if (gizmosEdge)
{
Gizmos.color = edgeColor;
if (ThreeD) Gizmos.DrawWireCube(transform.position + Vector3.up * worldHeight / 2, new Vector3(worldSize.x, worldHeight, worldSize.y));
else Gizmos.DrawWireCube(transform.position, new Vector3(worldSize.x, worldSize.y, 0));
}
}
#endregion
}
这个类实际内容没有多少,也没什么值得解释的地方,只是Gizmos浪费了大量篇幅罢了。因为寻路网格没有必要实时更新,所以提供了UpdateXXX()方法来根据需要更新,比如说在场景上新创建了一个新的碰撞器,就需要更新一下了。稍微自定义了一下这个mono的检视器,成品是这样了:
有了寻路算法,就需要一个类似NavMeshAgent的组件来就行寻路,我起名叫AStarUnit,实现如下:
public class AStarUnit : MonoBehaviour
{
[SerializeField, Tooltip("以单元格倍数为单位,至少是 1 倍,2D空间下 Y 数值无效")]
private Vector2Int unitSize = Vector2Int.one;
[SerializeField]
private Vector3 footOffset;
[SerializeField, Tooltip("位置近似范围半径,最小建议值为0.1")]
private float fixedOffset = 0.125f;
[SerializeField]
private Transform target;
[SerializeField]
private Vector3 targetFootOffset;
[SerializeField, Tooltip("目标离开原位置多远之后开始修正跟随?")]
private float targetFollowStartDistance = 5;
[SerializeField]
#if UNITY_EDITOR
[EnumMemberNames("普通位移(推荐)[物理效果差]", "刚体位移[性能消耗高]", "控制器位移")]
#endif
private UnitMoveMode moveMode = UnitMoveMode.MovePosition;
[SerializeField]
private new Rigidbody rigidbody;
[SerializeField]
private new Rigidbody2D rigidbody2D;
[SerializeField]
private CharacterController controller;
public float moveSpeed = 10f;
public float turnSpeed = 10f;
[SerializeField]
private float slopeLimit = 45.0f;
[SerializeField]
public float stopDistance = 1;
[SerializeField]
public bool autoRepath = true;
[SerializeField]
private LineRenderer pathRenderer;
[SerializeField]
private Animator animator;
[SerializeField]
private string animaHorizontal = "Horizontal";
[SerializeField]
private string animaVertical = "Vertical";
[SerializeField]
private string animaMagnitude = "Move";
[SerializeField]
private bool drawGizmos;
#region 实时变量
private Vector3[] path;
private int targetWaypointIndex;
private Vector3 oldPosition;
private bool isFollowingPath;
public bool IsFollowingPath
{
get => isFollowingPath;
set
{
bool origin = isFollowingPath;
isFollowingPath = value;
if (!origin && isFollowingPath) StartFollowingPath();
else if (origin && !isFollowingPath)
{
if (pathFollowCoroutine != null) StopCoroutine(pathFollowCoroutine);
pathFollowCoroutine = null;
}
}
}
private Coroutine pathFollowCoroutine;
private bool isFollowingTarget;
public bool IsFollowingTarget
{
get => isFollowingTarget;
set
{
bool origin = isFollowingTarget;
isFollowingTarget = value;
if (!origin && isFollowingTarget) StartFollowingTarget();
else if (origin && !isFollowingTarget)
{
if (targetFollowCoroutine != null) StopCoroutine(targetFollowCoroutine);
targetFollowCoroutine = null;
IsFollowingPath = false;
}
}
}
private Coroutine targetFollowCoroutine;
private Vector3 gizmosTargetPos = default;
public bool HasPath
{
get
{
return path != null && path.Length > 0;
}
}
public bool IsStop => DesiredVelocity.magnitude == 0;
public Vector3 OffsetPosition
{
get { return transform.position + footOffset; }
private set { transform.position = value - footOffset; }
}
public Vector3 TargetPosition
{
get
{
if (target)
return target.position + targetFootOffset;
return OffsetPosition;
}
}
public Vector3 DesiredVelocity//类似于NavMeshAgent.desiredVelocity
{
get
{
if (path == null || path.Length < 1)
{
return Vector3.zero;
}
if (targetWaypointIndex >= 0 && targetWaypointIndex < path.Length)
{
Vector3 targetWaypoint = path[targetWaypointIndex];
if (!IsFollowingPath && OffsetPosition.x >= targetWaypoint.x - fixedOffset && OffsetPosition.x <= targetWaypoint.x + fixedOffset
&& (AStarManager.Instance.ThreeD ? true : OffsetPosition.y >= targetWaypoint.y - fixedOffset && OffsetPosition.y <= targetWaypoint.y + fixedOffset)
&& (AStarManager.Instance.ThreeD ? OffsetPosition.z >= targetWaypoint.z - fixedOffset && OffsetPosition.z <= targetWaypoint.z + fixedOffset : true))
//因为多数情况无法准确定位,所以用近似值表示达到目标航点
{
targetWaypointIndex++;
if (targetWaypointIndex >= path.Length) return Vector3.zero;
if (autoRepath)
if (!AStarManager.Instance.WorldPointWalkable(path[targetWaypointIndex], unitSize))
RequestPath(Destination);
}
if (targetWaypointIndex < path.Length)
{
if (AStarManager.Instance.ThreeD && MyUtilities.Slope(transform.position, path[targetWaypointIndex]) > slopeLimit)
return Vector3.zero;
if (Vector3.Distance(OffsetPosition, Destination) <= stopDistance)
{
ResetPath();
return Vector3.zero;
}
return (path[targetWaypointIndex] - OffsetPosition).normalized * moveSpeed;
}
}
return Vector3.zero;
}
}
public Vector3 Velocity => IsFollowingTarget || IsFollowingPath ? (OffsetPosition - oldPosition).normalized * moveSpeed : Vector3.zero;
private Vector3 Destination
{
get
{
if (path == null || path.Length < 1) return OffsetPosition;
return path[path.Length - 1];
}
}
#endregion
#region MonoBehaviour
private void Awake()
{
if (controller && moveMode == UnitMoveMode.MoveController) controller.slopeLimit = slopeLimit;
ShowPath(false);
}
private void Start()
{
//Debug only
SetTarget(target, targetFootOffset, true);
}
private void Update()
{
if (pathRenderer && path != null && path.Length > 0)
{
LinkedList<Vector3> leftPoint = new LinkedList<Vector3>(path.Skip(targetWaypointIndex).ToArray());
leftPoint.AddFirst(OffsetPosition);
pathRenderer.positionCount = leftPoint.Count;
pathRenderer.SetPositions(leftPoint.ToArray());
if (Vector3.Distance(OffsetPosition, Destination) < stopDistance) ShowPath(false);
}
if (animator)
{
Vector3 animaInput = DesiredVelocity.normalized;
if (animaInput.magnitude > 0)
{
animator.SetFloat(animaHorizontal, animaInput.x);
animator.SetFloat(animaVertical, animaInput.y);
}
animator.SetFloat(animaMagnitude, animaInput.magnitude);
}
}
private void LateUpdate()
{
oldPosition = OffsetPosition;
}
private void OnEnable()
{
if (!AStarManager.Instance) enabled = false;
oldPosition = OffsetPosition;
}
private void OnDrawGizmos()
{
if (drawGizmos)
{
if (path != null)
for (int i = targetWaypointIndex; i >= 0 && i < path.Length; i++)
{
if (i != path.Length - 1) Gizmos.color = new Color(Color.cyan.r, Color.cyan.g, Color.cyan.b, 0.5f);
else Gizmos.color = new Color(Color.green.r, Color.green.g, Color.green.b, 0.5f);
Gizmos.DrawSphere(path[i], 0.25f);
if (i == targetWaypointIndex)
Gizmos.DrawLine(OffsetPosition, path[i]);
else Gizmos.DrawLine(path[i - 1], path[i]);
}
if (gizmosTargetPos != default && AStarManager.Instance)
{
Gizmos.color = new Color(Color.black.r, Color.black.g, Color.black.b, 0.75f);
Gizmos.DrawCube(new Vector3(gizmosTargetPos.x, gizmosTargetPos.y, 0), Vector3.one * AStarManager.Instance.BaseCellSize * unitSize.x * 0.95f);
}
}
}
#endregion
#region 路径相关
public void SetPath(IEnumerable<Vector3> newPath, bool followImmediate = false)
{
if (newPath == null || !AStarManager.Instance) return;
ResetPath();
IsFollowingPath = followImmediate;
OnPathFound(newPath, true);
}
public void ResetPath()
{
path = null;
if (pathFollowCoroutine != null) StopCoroutine(pathFollowCoroutine);
pathFollowCoroutine = null;
targetWaypointIndex = 0;
isFollowingPath = false;
ShowPath(false);
gizmosTargetPos = default;
}
private void RequestPath(Vector3 destination)
{
if (!AStarManager.Instance || destination == OffsetPosition) return;
AStarManager.Instance.RequestPath(new PathRequest(OffsetPosition, destination, unitSize, OnPathFound));
}
private void OnPathFound(IEnumerable<Vector3> newPath, bool findSuccessfully)
{
if (findSuccessfully && newPath != null && newPath.Count() > 0)
{
path = newPath.ToArray();
if (pathRenderer)
{
LinkedList<Vector3> path = new LinkedList<Vector3>(this.path);
path.AddFirst(OffsetPosition);
pathRenderer.positionCount = path.Count;
pathRenderer.SetPositions(path.ToArray());
}
targetWaypointIndex = 0;
if (IsFollowingPath)
{
StartFollowingPath();
}
}
}
private void StartFollowingPath()
{
if (pathFollowCoroutine != null) StopCoroutine(pathFollowCoroutine);
pathFollowCoroutine = StartCoroutine(FollowPath());
}
private IEnumerator FollowPath()
{
if (path == null || path.Length < 1 || !IsFollowingPath)
{
yield break; //yield break相当于普通函数空return
}
Vector3 targetWaypoint = path[0];
while (IsFollowingPath)//模拟更新函数
{
if (path.Length < 1)//如果在追踪过程中,路线没了,直接退出追踪
{
ResetPath();
yield break;
}
if (OffsetPosition.x >= targetWaypoint.x - fixedOffset && OffsetPosition.x <= targetWaypoint.x + fixedOffset//因为很多情况下无法准确定位,所以用近似值
&& (AStarManager.Instance.ThreeD ? true : OffsetPosition.y >= targetWaypoint.y - fixedOffset && OffsetPosition.y <= targetWaypoint.y + fixedOffset)
&& (AStarManager.Instance.ThreeD ? OffsetPosition.z >= targetWaypoint.z - fixedOffset && OffsetPosition.z <= targetWaypoint.z + fixedOffset : true))
{
targetWaypointIndex++;//标至下一个航点
if (targetWaypointIndex >= path.Length) yield break;
targetWaypoint = path[targetWaypointIndex];
if (autoRepath)
if (!AStarManager.Instance.WorldPointWalkable(targetWaypoint, unitSize))//自动修复路线
{
//Debug.Log("Auto fix path");
RequestPath(Destination);
yield break;
}
}
if (AStarManager.Instance.ThreeD && MyUtilities.Slope(OffsetPosition, targetWaypoint) > slopeLimit) yield break;
if (Vector3.Distance(OffsetPosition, Destination) <= stopDistance)
{
ResetPath();
//Debug.Log("Stop");
ShowPath(false);
yield break;
}
if (moveMode == UnitMoveMode.MovePosition)
{
if (AStarManager.Instance.ThreeD)
{
Vector3 targetDir = new Vector3(DesiredVelocity.x, 0, DesiredVelocity.z).normalized;//获取平面上的朝向
if (!targetDir.Equals(Vector3.zero))
{
Quaternion targetRotation = Quaternion.LookRotation(targetDir, Vector3.up);//计算绕Vector3.up使transform.forward对齐targetDir所需的旋转量
Quaternion lerpRotation = Quaternion.Lerp(transform.rotation, targetRotation, turnSpeed * Time.deltaTime);//平滑旋转
transform.rotation = lerpRotation;
}
}
OffsetPosition = Vector3.MoveTowards(OffsetPosition, targetWaypoint, Time.deltaTime * moveSpeed);
yield return null;//相当于以上步骤在Update()里执行,至于为什么不直接放在Update()里,一句两句说不清楚,感兴趣的自己试一试有什么区别
}
else if (moveMode == UnitMoveMode.MoveRigidbody)
{
if (AStarManager.Instance.ThreeD && rigidbody)
{
Vector3 targetDir = new Vector3(DesiredVelocity.x, 0, DesiredVelocity.z).normalized;
if (!targetDir.Equals(Vector3.zero))
{
Quaternion targetRotation = Quaternion.LookRotation(targetDir, Vector3.up);
Quaternion lerpRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSpeed * Time.deltaTime);
rigidbody.MoveRotation(lerpRotation);
}
rigidbody.MovePosition(rigidbody.position + DesiredVelocity * Time.deltaTime);
}
else if (!AStarManager.Instance.ThreeD && rigidbody2D)
{
rigidbody2D.MovePosition((Vector3)rigidbody2D.position + DesiredVelocity * Time.deltaTime);
}
yield return new WaitForFixedUpdate();//相当于以上步骤在FiexdUpdate()里执行
}
else
{
if (AStarManager.Instance.ThreeD)
{
Vector3 targetDir = new Vector3(DesiredVelocity.x, 0, DesiredVelocity.z).normalized;
if (!targetDir.Equals(Vector3.zero))
{
Quaternion targetRotation = Quaternion.LookRotation(targetDir, Vector3.up);
Quaternion lerpRotation = Quaternion.Lerp(transform.rotation, targetRotation, turnSpeed * Time.deltaTime);
transform.rotation = lerpRotation;
}
controller.SimpleMove(DesiredVelocity - Vector3.up * 9.8f);
}
yield return new WaitForFixedUpdate();
}
}
}
public void ShowPath(bool value)
{
if (!pathRenderer) return;
pathRenderer.positionCount = 0;
pathRenderer.enabled = value;
}
#endregion
#region 目标相关
public void SetDestination(Vector3 destination, bool gotoImmediately = true)//类似NavMeshAgent.SetDestination(),但不建议逐帧使用
{
if (drawGizmos) gizmosTargetPos = destination;
if (!AStarManager.Instance) return;
if (drawGizmos)
{
AStarNode goal = AStarManager.Instance.WorldPointToNode(destination, unitSize);
if (goal) gizmosTargetPos = goal;
}
IsFollowingPath = gotoImmediately;
RequestPath(destination);
}
public void SetTarget(Transform target, Vector3 footOffset, bool followImmediately = false)
{
if (!target || !AStarManager.Instance) return;
this.target = target;
targetFootOffset = footOffset;
if (!followImmediately) SetDestination(TargetPosition, false);
else
{
isFollowingTarget = followImmediately;
StartFollowingTarget();
}
}
public void SetTarget(Transform target, bool followImmediately = false)
{
if (!target || !AStarManager.Instance) return;
this.target = target;
targetFootOffset = Vector3.zero;
if (!followImmediately) SetDestination(TargetPosition, false);
else
{
isFollowingTarget = followImmediately;
StartFollowingTarget();
}
}
public void ResetTarget()
{
target = null;
if (targetFollowCoroutine != null) StopCoroutine(targetFollowCoroutine);
targetFollowCoroutine = null;
IsFollowingTarget = false;
}
private void StartFollowingTarget()
{
if (targetFollowCoroutine != null) StopCoroutine(targetFollowCoroutine);
targetFollowCoroutine = StartCoroutine(FollowTarget());
}
private IEnumerator FollowTarget()
{
if (!target) yield break;
Vector3 oldTargetPosition = TargetPosition;
SetDestination(TargetPosition);
while (target)
{
yield return new WaitForSeconds(0.1f);
if (Vector3.Distance(oldTargetPosition, TargetPosition) > targetFollowStartDistance)
{
oldTargetPosition = TargetPosition;
SetDestination(TargetPosition);
}
}
}
#endregion
private enum UnitMoveMode
{
MovePosition,
MoveRigidbody,
MoveController
}
}
寻路单位的移动函数使用协程完成,方便中断,也不用为刚体移动和Transform移动分开写移动函数并分别放在FixedUpdate()和Update()调用。整个寻路系统最大的性能消耗在于FollowTarget()的MoveNext(),因为在里边调用了寻路函数,知识有限,目前还没有想到什么好的优化办法,归根到底是寻路算法巨额消耗的锅。Unity虽然可以支持多线程操作,但是别想使用UnityEngine里面的API了,各种红字报错,在底层它自己lock了,用Thread Ninja插件貌似可以飞起,但是没时间去研究了,而且是14年的插件,不知道现在管不管用,比较新的一个Easy Threading也是2年前的了,也没研究。因此,寻路性能成为问题,所以大佬们写的寻路插件都能解决了这个问题,卖这么贵是有道理的= =
这就是一直提到的寻路单位了,自定义了一下检视器后,成品如下:
寻路效果动图我就不贴上来了,放个静态图意思意思:
基础A*寻出来的路走起来折来折去的,转向太大,怪别扭的,估计后面我会优化一下。
就先记到这里了。又要告一段落了,不知道什么是才能再上来写博了╮( ̄▽ ̄")╭,如果不需要这么精确的寻路,可以稍微改一下Unit的代码,直接把目的地加入路径中,让Unit径直走过去,这种做法普遍适用于小怪,甚至不需要寻路算法,只不过整合起来用显得比较装逼而已。
理论上可行的优化方案:使用“池”技术,也就是广度优先给每个格子预先计算它到其它格子的路径,并使用带双重索引的数据结构存储起来,同时在计算指定格子到目标格子的路径时,如果该格子已经在其他某一格子到目标格子的路径上,则截取已知路径作为该格子到目标格子的路径,省去寻路计算,最后提供一个刷新函数来更新池子。而仅仅是30×30的网格就会有多达900个格子,可想而知,预处理这些内容在启动游戏时得有多卡_(:3J∠)_,所以也只是一个理论方案。
19/6/11补充:时至今日我才知道PathFinding Project有free版,只不过Asset Srore没上架而已,要去官网下载,功能足够用了,性能比我自己写的好太多太多,2D、3D都能用,也很容易融合(●´∀`●)