外观
SplatoonInk
约 2238 字大约 7 分钟
2024-12-27
出于对《喷射战士》的兴趣,用 UE5 还原它最核心的"涂色"玩法做的一个技术 Demo:结合引擎已有的 UGC 编辑能力与 FPS 框架,把"在场景上喷涂、流动、结算占比"这一整套表现搭起来。这里记录拆解过程与实现思路,工程相关的内部资源、截图均已略去。
一、喷射战士 3 的表现分析
要还原,先得把游戏里"看起来理所当然"的表现拆成一条条可实现的规则。
静态表现
- 液体子弹溅射到地面/墙面上,会覆盖一层当前队伍颜色的涂层。双方颜色通常是一对近似互补、且都高饱和的颜色。
- 对局双方可以互相覆盖——同一块地面归属随时被改写。
- 玩家固定在某个点位、环绕一周喷射时,绘制出的表面呈放射状分布。
动态表现
垂直面流动:打在竖直墙壁上的液体会顺着墙往下流。
潜行留痕:角色可以潜入己方颜色中隐身,但移动时会在涂层上留下痕迹。
子弹轨迹与分裂:
- 子弹用一张纹理记录顶点偏移数据,每种武器的子弹独占一张顶点采样纹理;
- 部分子弹会在飞行途中分裂出一个小液体子弹块。
这里有个有意思的设计动机:很多武器是远程单发、射程较远。如果子弹不分裂,玩家为了铺出一条能"游过去"的墨汁通路,就得不断重复仰射、俯射。设计子弹分裂之后,可以快速铺出一条通往场地中心的路,让对战双方的冲突更快发生;从表现上,子弹分裂也更符合液体的特性——环绕喷射一周时能很快铺满四周。
喷射半径:大部分武器并不走标准抛物线,而是有一个"喷射半径"的设定——半径之内近似直线飞出,半径之外才快速衰减为自由落体。
二、UE 中的实现
整体思路:用一张 RenderTarget 记录全场景的涂色状态,绘制时把命中点映射到 RT 上的对应区域,着色时再把 RT 当作涂层信息去采样。下面分准备、绘制、流动、材质几块来说。
准备工作
RT 方案
一些还原参考工程会用 5 张 RT 分别记录前、后、左、右与地面的喷涂状态,每个方向内部再分层区分绘制区域,通过世界坐标(WP)传递绘制点位——这种方式对场景结构要求偏高。
这里改用单张 RT 记录全场景信息的方案:类似烘焙光照贴图,把场景里所有可喷涂的物体展开到同一张 RT 上。考虑到要支持 UGC 自由编辑,最终落在 2000×2000、RGB 格式(多出来的通道预留给后续扩展)。实测一张标准对战地图大概只会用到 16% 左右的面积,单张 RT 完全够用。
UV 预处理与分配
为了在绘制时准确知道"该画到 RT 的哪个位置",同时又能给玩家实时编辑试玩的能力,需要对模型做一些预处理:
- 统一展开:先看 2D 情况,对每个 2D 面片都按规则展开到正方形 UV 空间内,保证纹理密度一致;
- 跟随缩放:玩家在场景里摆放 Actor 时,面片对应的单位方形会根据 Actor 的缩放同步变成各种矩形,并记录其
Scale; - 打包到 RT:摆放完成后收集场景内所有可绘制 Actor,用 MaxRect 装箱算法填充 RT,记录每个 2D 图形在 RT 上的
Offset与Scale。
Auto Instance 与 Custom Primitive Data
在支持 UGC、需要承载大量可编辑物件的场景里,为了保持性能,通常会充分利用 UE 的 GPU Scene / Auto Instance 能力,并在材质层做相应改造:把部分可调参数通过 Custom Primitive Data 记录在每个 Mesh 实例上。
这样,区域颜色、一些材质表现相关的参数,都能通过 Custom Primitive Data 的结构暴露给关卡和 UGC 玩家。在材质实例数量可控的前提下,合批粒度和编辑自由度可以同时兼顾。本 Demo 里,前面提到的 Scale 和 Offset 也是通过 Custom Primitive Data 来保存和传递的——同时方便序列化存储与服务器同步。
绘制过程
绘制点的 UV 计算
当液体子弹命中可绘制物体时,可以拿到 Hit 信息,其中包含命中点的 UV。结合预处理阶段记录的 Scale 与 Offset,就能算出该画到 RT 的哪个区域,也就是 RT 对应的 UV。
这个 UV 还要再处理一下:一般我们会认为子弹命中点应当是液体笔刷的中心,所以要按贴图的 Width / Height 计算一个偏移,让结果和直觉一致。
接着求朝向:
- 根据 Hit 点的
Normal和Tangent求出BiTangent,构建 TBN 矩阵,转换到世界空间; - 用
Normal把子弹速度Velocity投影到 2D 物体所在平面,与上一步得到的向量点乘,得到旋转角度——这就是绘制笔刷(Brush)时的旋转角,确保液体能沿着子弹方向喷溅出去。
掠射角度与痕迹拉伸
除了朝向,还有一个掠射角度的设计。
喷射用的 Brush 有多种纹理用于随机,但本身不带尺寸变化。根据子弹速度和法线方向可以算出掠射角度:
- 用掠射角度与一个阈值比较,取用两套不同的 Brush 序列——接近垂直时用正方形纹理,接近平行(掠射)时用长方形纹理;
- 绘制到 RT 时,角度还会用来拉伸 Brush,让结果更符合流体被"甩"出去的特性。
流动表现
斜面流动
流动表现同样基于 DrawCanvas()。命中之后,根据 Hit 点以及由 Normal 与 Gravity 算出的斜面角度,在一段时间内反复绘制特殊的流动 Brush,每次绘制沿斜面步进一段距离,以此模拟液体往下流的过程。
朝向计算和前面类似,只是把 Velocity 换成 Gravity,得到 Brush 的旋转角度和步进方向。
实现上:绘制次数与斜面角度相关——Hit 之后生成一个 Handle 和对应的数据结构,设置好绘制次数,每帧步进一次,次数归零后从管理器里移除该 Handle。
材质表现
考虑到目标机型的性能预算,以及 TPS 多人玩法对帧率的要求(原作运行在性能有限的掌机上),涂层的光照计算其实并不复杂,这里也做了类似的简化还原:
- 从 RT 采样得到颜色区域后,把像素值当作高度(Height);
- 通过相邻像素的高度差计算
Normal; - 预先确定的颜色作为
BaseColor; - 再根据高度确定颜料区域的范围,边缘部分更亮一点,符合颜料堆积的观感。
弹道与战斗结算
对局结束后要统计双方颜色的占比;对战中也会根据脚下踩踏区域的归属给到 Buff / Debuff,所以需要实时读取 RT 的统计信息。
UE 自带的逐像素读取函数很慢——要把 RT 从 GPU 取回,再交给 CPU 处理。这里改为直接用 Compute Shader 在 GPU 上并行读取与统计,把结果直接写入特定的数据结构,再通过一次 GPUReadback 取回数据,提供给 UI 或玩法逻辑(GP)使用。
关卡编辑工具
参考的是《喷射战士 3》中一张结构相对简单的对战地图。喷射战士的对战地图大多是 2D 平面中心对称的,所以顺手做了一个一键对称的小工具来加速搭图。
小结
这个 Demo 的出发点很简单:喷射战士的画风、玩法都很有意思,正好可以把"RT 涂色 + UGC 编辑 + FPS 框架"几样东西拼到一起,组合出一套完整、可玩的涂色玩法。
整条链路里值得记住的几个点:
- 单张 RT + MaxRect 打包就足以承载一整张地图的涂色信息;
- 命中点 UV → RT UV 的映射,配合 TBN + 速度投影求朝向,是"喷溅方向正确"的关键;
- 掠射角度决定笔刷形状与拉伸,是液体感的来源;
- 流动用
Gravity替换Velocity复用同一套绘制逻辑; - 统计占比这类"读 RT"的需求,用 Compute Shader + GPUReadback 远比逐像素回读划算。
