引入
流场寻路是一种基于向量场寻路的群体寻路算法。通过在网格中计算向量场,单位根据脚下向量场的方向移动即可渐渐向目标移动。因为所有要移动的单位共享一个向量场,计算一次就可以给所以单位导航,实现了群体寻路的低开销。
可以想象一个高低不同的沙盘,往盘中到水产生了流动(向量场),沙盘上棋子自然会向底处(目标)流去。
构建数据结构
先使用二维数组构建一个网格数据结构。要包含以下数据:
- 表示该网格是否可以通行(该项目中只有障碍物情况)。
- 该网格到目标的最短路径代价,不可通行设置为MAX值。
- 该网格包含的向量,指向代价最小的邻接位置。
|
|
标记障碍数据
可以使用unity的Physics.CheckBox检测是否和场景中物体有碰撞。最直接的方式是逐个网格检查后在obstacleField中标记。但是这样在100x100网格下就要做1万次检测了,静态场景无所谓,但是当场景会变化,要更新障碍信息时会非常消耗时间。
可以采用类似四叉树检测的方式,先在整个网格区域进行检查,没有碰撞全部标记为无障碍完成计算,有碰撞就将网格四等分,然后在每个等分的网格递归上述步骤,直到最终划分的网格小于等于每个网格的尺寸CellSize。
以下为具体实现:
|
|
我又另外写了个直接逐网格检测的函数就行对比。



可以看到,在1000x1000网格下,直接检测花了351ms,而四叉树优化版本花了12ms。提升了将近96.6%。
计算代价图
接下来要计算每个网格到目标的代价。可以从目标点开始,使用BFS广度优先遍历张网格。目标点代价为0,接下来访问其邻接节点,代价为之前访问的节点代价加1,整个网格遍历完成后即完成代价图的计算。
以下是实现:
|
|
因为只是涉及数组访问和数值计算,速度还是挺快,这里先不考虑优化。
计算向量图
向量场的计算非常简单,只要遍历所有网格,找到网格周围代价最小的邻接网格,再计算指向它的单位向量即可。
|
|
管理器
接下来,简单编写一个unity组件,用于管理该数据。它应该存储一个向量图的引用,负责其初始化,设置向量图目标以开始计算。
先在FieldGridSO添加相关函数并暴露。
|
|
然后是管理器类。
|
|
代理器
创建代理器代理单位的寻路。
|
|
相关优化
- 计算向量图可以使用unity job system进行优化多线程并行。
- 因为计算多多少少有点时间,可以使用异步等待计算完成而不必阻塞主线程。不过注意大部分unity的组件API的方法只能在主线程使用。所以使用了Physics.BoxCheck的的BakeObstacleField无法在后台进行或者多线程。
修改SetTarget为异步函数。
|
|
- 路径优化。直接使用向量图中的方向移动单位,单位间还有行动简单,碰撞和移动不平滑等问题。
- 因为单位可能同时在不同格子上,所以可以对周围格子的向量进行插值来让移动更平滑一点。
1 2 3 4 5 6 7 8 9 10//修改获取向量的函数,用周围四格来插值 int lx = intPos.x <= cellPos.x ? -1 : 1; int ly = intPos.y <= cellPos.y ? -1 : 1; float wx = (pos.x - cellPos.x) * 2/ CellSize; float wy = (pos.y - cellPos.y) * 2/ CellSize; Vector2 x1 = Vector2.Lerp(GetFlowField(intPos.x, intPos.y), GetFlowField(intPos.x + lx, intPos.y), wx); Vector2 x2 = Vector2.Lerp(GetFlowField(intPos.x, intPos.y + ly), GetFlowField(intPos.x + lx, intPos.y + ly), wx); //GetFlowField需要对越界进行处理,如返回回到网格的向量 Vector2 res = Vector2.Lerp(x1, x2, wy); return res.normalized;- Steering behavior。可以使用类似于Boid鸟群的思想。为壁障,避免单位碰撞分别计算一个力相加来完成这些行为。