ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多人游戏可破坏环境落地:服务器权威、状态机与破坏预算

多人游戏可破坏环境落地:服务器权威、状态机与破坏预算

1. 拆解之前:破坏系统到底在解决什么问题

《The Finals》的破坏系统是我这两年反复拆得最多的一套东西。它最容易被误读的地方在于:很多人第一次看到大楼被整层削掉、地板塌下去、掩体被轰成碎渣,第一反应是"这游戏的物理引擎真猛"。但从工程角度看,真正猛的不是物理,而是一套被严格约束过的破坏规则——它把"看起来无所不能的破坏"压缩进了多人联网对局的性能预算和同步预算里。这事儿跟物理引擎的强弱关系不大,跟取舍关系极大。

我把这套系统的价值总结成一句话:它让"地图"从一张静态的图纸,变成了一个玩家可以参与改写的变量。你打穿一面墙,不是为了看碎块飞,而是为了绕开正面火力;你把楼板轰塌,不是为了炫技,而是为了让楼上的对手失去高度优势。破坏在这里不是特效,是战术资源。

这篇东西写给两类人看:一类是做多人射击、动作类项目的同行,想搞清楚"可破坏环境"在网络游戏里到底该怎么落地;另一类是关卡设计或者技术美术,正在纠结破碎资产怎么切、破坏预算怎么定。我不打算复述任何一场演讲的内容,只按我自己拆系统、搭原型的经验,把这类系统的骨架、坑和调参手感讲透。里面涉及具体数值的地方,有些是从公开资料里能推出来的量级,有些是我自己在项目里试出来的经验值,我会标清楚哪些是推断、哪些是实测。

1.1 "全场景可破坏"是个陷阱,先认清它的代价

新手做破坏系统最容易犯的错,是一上来就追求"所见即可破坏"。听着很爽,做起来是灾难。原因很简单:一旦每个物体都能碎,你就要为每个物体准备破碎资产、碰撞体、物理刚体、网络状态、音效、粒子、导航更新,而这些东西的成本不是线性叠加,是乘法叠加。一栋楼 200 面墙,每面墙碎成 8 块,就是 1600 个动态物体;如果每块还要参与刚体模拟和网络同步,帧率和对局稳定性直接崩。

《The Finals》的做法我判断是分层可破坏:地图里绝大部分结构是可破坏的,但破坏的粒度是预先设计好的。哪些墙能打穿、打穿之后碎成几块、打穿后露出的空间是否连通、塌陷之后会不会把通道堵死,全部在关卡制作阶段就定死了。玩家感受到的是"什么都能拆",实际运行时是"按剧本拆"。这中间的落差,就是技术团队省下来的全部性能。

我建议任何想做这套系统的团队,先在白盒阶段就画一张破坏地图:把场景里所有可破坏体标出来,按重要性分三级。一级是战术关键掩体,必须支持破坏且必须支持多点破坏;二级是装饰性结构,碎了只有视觉反馈不影响通行;三级是纯背景,干脆做成不可破坏的贴图。这张图决定了后面 80% 的工作量。

1.2 破坏在玩法层面真正起到的三个作用

我看过太多团队把破坏当成"加分项特效",结果做出来玩家根本不用。破坏要成为核心玩法,必须在机制上提供别的手段给不了的东西。按我的观察,这类系统在玩法上通常承担三件事。

第一件是改写掩体关系。射击游戏的攻防节奏本质上是掩体争夺,静态掩体玩久了就变成背点位。可破坏掩体把"点位"变成了"消耗品",你蹲的那堵墙撑不了三十秒,逼着你动起来。这一条对节奏的改善最直接。

第二件是制造垂直通路。地板能塌、天花板能破,意味着楼层之间不是靠楼梯连接的,而是可以被临时打通的。这让"上楼"这件事有了第二种解法,绕后路线的数量直接翻倍。但代价是导航和关卡连通性必须重新验证,后面我会专门讲。

第三件是提供信息扰动。破坏产生的烟尘、碎块、声音,本身就是一种遮挡和误导。会玩的玩家会故意打穿一面远处的墙,制造噪音掩盖自己的转移路线。这种"用破坏做假动作"的深度,是纯静态地图永远给不了的。

