掩体系统(上)
掩体系统在gameplay中是个复杂度较高的系统,其独立性较高的同时,也跟其他3C系统关联很大,并且不同游戏中做法也不一样。本文目的在于实现高弹性动作射击游戏的掩体系统。
掩体地形数据
掩体地形数据有些游戏是通过策划配置,编辑那些掩体哪个面,或者配置掩体位置的曲线表示哪里能进那些不能。这种做法虽然比较可控,但是制作非常繁琐。在大世界游戏中会有大量PCG内容,虽然部分掩体物体都是通过模型来构建,而通过赋予模型掩体数据来实现也不失为一种简单有效的方案,但是对面物体模型组合变体(如多个箱子叠在一起)难以处理,而且也没法处理地表掩体。
本文偏向于动态生成掩体数据,目的是能制作出在场景任何地方,符合掩体条件的地形均能进入掩体的一个动态系统。
游戏参考
对于复杂的模型,掩体碰撞数据是建议不要直接用其MeshCollision。理论上模型在任何引擎都应该生成两个碰撞数据:简单碰撞和复杂碰撞。复杂碰撞则是模型的MeshCollision,所见则所碰撞,对于射击游戏哪里子弹能穿过去,哪里不能,复杂碰撞就如此重要。而复杂碰撞面对人物移动时的碰撞是不适用的,除了性能考虑之外,复杂的物体对于人物移动来说是产生Bug的根源,包括遇到刁钻的角落会被卡住,或者人物IK在复杂模型表面乱动都有可能。简单碰撞则能解决上述问题,因为本身人物移动位置也不需要精确到模型细节表面,而是只考虑其概括表面,使用面数简化,甚至是简单模型体组合的简单碰撞也是游戏碰撞里必须的。
掩体也是同理,玩家在选择或者进入掩体时,并不会考虑掩体的细节表面,而是如果观察物体的概括表面,让它”看起来“像是掩体,就应该是掩体。而掩体移动时,表面上”看起来“应该能走,掩体就需要能走,而且掩体移动也必须避免尖刺、凹陷、角度刁钻的情况,否则掩体移动会非常不流畅。
Division2中进入掩体的时候会有个灰色透明的墙,可见掩体使用的是简单碰撞
Division2同一凳子模型掩体数据是一样的,掩体数据跟模型本身就是强绑定
掩体除了使用模型的碰撞数据,也必须有手工配置的需求,比如说有些镂空的掩体,只用射线检测来运行时判断的话有时会出乎意料,比如射线检测精度不够的话,凑巧把镂空的掩体当成实心掩体,进入这种掩体玩家会被射成筛子,体验极差,还不如进入此部分掩体进入。
Division2红框是可进掩体,黄色则不可进,除了使用动态检测,也需要有手工编辑的需求
整体玩下来Division2的掩体规则性特别明显,所以大概率是使用动态检测/动态生成的方式来查询掩体,手工编辑为辅来修正掩体。不过以我玩过的游戏,手工编辑掩体的游戏还是居多,不如说合金装备5,手工指定掩体的痕迹很明显。
MGS5似乎并不支持地形掩体,只支持对场景摆件的物体的掩体生成或者编辑。部分场景摆件如楼房,有部分墙面支持掩体进入部分则不支持,因此手工编辑的痕迹比较明显
MGS5的掩体墙的前面重叠了掩体摆件箱子的情况下爬上箱子是无法进入墙体掩体的,可见掩体点是生成在立足点到角色身高高度的区域,对于Navmesh数据这块是可行走区域,也没有明显的过滤特征,因此使用Navmesh做掩体数据可能性不大。
MGS5的掩体墙的前面重叠了掩体摆件箱子的情况下爬上箱子无法进入墙体掩体,估计几乎没用动态检测
子弹可穿透的掩体也能进去,掩体没有做任何的可穿透检测,掩体完全手动编辑的可能性很小因为这个掩体就完全没有加掩体数据的必要,推测掩体数据是基于模型的简单碰撞的三位网格的离线生成。
掩体系统架构
掩体系统大概分为三个部分:数据生成、运行检测、动画表现。
掩体数据生成方案一:3D Object Based Grid Generator
此方案用于收集掩体表面可以作为掩体进入的点,掩体点并非最准确的掩体点,只是作为一个进入掩体的参考,代表此处可以作为掩体站立。
此方案的原理是:通过一个正方体区域,按照一定的距离分布在3D空间上划分数个点,每两个相邻的点在Z轴打射线。为什么是Z轴?因为掩体除了能作为障碍阻挡子弹之外,能让人站在附近才是首要目标,障碍位置因为跟人物身高有关,一般是使用运行时检测。然后收集到所有碰撞到的点,通过一些自定义过滤,如离掩体模型过远过滤掉。这种方案简单粗暴,而且能处理多层掩体问题。
黄色为掩体内的点,所有相邻黄色的粉色的点都是最优掩体点,每个粉色点遍历四个方向即可
高度与倾斜处理:角色对于无法进入的角落,也是掩体需要考虑过滤的区域。Z轴的射线高度决定角色高度,无法到达地面的视为非掩体点。由于要兼顾多层结构的情况,所以不能只由一条角色高度的线来决定能否进入,从地面掩体点为起点,从下往上开始检测。通过分段按Z划分的射线,若射线的长度和大于角色高度,且所有射线均没有受到碰撞,则代表是可进入的掩体点
这种分段的做法还有一个好处,就是同一份离线数据能实时应用到不同身高的角色
这个方案整体来说可行,面对复杂的物体也能处理得很好,但是因为算法粗暴,效果也跟精度有关,对于掩体系统较复杂,场景较复杂和大世界都没法高效地跑起来。
情景 | 优 | 劣 |
---|---|---|
物体模型 | 按物体生成,可物体公用 | 物体越大性能越差,且不支持动态地形 |
三维点网格 | 轻松识别多层物体,倾斜物体 | Z轴划分小,精度才能上去,有性能要求 |
复杂转角物体 | 只要精度上去,就能处理转角不平滑问题 | - |
组合物体 | 支持组合物体,物体组合地面的情况下Z轴实时偏移即可 | 对于凹凸不平的地形鲁棒性较差,且不支持离线调试 |
掩体数据生成方案二:基于Navmesh
3DGrid最大缺点就是生成效率低。虽然考虑通过生成算法优化也未尝不可,不过在UE中也有个类似数据可以被我们利用——Navmesh。Navmesh数据本身需要生成,能重复利用减少数据量与生成时间,UE Navmesh本身支持运行时生成,可以做到动态生成掩体点。
本方案主要利用Navmesh的障碍物边缘。利用边缘做垂直探测,边的两顶点作为探测起初点和结束点。探测需要保持固定步长。
算法过程:获取NavMesh的所有边缘,将边划分为数份,每一份都从边的垂直方向取一点,向反向放发出射线(见下图最左侧水平射线)。然后分两次以一定角度和距离(见图CliffDistance)发射射线,若产生碰撞,则取碰撞点最低点为掩体点(如图CoverPoint);若未碰撞,则继续迭代(见下图蓝斜线)。这里的CliffDistance和角度均是为了避开掩体最上层的凸角(Cliff)。
下图是顶视图的分部射线检测,步长Step = EdgeSize / CoverCellDistance。其中EdgeSize是边长,CoverCellDistance是参数常量。
下图为上面算法的落地代码,在RecastNavMesh的回调,获取所有边的起点和端点。分段迭代传入起始点和迭代方向,根据上述过程做射线检测,如果边过短,则只处理起始点和端点。在RecastNavMesh中端点有可能遇到障碍物转角较大的情况,可通过45度的转角优化。
经过考虑我们最终使用基于NavMesh方案,其一避免性能问题,增加掩体划分区域工具和掩体排除组建,非排除的划分区域内物体才生成对应掩体;另外此方案生产环境比3DGrid要复杂得多,需要测试场景中多个物体组合的情况,可视化工具尤为重要,需要完善工具链供关卡编辑策划使用。而且由于几乎是全自动生成,手动编辑的地方较少,能减轻场景编辑工作量。
两种方案优劣如下:
3D Grid | NavMesh | |
---|---|---|
是否支持多层场景 | 是 | 是 |
是否支持地形 | 否 | 是 |
共用性 | 同一物体共用 | 场景高相关性,不共用 |
场景组合鲁棒性 | 较差 | 根据场景组合生成,极好 |
可编辑灵活性 | 可对共用物体进行编辑 | 可使用NavMesh编辑工具任意编辑 |
性能 | 根据场景大小程指数级增长 | 取决于NavMesh生成,取决于场景复杂度,场景大小影响不大 |
八叉树优化
由于掩体点的储存主键为位置,附带掩体法线,毋需体积形状,为加速运行时的查询,使用八叉树数据结构尤为合适。
八叉树的数据流主要分为两部分,创建部分(Generation Phase)和运行时部分(Runtime Phase),跟一般场景管理一样,创建部分是储存掩体数据点,运行部分是运行时查询掩体数据点,多说无益直接上数据流图吧:
然后呢我们的查询是使用AABB来作为输入,使用角色的AABB查询能在角色跟周边掩体互动的时候查询对应的数据,性能上几乎没有消耗,可配置这种查询也没有较大的精度需求,AABB足矣。
另外为处理需求AABB在角色不同的运动模式和速度情况下大小会不一样:
多线程优化
如果使用运行时生成掩体方案,生成过程肯定不能阻塞占用主线程时间,而应该在子线程执行逻辑。UE的AsyncTask能满足要求,使用NonAbandonableTask让系统自行分配线程,在后台执行即可。 在运行时创建掩体点的模式下,主线程的角色运行状况依赖角色当前附近创建的掩体点。主线程会轮询所请求的掩体数据,无则假设当前没有掩体,直到收到掩体数据处理。掩体点的数据会按区域划分防止掩体打分时造成的错误。
边界问题
NavMesh处理地形也有较好的效果。本身算法是基于NavMesh数据,而NavMesh在处理地形已经比较成熟。不过使用地形拓扑有时候会有非预期的情况,如高山的不规则导致掩体移动的不流畅,此时需要修改拓扑或者Navmesh。
有些模型的不规则,或者是模型内部的空区域,Reast有时候无法识别区域的合理性,甚至无法识别封闭区域,此时只能使用NavModifierVolume手动进行编辑
有些凹凸不平的地面,掩体点的生成的方向会很不规则,理论上这些地面是不可能作为掩体的。非掩体的碰撞,必须把掩体射线的Channel Ignore掉,否则会在掩体点生成时错误的掩体点,项目使用的NonUnits通道
本章赘述了掩体的地形数据(掩体点)生成流程。下一章会探讨掩体运行时的细节运作流程。