一. 基础概念
grid map
- 2d-rts游戏一般不用物理引擎而用网格地图(比如:老古董wymsum,红警2好像也是)来实现地图功能,这里也使用网格地图,不用物理引擎和导航网格体
- 默认每个grid最多放一个单位,就不需要用rvo2避让算法,也不需要碰撞
- 基于grid map的寻路一般有寻路距离限制,地图大小限制,路太长图太大会有性能瓶颈,达不到”实时“的要求
a*寻路算法
- a*算法的结果并不是最优路径,但它的速度快
- 如果a*的目标点不可达,那a*会将能遍历的区域都遍历一遍,这将无法发挥它的优点,另外有的目标点需要a*遍历地图的大部分才能找到路线,这也是个性能问题
flow field pathfind 算法
- 有利于群体寻路,这个算法好像是主流
- 生成流场会从目标点开始广度优先遍历全图,将比a*更消耗性能,需要优化
二. 解决a*目标点不可达 带来的性能问题
设置 max_deep
max_deep为最大遍历次数,当超过该次数时,以当前遍历到的路线为最终路线,这样就改变了目标点的位置
新进/线程+回调
既然需要时间,那就等吧,别阻塞当前程序就行
动态标记哪些位置可达或不可达
并不好用
先看这种情况
一张地图上有几个岛(岛与岛之间没有陆地连接),其余都是海,单位无法移动到海中,只能在陆地上移动,A岛的单位自然也无法移动到B岛
这种情况较好解决,只要在游戏初始化好前(包括制作地图时)标记好每个grid属于A岛、B岛还是海。当目标点为属于海或其他岛时,该点则不可达。
如果策划非要到这个点,
- 单位位置与该点连一条线段,从靠近该点的一端开始遍历,将第一个可以到达的点更新为目标点
- 以该点为起始点广度优先遍历出第一个可以到达的点,并更新为目标点,这可能出现新的性能问题
单位环(仅idea)
接上,一个岛上有好几个单位围成一个环,中间有几块空地没有单位,这些空地就是无法到达的地方,这个问题我还没实现过,只有思路
可以在一帧当中的恰当时机,遍历所有单位生成环,在起始点和目标点连一条线段,线段与环的交点情况:
如果一个环与线段有连续的交点,将这些交点视为一个交点
如果一个环与线段有连续的交点,该段交点的两个端点,如果它们附近不在线段上的点都在线段同一侧,则将该段交点视为零个交点,否则视为一个交点
- 偶数个交点:目标点和起始点没有被该环包围,
可达
- 奇数个交点:目标点或起始点被环包围,不可达
三. 寻路地图构建
默认是grid map
这里需要用到一些游戏逻辑层的东西
单位移动引擎
坦克只能在陆地上行走,船只能在上面行走,直升机可以在任何地方行走,这几种单位所用的移动引擎各不相同
引擎对应的移动地图
一张grid map 地形图当中可能由:山、丘陵、河流、海
那么,对于坦克的引擎的移动地图,就要从地形图当中映射
def geo_mapper(v):
# 9999 表示不可到达
table = {山:1, 丘陵:1, 河流: 9999, 海: 9999}
return table.get(v)
得出一张坦克引擎的移动地图,a*就是依据这张地图来进行计算的
接受多个图层的影响
依据上面的mapper,坦克可以移动岛丘陵上,但当该丘陵上有一个单位时,由于一个grid最多放一个单位,该坦克就无法移动到丘陵上
这时可以再加一个单位图层,用来标记单位的位置,可以规定:0表示没有单位,其他值表示有单位,添加引擎对单位地图的更新函数
def unit_mapper(v):
return 0 if v == 0 else 9999
移动地图的更新
当地形图更改、单位移动、单位创建和单位摧毁都会局部更新移动地图,这个步骤不怎么消耗性能
def refresh_move_map(loc):
move_map[loc[1], loc[0]] = geo_mapper(geo_map[loc[1], loc[0]])
unit_map_v = unit_map[loc[1], loc[0]]
if unit_map_v != 0:
move_map[loc[1], loc[0]] = unit_mapper(unit_map_v)
单位移动时
由于每个grid最多放一个单位,当一个单位想从A点移动到它的邻居B点时,必须先将B点占有
(同时也可以将原来占有的A点释放
)。
有时移动地图在不断变化,单位的移动路线也需要在恰当的时机(比如:a*寻路前提下占有
失败时)自动更新
四. 用a*来群组寻路
群组中每个单位的目标位置的选择
- 计算群组中所有单位的坐标的中心点
center_loc = (average(x), average(y))
- 玩家想要移动的位置,即群组的目标点,
target_loc
- 流场方式下共用一个
target_loc
就行了,单位卡住一段时间后将视为到达目标,退出寻路,下面写的是不用流场而用a*+碰撞时(无法到达nextPoint)更新时的方案
方案一
各单位与center_loc
的差值(offset = loc[0]-center_loc[0], loc[1]-center_loc[1]
)就是改单位目标点与target_loc
的差值
这样算出的目标点可能是不可移动的区域,方案二解决了这个问题
方案二
以target_loc
为起始点广度优先遍历出几个可移动(而且要可到达)的点,数量与单位的数量一致,先把它们称为候选目标点吧。离target_loc
最远的单位应该移动到候选目标点中离center_loc
最近的点上
效果
如果在一帧中对组中所有单位进行寻路,会出现以下效果
![]() |
![]() |
![]() |
![]() |
所有引擎的移动地图的值全为1,不用管贴图
先看图1 图2
中间的蓝色坦克寻不到路,左边的两辆火箭车和上面的火箭车多走了一格
依据上面的单位移动相关内容,单位寻不到路时可以等一段时间再寻一次。这样就实现了基本的功能
目前我的优化方案效果并不明显:
离target_loc
越远的单位将等待更长的时间才开始寻路并移动,然后中间的坦克就跑到图3 中的左边去了,它打算绕个弯到达他的目标点
图4是其他单位都停止移动,左边蓝色坦克找不到路,乱窜
还是要用流场
五. flow field pathfind优化
构造流场时,会用广度优先将整张地图或整个岛遍历一遍得出一个矩阵(先这样称吧)群组中的所有单位都用这个流场来计算下一个移动点的位置。
当地图过大时(10000*10000),python创建一个numpy.ndarray就需要大概0.1s,pythonfor pass
一亿次大概要3s。一张10000*10000的numpy.ndarray占256MB。性能和内存都要优化
方案一
既然耗时,那就放到独立的线程中去吧,在计算出结果之前,单位就先卡在那里(或者用a*+max_deep算一条短点的路线给它走走)…似乎没有内存优化
方案二
- 对地图进行分块,先默认每块大小相同
- 模仿numpy.ndarray定义数据结构 BlockedNdarray(名字随便起的),专门用来存放流场
class BlockedNdarray:
def __init__(self, ...):
self.mapSize = ... # 移动地图的大小
self.blockMapSize = ... # 块的数量
self.blockSize = ... # 块的大小
self.storage: Dict[Tuple[int, int], numpy.ndarray] = {} # 存放块
def __setitem__(self, key, value):
...
def __getitem__(self, item):
...
这个数据结构是用来省内存的
- 遍历群组中的所有单位
for u in group
- 用a*+max_deep算出一条路线,到达不了目标点就将该单位目标点更新为当前计算出的路线的终点,标记该单位不使用群组的公共流场
- 当遇到第一个算出正确路线的单位时,计算该路线经过的地图块,将这些块周围(八个方向)的块也加入这些块中,将移动地图上位于块范围内的数值写入到BlockNdarray中,对BlockNdarray遍历来构造流场,访问时BlockNdarray中没有的块都视为障碍物,这个BlockNdarray存放的是组中所有单位共用的流场
- 算出第一条正确路线后,将之后的所有单位的寻路方式稍作修改:先判断该单位是否位于已存在的BlockNdarray流场中,如果存在,continue;
- 如果不存在,用a*+max_deep 对目标点寻路,每次遍历一个点,判断该点是否存在与BlockNdarray的流场中,如果找到了这样的一个点,终止遍历,将当前计算出的路线以3的方式对BlockNdarray进行更新,不同之处是3是从值为1的目标点开始遍历,而这次是从当前计算出的路线的终点开始遍历,遍历的初始值和初始位置都不同
- 如果没找到这样的一个点,用2的方式处理