反过来说,如果一套破坏系统只做了第一件、没做第二第三件,那它在玩法上基本等于"换弹时顺手拆个墙",很快就会被玩家忽略。

1.3 认知切换:把它当状态机,别当物理仿真

这是我最想强调的一点。很多人一想到破坏,脑子里就是刚体、约束、断裂力学、有限元。真做联网游戏你会发现,破坏的物理仿真只需要在视觉层成立,逻辑层必须是状态机。

原因是对局需要确定性。同一个爆炸,服务器和三个客户端算出来的碎片位置如果不一样,那碎片就可能挡住某个玩家的视线或者子弹,产生"我看得到你你看不到我"的经典事故。而刚体仿真天然不可复现——浮点误差、迭代顺序、帧率差异,都会让结果漂移。

所以逻辑层我坚持只维护一件事:这个可破坏块现在是完整的,还是已经碎了。至于碎成什么样、飞到哪里去,交给表现层去自由发挥。这个切分一旦做对,后面所有的同步、回滚、性能优化都会变得简单;切分做错,你会陷入无穷无尽的"物理结果不一致"排查。

2. 架构拆解:服务器说了算的破坏

多人游戏里,破坏必须是服务器权威的。这不是选择,是底线。客户端能做的只有两件事:预测自己造成的破坏、播放别人造成的破坏。中间任何一步越权,都会变成作弊入口或者同步灾难。

2.1 三层结构:指令层、状态层、表现层

我把这类系统拆成三层,每层的职责边界必须画清楚,越界就是事故源头。

指令层负责"谁在什么时候对什么造成了多少伤害"。这一层的数据极小:一个目标 ID、一个伤害值、一个命中点坐标、一个方向向量。服务器收到后做校验——这个目标在不在合法射程内、伤害是否超过武器上限、玩家是否有视线——然后把结果交给状态层。这一层还负责处理伤害累积,因为大多数可破坏块不是一枪碎,而是要累积到阈值。

状态层是唯一的真相来源。它维护每个破坏块的当前血量、当前破碎层级、是否已经彻底消失。所有客户端看到的破坏结果,最终都要和这一层对齐。这一层的状态变更频率其实很低——一局比赛里真正被打碎的块可能也就几百个,相比每帧几千次的角色位置更新,这个量级几乎可以忽略。

表现层是纯本地的。它根据状态层下发的结果,决定播放哪种破碎效果、生成多少碎块、碎块往哪飞、烟尘多大、声音多响。这一层的所有东西都是"可以错的"——只要误差在玩家感知不到的范围里,就不需要修正。

我见过的最常见的架构错误,是把破碎逻辑写在表现层里,比如"播放破碎动画时顺便销毁碰撞体"。这样做的后果是,一旦动画被打断或者延迟,碰撞体就不知道什么时候该消失,玩家会撞到一堵"已经碎掉但还能挡人"的幽灵墙。

2.2 碎片为什么不进网络同步

这是整套系统里最省钱也最关键的一个决定。假设一次破坏生成 12 块碎片,一场激烈对局里同时存在 200 块碎片,每块碎片同步位置、旋转、速度,按每秒 20 次计算,就是 200 × 20 × 约 30 字节 ≈ 120KB/s 的下行流量。这个数字对任何对局来说都是不可接受的。

所以正确的做法是:网络只同步"破坏块碎了"这个事件,不同步碎片的运动。碎片的位置、旋转、速度全部由客户端本地用一个伪随机种子算出初始状态,然后本地物理或者本地动画驱动。因为碎片本身不参与任何逻辑判定(不挡子弹、不挡视线、不挡路),它飘到哪里都无所谓,玩家甚至不会注意到两台机器上碎片落点不一样。

这里有个细节值得说:伪随机种子要跟破坏事件绑定。比如用"破坏块 ID + 服务器时间戳"做种子,这样同一块墙在重连之后还能算出大致相同的碎片分布,不会出现"我重连回来墙又碎了一遍、碎的方向完全变了"的诡异感。成本几乎为零,体验提升很明显。

2.3 客户端预测在破坏上的特殊处理

破坏的预测比其他动作要麻烦,因为它有延迟累积。玩家开枪打墙,本地要立刻看到墙出现裂纹或者掉渣,不能等服务器回包——RTT 高的时候等 100ms 才反馈,手感会碎。

