从坦克大战的砖墙到星际2中哨兵的力场,动态阻挡作为地图或关卡中的重要元素,在游戏开发中可以说是屡见不鲜。
不过由于算力和内存的限制,往往只能在单机游戏或一些地图偏小可以通过gridmap方式描述地图的游戏(如RTS、MOBA)中实现。
而对于MMORPG或者沙盘类的SLG游戏,情况则会复杂很多。
1.由于这类游戏的地图往往很大,用gridmap内存开销会很夸张(试想一下开一个1w * 1w的数组)。并且就算内存可以承受,基于这种规模的gridmap的a*搜索也几乎无法在有效搜索深度内给出可行解。
2.由于是大量玩家同时在线,无法仅加载地图的某一个部分(对MMO来说往往还需要同时加载多张地图)。而且需要保证不同玩家看到的移动行为基本一致,这就要求服务器和客户端之间的逻辑处理的一致性,很难通过纯客户端的一些trick的方法来实现效果
大地图的一般性寻路的解决方案:导航网格(更完整的介绍可以参考之前的专题)
核心思路就是尽可能的将地图中互相之间直线可达的区域合并为同一个凸边形,最后将地图处理成由若干个凸多边形构成的联通区域。在这个抽象出来的凸多边形集合上进行a*搜索,结点数量将会比直接用gridmap少非常多。
在RecastNavigation中,其实给出了一套处理动态阻挡的解决方案,核心思路是
1.将地图划分成若干网格状的tile(tile的尺寸比grid要大很多,比如grid是1 * 1的小格子,而tile往往是100 * 100或者更大尺寸的大网格)
2.以tile为单位,每个tile独立生产导航网格,然后再处理相邻tile之间的连通性
3.当加入或移除一个动态阻挡时,判断该阻挡与那些tile相交,然后重算这些相交的tile的导航网格,其他tile不处理。
这种方案可以满足一定的业务需求(比如哨兵的力场用这种方案应该很容易解决),但也有一些局限性:
1.重算导航网格的计算量远比gridmap修改标记位要复杂,虽然以tile为单位减小了影响的范围,但对于MMO或SLG这类有大量玩家同时在线操作的游戏来说,还是会成为计算瓶颈
2.动态阻挡的形状是提前定制的(比如标准的立方体、圆柱体),因为这种标准的凸多面体(多边形)计算相交的tile会比较容易。而想要支持更一般的形状的阻挡则比较困难
3.阻挡的尺寸不能小于体素块的尺寸,而对于规模比较大的地图来说,体素块尺寸也无法做的很小(比如8000 * 8000的地图,如果使用1 * 1 * 1的体素,在生成导航网格阶段内存可能直接就爆了)。加入你的游戏中需要有很薄的墙,就需要考虑清楚能不能在这种尺寸下生产导航网格。
笔者在实际的开发过程中针对不同的业务需求采用了一些方法来绕过上述这些局限性
需求情境1:
关隘类玩法(代表游戏《万国觉醒》)
沙盘地图被划分成9块独立的区域,区域内部可以自由行军,但区域之间的行军路线必须通过连接两块区域的关隘
并且如果关隘没有被占领,则无法通行
解决方案:
0.预处理:
a.不同区域之间在导航网格层面不联通,相同区域内部联通
b.每个关隘抽象为一对连接点,这对连接点记录了它们关联的两个相邻区域,它们之间的路径就是点到点的线段
c.属于同一片区域内部的连接点,根据按导航网格与计算出它们的联通路径。
此时所有相邻的点对之间的路径已经计算完毕
1.寻路分两层:
a.第一层首先判断起点和终点是否在同一片区域内,如果是则直接按导航网格寻路
b.如果是两个不同的区域,则枚举起始区域的所有被占领关隘的连接点和终点区域的所有被占领关隘的连接点,求它们之间的最短路。
c.由于这些连接点之间的直连边长度在预处理阶段已经提前算好,此时的寻路问题已经转换为普通的最短路问题,直接上prime算法即可
需求情景2:
桥梁玩法(代表游戏《红警》)
1.桥梁可以被炸毁、修复或占领
2.从河岸的一边移动到另一边不一定要经过桥梁(如果有更近的路,或者桥被炸毁也可以绕远路)
3.桥梁可以出现在地图的任意位置。
乍一看桥梁和关隘似乎差不多,但上面关隘的解决方案的前提是关隘只存在于不同区域的连接处,区域内部是保证联通的,而跨过区域的寻路必须经过关隘。在桥梁的情境中,它只是被当作一般性寻路时的备选项之一。
解决方案:
在RescastNavigation中提供了标签机制,可以在体素化阶段对指定范围的体素块进行标记,进而在最后生成的可行走面中,保存下这份标记值。
在a*搜索阶段,针对当前搜索到的可行走面(搜索结点),添加一个过滤函数,除了需要满足联通性之外,还需要满足过滤条件
在桥梁问题中,可以记录下寻路实体所占领的所有桥梁id,让后判断当前行走面记录的id是否在占领列表中,如果不在,则剔除这一块可行走面
需求情境3:
玩家可以在大地图上进行DIY建造建筑、围墙等,这些建筑会对地形产生影响,部队必须绕过这些建筑行军
从玩法和技术实现层面两个角度思考之后,我们对这样的DIY加了一些限制条件:
1.建造范围必须在玩家主城的一定范围内
2.多个玩家之间不能完全把路堵死
针对这两个条件,我们最后修改了建造规则:
1.每个玩家有一个圆形的建造范围圈
2.不同玩家的建造圈直接不能重叠(实际上相切也不行,有至少1个单位的外围宽度)
解决方案:
1.由于保证了不会完全把路堵死,所以地图上两点之间的连通性实际上没有变化(即如果原本通过导航网格可以寻路的点,在建造之后依然可以通行,只不过路径需要修正)
2.我们首先按导航网格算出一条初始路径。如果这条路径没有与任何主城圈相交,则可以直接作为最终的寻路路径
3.如果与主城圈发生了相交,则将相交的这一段线段替换为绕着主城圈的一段圆弧(保证路径不经过圈内部),当然相交可能不止一处,每一处按相同方式处理即可
4.当寻路的起始点和目标点在主城建造圈内部时,需要再进行特殊处理:
a.由于建造圈的范围一般在50 * 50以内,这时候gridmap就有用武之地了。
b.以主城所在位置为中心,为每个主城维护一个50 * 50的gridmap,当建造、移动、拆除建筑时,对该对应的gridmap的数据
c.当寻路中包含“出城”或“入城”时,将这一段路径单独拆分出来,通过针对gridmap的a*寻路计算路径,再拼接上剩余路径
5.到目前为止只是一条预算的路径,仅仅可以保证可达性,但绕行的行为不能保证是实际最优的。所以在每一次的坐标更新tick中,再判断一下当前移动中的物体是不是又走到的一个新的主城圈范围附近。当第一次走到新主城圈附件时,会立即触发一次重算,将剩余路径拆分成城内gridmap的a *+剩余部分的导航网格计算