2d-rts 大规模实时寻路方案

一. 基础概念

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
图2
图3
图3

所有引擎的移动地图的值全为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):
    	...

这个数据结构是用来省内存的

  1. 遍历群组中的所有单位for u in group
  2. 用a*+max_deep算出一条路线,到达不了目标点就将该单位目标点更新为当前计算出的路线的终点,标记该单位不使用群组的公共流场
  3. 当遇到第一个算出正确路线的单位时,计算该路线经过的地图块,将这些块周围(八个方向)的块也加入这些块中,将移动地图上位于块范围内的数值写入到BlockNdarray中,对BlockNdarray遍历来构造流场,访问时BlockNdarray中没有的块都视为障碍物,这个BlockNdarray存放的是组中所有单位共用的流场
  4. 算出第一条正确路线后,将之后的所有单位的寻路方式稍作修改:先判断该单位是否位于已存在的BlockNdarray流场中,如果存在,continue;
  5. 如果不存在,用a*+max_deep 对目标点寻路,每次遍历一个点,判断该点是否存在与BlockNdarray的流场中,如果找到了这样的一个点,终止遍历,将当前计算出的路线以3的方式对BlockNdarray进行更新,不同之处是3是从值为1的目标点开始遍历,而这次是从当前计算出的路线的终点开始遍历,遍历的初始值和初始位置都不同
  6. 如果没找到这样的一个点,用2的方式处理
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值