我的做法是分阶段预测。碎裂的第一阶段(表面破损、掉渣、裂纹)纯本地立即播放,因为这个阶段不改变碰撞和通行,错了也没关系。碎裂的最后阶段(墙体彻底消失、碰撞体移除)必须等服务器确认,因为这一步会改变战术环境,不能提前生效。

中间会有一个短暂的"看起来快碎了但还能挡人"的窗口,通常 50-100ms。处理方式是让视觉效果往回收一点——比如用裂纹加深、颜色变暗来暗示"还没完全破",让玩家心理上接受这面墙仍然有效。这个做法我试过好几个版本,比"直接消失再回滚"的接受度高很多,因为玩家对视觉回滚极其敏感,对"裂纹变深"完全不敏感。

重连和回滚的时候,客户端需要拿到完整的破坏状态快照。所以状态层得支持导出:一份完整的"哪些块碎了"的位图。这个东西压缩一下,一个中等地图也就几十字节,重连的时候一次性下发,比逐条补事件快得多也稳得多。

3. 关卡资产:碎块是被"预制"出来的

破坏系统的天花板,其实在美术和关卡阶段就定死了。运行时再优化,也救不回来一个没切好的资产。

3.1 层级化破碎与破坏树

我看到的做法基本是预先切分 + 层级化。一整面墙不是一个物体,而是一棵树:根节点是完整墙体,第一层碎成 4 到 8 块大块,每块再往下碎成 4 到 6 小块。通常只做两层,最多三层。为什么不做更深?因为每多一层,资产数量和内存占用翻倍,而玩家在实际对局里几乎感知不到第三层的差异——他们能看出"墙塌了"和"墙变成渣了",但看不出"渣再碎一次"。

层级化的另一个好处是破坏进度可视化。第一层碎了,墙体上会出现大洞,玩家一眼就知道这面墙被打过了;第二层碎了,整个结构变成一堆瓦砾,变成不可通行的地形。这个"半破→全破"的两段式,比一枪碎掉的单段式在战术上更有意思,因为它给防守方留了反应时间。

切块的时候有几个实操要点。切分面要和材质的纹理走向对齐,不然碎块边缘会出现拉伸;每块碎块的碰撞体要用凸包,不要用原始网格,凹形碰撞体的性能开销高一个量级;大块碎块要单独给一个简化的"低模版本",远处观战时直接切低模,省下来的预算可以多放几百块碎片。

3.2 关卡白盒阶段的破坏预算

这一步最容易被跳过,但它是后面所有性能问题的源头。我的建议是:在设计文档里就给每张地图定一个破坏预算,比如"同屏最多 1200 个活跃碎块、单次最大破坏事件产生不超过 40 块碎片、单块碎片的物理寿命不超过 6 秒"。

预算定下来之后,关卡设计师每放一个可破坏体,就要在这本账上记一笔。我见过太多项目是关卡先做完、再交给技术团队"优化一下",结果发现某条街道上密集堆了 30 面可破坏墙,一次交火能同时触发 15 次破坏,帧率直接掉到 30 以下,最后只能把一半的墙改成不可破坏——白做。

还有一条:承重结构要单独标记。玩家对"打掉承重柱导致整栋楼塌"的期待是很高的,但对"打掉装饰柱什么都没发生"的失望也很高。所以要么给承重结构设计专门的坍塌动画和物理连锁,要么干脆别让玩家打它。中间状态最伤体验。

3.3 碰撞体、导航与视觉代理的分离

这三个东西必须分开维护,混在一起就是灾难。

碰撞体决定"能不能走过去",必须跟着破坏状态实时更新。导航网格决定"AI 能不能走过去",更新成本极高,通常做法是预烘焙多套导航:完整状态下的一套、主要破坏路径完成后的一套。或者干脆接受 AI 在破坏后的区域走得比较笨,用局部的临时避障来兜底。全动态导航在多人对局的实时性要求下基本不现实。

视觉代理是给远处玩家看的简化版本。破碎之后瓦砾堆的视觉表现可以用一个整体的"废墟模型"加一层贴花,而不是真的堆几百个碎块。玩家在 30 米外根本分不清你堆的是 300 块还是 3 块。

