网络的部分搞定后,主要就是RTS的几大难点.
这里先复盘一下遇到的最大的坑:寻路系统
方案的选择:
1.基于路点寻路+A*算法的Unity Navigation
优点:优化非常好,上手很简单,使用了地图烘焙
缺点:
1.多角色之间的碰撞和挤压很难看且难以避免
如果是人型角色这倒没什么,谁没在CS里被挤过,但是一堆坦克挤来挤去就很酸爽了.这点亲身体会,另外可以参考这个帖子.
总之一点,这个东西对于单人或者对挤压没那么敏感的游戏可以,否则得深入底层改一些东西.
2.动态添加障碍物比如RTS这种能够随时随地建建筑,会产生一定bug
3.不适用于帧同步(浮点\碰撞)
2.现成的A*框架
性能开销过大,适用于单人或者少量角色.
*3.流场寻路算法(采用)
思路参考:
优点:大量单位公用同一份寻路结果节省了大量时间
PS.寻路远非线路规划这么简单,因为没有单位能够完全按照规划路线行进,每个单位之间会相互影响,这个影响逻辑怎么写是最麻烦的,这不在本文复盘内容之内.
一.建立网格系统
首先无论是A*还是流场寻路,都必须建立一个网格系统,网格系统听着复杂,其实就是写一些方法,将当前的Unity 转化成抽象的网格上的坐标.
比如我有一个位置Vector3(5,10.6,8),我想知道他在我抽象的网格世界的网格坐标是什么
就有了了Vector3Int currentGrid = Grid.FromWorldToCell(Vector3(5,10.6,8));
我这里的代码是用LStepLock(一个开源定点库)的LVector3,原理一样.
using Lockstep.Math;
using UnityEngine;
public class LGrid : MonoBehaviour
{
[SerializeField] private LFloat cellSize;
/// <summary>
/// 获得当前真实三维坐标对应的网格坐标
/// </summary>
/// <returns></returns>
public LVector3 LWorldToCell(LVector3 world)
{
return new LVector3(
LFloat.Divide(world.x, cellSize),
LFloat.Divide(world.y, cellSize),
LFloat.Divide(world.z, cellSize)
);
}
/// <summary>
/// 获得当前网格(左下角)的真实三位坐标中
/// </summary>
/// <returns></returns>
//默认网格初始位置0,0,0
public LVector3 LCellToWorld(LVector3Int cell)
{
return new LVector3(
(cell.x -1) * cellSize,
(cell.y) * cellSize,
(cell.z -1) * cellSize);
}
/// <summary>
/// 获得当前网格(中心点)的真实三位坐标
/// </summary>
/// <returns></returns>
public LVector3 LGetCellCenterWorld(LVector3 cell)
{
return new LVector3(
(cell.x-1)*cellSize+cellSize/2,
(cell.y)*cellSize,
(cell.z-1)*cellSize+cellSize/2
);
}
/// <summary>
/// 获得当前网格(中心点)的真实三位坐标*1000000L的坐标的值
/// </summary>
/// <param name="cell"></param>
/// <returns></returns>
public LVector3 LGetCellCenterWorld_s(LVector3 cell)
{
return new LVector3(
((cell._x-1)*cellSize+cellSize/2),
(cell._y)*cellSize,
(cell._z-1)*cellSize+cellSize/2
);
}
}
二.初始化导航地图
这个步骤相当复杂,但是简单来说就是建立一个bool[,]的二维地图,尺寸比如500x500,值为false表示障碍物,true表示可通行.
对于这次的复盘的任务而言就是传入一张这样的地图,起始点,终点,怎样找到一条从起点到终点的二维地图中的坐标数组
流场算法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Lockstep.Math;
using UnityEngine;
/// <summary>
/// 流场算法
/// </summary>
public class FlowFieldPath
{
private bool[,] grid;
public int[,] heatMap;
private int targetX, targetY;
private int[,] flowField;
public bool ifGenerateFineshed = false;
/// <summary>
/// 初始化流场寻路:传入导航地图
/// </summary>
/// <param name="grid"></param>
public FlowFieldPath(bool[,] grid)
{
this.grid = grid;
heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
for (int i = 0; i < heatMap.GetLength(0); i++)
{
for (int j = 0; j < heatMap.GetLength(1); j++)
{
heatMap[i, j] = int.MaxValue;
}
}
}
/// <summary>
/// 传入终点,根据重点生成导航热力图
/// 1.创建新的线程
/// </summary>
/// <param name="target">终点</param>
public void GenerateHeatMap(Vector2Int target)
{
new Thread(() => { GenerateHeatMap_T(target); }).Start();
}
/// <summary>
/// 传入终点,根据重点生成导航热力图
/// 2.正式开始生成热力图,生成的热力图
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
public int[,] GenerateHeatMap_T(Vector2Int target)
{
heatMap = new int[grid.GetLength(0), grid.GetLength(1)];
for (int i = 0; i < heatMap.GetLength(0); i++)
{
for (int j = 0; j < heatMap.GetLength(1); j++)
{
heatMap[i, j] = int.MaxValue;
}
}
this.targetX = target.x;
this.targetY = target.y;
Queue<Vector2Int> queue = new Queue<Vector2Int>();
queue.Enqueue(new Vector2Int(targetX, targetY));
heatMap[targetX, targetY] = 0;
while (queue.Count > 0)
{
Vector2Int current = queue.Dequeue();
List<Vector2Int> neighbors = GetNeighbors(current);
foreach (Vector2Int neighbor in neighbors)
{
if (grid[neighbor.x, neighbor.y])
{
int newHeat = heatMap[current.x, current.y] +
((neighbor.x == current.x || neighbor.y == current.y) ? 1 : 2);
if (newHeat < heatMap[neighbor.x, neighbor.y])
{
heatMap[neighbor.x, neighbor.y] = newHeat;
queue.Enqueue(neighbor);
}
}
}
}
ifGenerateFineshed = true;
return heatMap;
}
/// <summary>
/// 获取热力图某点的热力值
/// </summary>
/// <param name="loc"></param>
/// <returns></returns>
public int GetHeatMapPoint(LVector2Int loc)
{
return heatMap[loc.x, loc.y];
}
/// <summary>
/// 根据热力图生成流场图
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
public int[,] GenerateFlowField(Vector2Int target)
{
Debug.Log("确定流场目标:" + target.x + "," + target.y);
GenerateHeatMap(target);
flowField = new int[grid.GetLength(0), grid.GetLength(1)];
for (int i = 0; i < flowField.GetLength(0); i++)
{
for (int j = 0; j < flowField.GetLength(1); j++)
{
List<Vector2Int> neighbors = GetNeighbors(new Vector2Int(i, j));
int minHeat = int.MaxValue;
foreach (Vector2Int neighbor in neighbors)
{
if (grid[neighbor.x, neighbor.y] && heatMap[neighbor.x, neighbor.y] < minHeat)
{
minHeat = heatMap[neighbor.x, neighbor.y];
flowField[i, j] = DirectionToIndex(new Vector2Int(i, j), neighbor);
}
}
}
}
return flowField;
}
public int CheckDirection(LVector2Int loc)
{
return flowField[loc.x, loc.y];
}
private List<Vector2Int> GetNeighbors(Vector2Int point)
{
List<Vector2Int> neighbors = new List<Vector2Int>();
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0) continue; // 跳过自身
int nx = point.x + dx;
int ny = point.y + dy;
if (nx >= 0 && ny >= 0 && nx < grid.GetLength(0) && ny < grid.GetLength(1))
{
neighbors.Add(new Vector2Int(nx, ny));
}
}
}
return neighbors;
}
private int DirectionToIndex(Vector2Int current, Vector2Int target)
{
Vector2Int direction = target - current;
if (direction.x == 0 && direction.y == -1) return 0; // 上
if (direction.x == 1 && direction.y == -1) return 1; // 右上
if (direction.x == 1 && direction.y == 0) return 2; // 右
if (direction.x == 1 && direction.y == 1) return 3; // 右下
if (direction.x == 0 && direction.y == 1) return 4; // 下
if (direction.x == -1 && direction.y == 1) return 5; // 左下
if (direction.x == -1 && direction.y == 0) return 6; // 左
if (direction.x == -1 && direction.y == -1) return 7; // 左上
return -1;
}
public LVector2Int GetFlowFieldDirection(LVector2Int point)
{
int directionIndex = flowField[point.x, point.y];
LVector2Int direction = IndexToDirection(directionIndex);
return direction;
}
private LVector2Int IndexToDirection(int index)
{
switch (index)
{
case 0: return new LVector2Int(0, 1); // 上
case 1: return new LVector2Int(1, 1); // 右上
case 2: return new LVector2Int(1, 0); // 右
case 3: return new LVector2Int(1, -1); // 右下
case 4: return new LVector2Int(0, -1); // 下
case 5: return new LVector2Int(-1, -1); // 左下
case 6: return new LVector2Int(-1, 0); // 左
case 7: return new LVector2Int(-1, 1); // 左上
default: return new LVector2Int(0, 0); // 默认情况,没有方向
}
}
}