提示:任何会改变通行性的破坏,都必须同步更新碰撞体并在下一个 tick 生效,晚一帧都会出现"人卡在已经碎掉的墙里"的情况。

4. 表现层:让破坏"看得懂"的那些细节

玩家能感知到的破坏质量,八成来自表现层,跟底层物理精度关系不大。碎块飞得对不对、烟尘遮不遮视线、声音有没有重量,这些东西直接决定了玩家觉得"这游戏破坏做得好不好"。

4.1 碎块飞散方向与烟尘遮挡的平衡

碎块的飞行方向要顺着冲击方向走。子弹打墙,碎屑应该往弹道方向的反面喷;爆炸炸墙,碎块应该以爆点为中心呈锥形散射。这个方向性给玩家的反馈极强——他能从碎块飞出的方向判断子弹是从哪来的,这在战术上是有效信息。

但烟尘是个双刃剑。烟尘能强化冲击感,也能意外制造视野优势。如果一堵墙被打碎后烟尘弥漫三秒,那这三秒里穿过烟尘的玩家就是隐身的,等于凭空多了一个烟雾弹。我的处理方式是:烟尘的浓度和持续时间按可破坏体的重要性分级。战术关键墙体破坏后烟尘要快速消散(0.8 到 1.2 秒),而且不能完全遮挡视线,保留大概 30% 的透明度;装饰性墙体可以放得夸张一点,因为没人靠它打仗。

另外要注意粒子的"体积感"。纯 sprite 粒子在近处看会很薄,玩家贴脸打墙的时候会觉得很假。做法是混合使用粒子、少量低模碎块和一层半透明的体积雾,近处靠低模撑体积,远处靠粒子撑数量。

4.2 音效分层与破坏的重量感

破坏的重量感,七成靠声音。我的分层方案是三层:撞击层(子弹打在材质上的那一刻,短促、干)、碎裂层(结构开裂,带一点延迟,0.05 到 0.1 秒)、坍塌层(大块掉落和瓦砾堆积,长尾,1 到 3 秒)。

延迟是关键。很多项目把碎裂声和撞击声同时播,听起来像"啪"一下东西就没了,轻飘飘的。真实的破坏有传导过程——先撞击,再听到内部开裂,最后才是坍塌。加上这 50 到 100ms 的延迟,同样一段素材的重量感能提升一个档次。

材质分类也要做细。混凝土、金属、木材、玻璃的破坏声完全不同,而且要考虑混合材质:一堵混凝土墙里可能有钢筋,碎的时候要有金属的余韵。这个细节玩家说不出为什么,但去掉之后立刻觉得"假"。

声音还有个功能性的作用:给远处的玩家提供信息。听声辨位在射击游戏里是核心能力,破坏声要能被定位。所以破坏音效要挂在破坏位置上做空间化,而不是放在 2D 层。

4.3 碎片生命周期与对象池

碎片的生命周期管理是个纯工程问题,但做不好会毁掉整个体验。碎片不能永久存在,否则对局打到后期,地图上堆几千个碎块,帧率必然完蛋。

我的经验值是碎片存活 3 到 6 秒,超出之后按距离和重要性分批回收。近处的碎片留久一点,远处的一秒后就淡出。回收的时候不要直接删,要做一次快速的缩放淡出或者沉入地面,直接消失会被玩家看到。

对象池要按破坏层级预分配。大块碎块池给 200 个,小块碎块池给 800 个,粒子系统池给 300 个。池子满了之后走优先级替换:优先回收最远的、最不显眼的、已经静止的碎片。这套逻辑写起来不复杂,但必须提前做,事后补会牵动一大片代码。

注意:物理刚体的回收一定要和视觉回收解耦。视觉上碎片可以停留 6 秒,但物理模拟 1.5 秒之后就切成静态,静止的刚体不参与求解,这部分省下来的 CPU 相当可观。

5. 性能账本:一次大范围破坏花多少钱

不给出具体数字的性能讨论都是空谈。下面这张表是我自己项目里调出来的经验分布,量级上应该和同类项目接近,具体比例会随引擎和平台浮动。

5.1 帧预算怎么分

按 60 帧的目标,单帧预算 16.6ms。一次中等规模破坏(20 块碎片、2 组粒子、4 个音效)的瞬时开销大致是这样分布的:

开销项典型耗时峰值耗时说明
物理刚体创建0.3-0.6ms1.2ms20 个刚体,含碰撞体实例化
刚体模拟(首帧)0.4-0.8ms2.0ms碎片互相碰撞,首帧最贵
粒子发射0.2-0.4ms0.9ms视粒子数量,GPU 粒子可再降
音效触发0.05ms0.1ms含空间化计算
视觉替换与材质切换0.1-0.3ms0.8ms完整模型换破碎模型
网络包处理0.02ms0.05ms只在收到状态变更时发生

关键结论是:单次破坏本身不贵,贵的是"同时发生"。一次 20 块碎片的破坏只花 1-2ms,你还有余量;但如果五个人同时打五面墙,就是 10ms,帧率立刻从 60 掉到 40。所以真正要做的优化不是"让单次破坏更快",而是给破坏事件排队和限流。

我的做法是给破坏事件加一个每帧的处理上限,比如每帧最多处理 3 个破坏事件,超出的排队到下一帧。视觉上完全看不出来——因为破坏本来就是有过程的,晚 16ms 播和早 16ms 播,玩家分不清。但帧率的抖动会立刻消失。

5.2 几个典型的性能陷阱

第一个陷阱是重叠破坏。一块墙被打碎的同时,它旁边的墙也被打碎,两块墙的碎片在同一个空间里生成,物理引擎要处理大量初始重叠的刚体,求解器会在第一帧疯狂迭代。解决办法是给相邻的破坏块加一个极小的位移偏置,让碎片生成时就错开一点。

第二个陷阱是破坏触发连锁。塌陷的碎块砸到下面的可破坏体,触发二次破坏,二次破坏又触发三次,一栋楼塌下去能触发上百次事件。必须给连锁破坏加深度限制,我一般设 2 层,超过就不再触发新的破坏,只做视觉表现。

第三个陷阱是材质切换的 draw call 飙升。完整模型和破碎模型如果材质不同,切换的瞬间会打断合批。做法是让破坏块使用和完整模型相同的材质图集,破碎模型只是不同的 UV 区域,这样切换时不产生新的材质实例。

第四个陷阱是阴影。几百个碎块如果都投射阴影,阴影 pass 的开销可能比主 pass 还高。我的做法是:只有大块碎片投射阴影,小块碎片不投射,靠接触阴影的贴花补一点接地感。玩家完全看不出区别。

6. 常见问题与排查实录

下面这些是我和同行交流时反复听到、自己也都踩过的问题,按类别整理成速查表。

6.1 同步类:墙体"复活"和幽灵碰撞

最经典的 bug 是"我这边墙已经碎了,队友那边墙还在"。成因通常是状态变更包丢包或者乱序,客户端没收到。排查思路是先把破坏状态做成带版本号的可靠通道,每次状态变更带一个单调递增的序号,客户端发现序号跳变就主动请求一次全量快照。

第二个经典问题是幽灵碰撞:视觉上墙碎了,但碰撞体还在,玩家撞上去弹开。这基本可以断定是"视觉替换和碰撞体销毁分属两个不同步的流程"。解决办法是把碰撞体的销毁挂在状态层的事件回调上,而不是挂在动画结束的回调上。

第三个问题是重生后地图状态不一致。玩家重生时如果只拿到增量更新,很容易漏掉自己死亡期间的破坏。建议重生时强制下发一次全量破坏位图,几十字节的事,能省掉一大堆玄学 bug。

6.2 物理类:碎片穿模与堆叠抖动

碎片穿模通常有两个原因。一是碎片生成时和原墙体的碰撞体重叠,物理引擎把它"弹"出去或者卡在内部。解决办法是破坏发生的瞬间就把原碰撞体禁用,而不是等动画播完。二是碎片速度过快,一个 tick 内位移超过自身尺寸,隧穿。解决办法是给碎片加连续碰撞检测,或者干脆把碎片的最大初速度压到一个安全值——反正它不影响逻辑,飞太快也没有意义。

堆叠抖动是老问题了。几百个碎块落地后互相挤压,求解器每帧都在修位置,看起来像果冻在颤。我的做法是碎片落地静止 0.5 秒后直接切成静态碰撞体,退出动力学模拟。视觉上完全看不出来,CPU 开销能砍掉一大半。

6.3 体验类:玩家被卡住、找不到路

这是最容易被技术团队忽略、但玩家抱怨最多的一类。破坏改变了地形,玩家可能被埋在瓦砾里出不来,或者原本的通道被堵死导致地图某侧完全没有入口。

我的处理有三条。第一,破坏产生的瓦砾堆要设置一个"可跨越高度",超过这个高度的堆积不参与碰撞,玩家可以直接穿过去。第二,所有关键通路上要放"不可破坏的地面锚点",保证任何情况下至少有一条路是通的。第三,加一个卡住检测:玩家在同一位置停留超过 5 秒且位移小于阈值,自动给一个短距离的脱困位移。

第三条听起来很粗暴,但实测下来比任何精细的碰撞修复都有效,而且玩家几乎不会察觉,只会觉得"我刚才好像滑了一下"。

问题现象最可能原因首选排查手段
墙体视觉已碎但还能挡人碰撞体销毁未挂状态回调断点看状态层事件是否触发
两台机器碎片落点差异巨大伪随机种子未与事件绑定检查种子生成逻辑
破坏瞬间掉帧 20 帧以上相邻破坏事件同帧触发打日志看每帧事件数
碎片落地后果冻抖动静止刚体未退出模拟检查休眠阈值设置
重生后地图状态错乱只下发了增量更新改为重生时全量快照
玩家被瓦砾困住堆积碰撞未做高度裁切检查碎块碰撞体开关

7. 复刻一套轻量版的实操路径

如果你只是想在项目里做一套"够用"的破坏,不需要复刻到对局级规模,可以按下面这个路径走,一个人两三周能出可玩版本。

7.1 最小可行版本:四步搭起来

第一步,做资产。找一面墙,在建模软件里切成 6 块,每块单独导出,保留一个原始完整模型。切的时候注意用凸包碰撞,别用原始网格。这一步大概一天。

第二步,做数据。定义一个 ScriptableObject 或者 DataTable,存每个可破坏块的 ID、最大血量、破碎层级、子块列表、破碎后是否改变通行。血量我建议按"武器单发伤害 × 4 到 8 发"来定,太脆了没手感,太硬了玩家不知道能打破。

第三步,做状态机。核心就三个状态:完整、半破、消失。伤害累积到第一阈值进半破,换模型、更新碰撞、播放一次碎屑;累积到第二阈值进消失,销毁碰撞、生成碎片、播放大效果。所有状态变更走一个中心化管理器,别分散在各个物体上。

第四步,接表现。碎片用对象池,池子大小按"单次破坏最大碎片数 × 5"来定。碎片存活 4 秒,落地 0.8 秒后休眠。粒子用最简单的 GPU 粒子,烟尘控制在 1 秒内散掉。

这套做完,你已经有一个"能打穿墙"的可玩版本了。剩下的全是打磨。

7.2 参数调优与验证清单

调参的时候我会按这个顺序验证,每次都记录数值,别凭感觉:

参数起步值调整方向
单块血量4 发步枪弹玩家抱怨"打不烂"就往下调
碎片数量(单块)6-8 块超过 12 块收益递减明显
碎片存活时间4 秒近处 6 秒,远处 2 秒,按距离分档
物理模拟时长1.2 秒之后切静态,省 CPU
烟尘消散时间1.0 秒战术关键墙体不超过 1.2 秒
每帧破坏事件上限3 个卡顿就往下调,最低 1 个
连锁破坏深度2 层超过 2 层只做视觉

我在实际项目里体会最深的一点是:破坏系统的难点从来不在"怎么把东西打碎",而在"怎么让打碎这件事既不影响帧率、又不出同步事故、还不把玩家卡在墙里"。前面两条是技术问题,有明确的解法;最后一条是设计问题,得靠关卡和数值一起兜。我踩过的最大一个坑,是把破坏预算表放到项目中期才做,结果美术资产全部返工重切,多花了三周。现在我的习惯是:只要项目里有一丁点可破坏的内容,白盒阶段就把预算表拉出来,宁可前期抠得细一点,也别后期返工。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进