ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

帧同步游戏为何必须共享随机种子

2026/8/20 15:51:03 拓冰建站 浏览量
帧同步游戏为何必须共享随机种子 上一篇讲伪随机,结尾说随机数生成器在确定性系统里是被当逻辑状态严格管着的,种子要全端同源。这一篇把它落到帧同步这个具体架构上——为什么一局帧同步的游戏,开局第一件事就是所有人拿到同一个初始随机种子,为什么这颗种子丢了或者不一致,整局就没法玩。这事顺着前面所有关于确定性的铺垫,水到渠成。先把帧同步是啥说清楚要理解种子的地位,得先明白帧同步(lockstep)到底怎么工作,它和你可能更熟的状态同步是两条完全不同的路。状态同步是这样:服务器算好一切,然后把结果——谁在哪、血量多少、谁死了——打包发给每个客户端,客户端负责显示。客户端基本是个显示器,真相在服务器那份状态里。这种模式下随机在服务器算就行,算完把结果发下来,客户端照着显示,种子一致不一致无所谓,反正客户端不自己算。帧同步反过来:服务器(或者中转)只转发玩家的输入指令,不发游戏状态。每个客户端拿到所有人的输入后,在本地自己把整局游戏完整地算一遍。你按了移动、他开了枪,这些指令广播给所有人,然后每台机器独立地、各自地把这一帧的世界推演出来。状态同步: 帧同步: 服务器算好状态 服务器只转发输入 │ │ ┌────┼────┐ ┌────┼────┐ ▼ ▼ ▼ ▼ ▼ ▼ 客户端各自显示 每个客户端拿到相同输入 (自己不算) 各自把整局完整算一遍帧同步的最大好处是省带宽(只传输入,不传庞大的世界状态)、天然支持海量单位(RTS 里几百个兵,状态同步传不起,帧同步只传那几条指令),回放也简单(存下所有输入就能重演整局)。RTS、很多 MOBA、格斗游戏都用它。但帧同步有个近乎苛刻的前提,整个模式就架在这个前提上——帧同步的命根子:每台机器必须算出完全一样的结果既然每台机器都在本地独立算整局,那就必须保证:给定完全相同的输入序列,所有机器算出的世界状态逐比特一致。这个要求有多硬?想想看,服务器不发状态,只发输入。那你这台机器上的世界,和队友那台机器上的世界,是两份各自独立演算出来的东西。它们凭什么会一样?唯一的凭据就是——输入一样,而且演算过程处处确定。只要有任何一个地方,两台机器算出了哪怕最低位的差异,这点差异就会像滚雪球一样被后续每一帧放大:这一帧你的角色位置差了 0.001,下一帧他就撞到了本不该撞的墙,再下一帧他走了完全不同的路,几秒钟后两台机器上的世界彻底变成两个平行宇宙。这就是帧同步最怕的词:desync(不同步)。一旦某台机器和别人分叉,它看到的游戏和别人看到的完全不同——你这边人还活着,他那边你已经死了,谁也说服不了谁,因为没有一个服务器状态来当权威裁判。整局崩溃,只能强制重连或者判定作废。这个系列前面讲的所有东西——定点数代替浮点、自己查三角函数表、重力常量烘成整数、着地迟滞用帧不用时间——全都是为了守住这一条逐比特一致。因为帧同步不给你犯错的余地,一处泄漏,满盘皆输。随机数:帧同步里最刺眼的一个不确定源现在把随机数摆进来。游戏里到处是随机:暴击不暴击、掉什么装备、怪从哪个门刷、霰弹往哪散、技能有没有触发特效。在帧同步下,这些随机的结果必须在所有机器上完全一样——因为每台机器都在独立演算,你这台算出暴击了、他那台算出没暴击,伤害就不一样,血量就分叉,desync 立刻发生。而上一篇讲透了:计算机里的随机是伪随机,一串数由种子唯一确定,同种子同算法必然吐出同一条链。这个伪的特性,在帧同步里简直是天赐的解药——只要所有机器从同一个初始种子出发,用同一个算法,那么整局所有的随机,每一次暴击判定、每一次掉落、每一颗弹丸的散射方向,在所有机器上都会算出一模一样的结果。因为大家跑的是同一条伪随机链,第一个随机数一样、第二个一样、第 10000 个也一样。开局:服务器/房主 生成一个初始种子 seed │ 广播给所有客户端 │ ┌──────────┼──────────┐ ▼ ▼ ▼ 客户端A 客户端B 客户端C RNG(seed) RNG(seed) RNG(seed) ← 都用同一颗种子初始化 │ │ │ 这局所有随机,三台机器逐比特一致这就是为什么帧同步离不开初始随机种子:它是让所有机器上的随机结果保持一致的唯一手段。没有这颗共享的种子,每台机器各自随机各的,随机结果立刻分叉,而随机又渗透在战斗的方方面面,几乎是开局几秒就 desync。种子是帧同步确定性拼图里,专门负责随机这一块的那块拼图,缺了它,整幅图拼不完整。种子怎么发:开局定死,全局共享具体操作上,初始种子的处理有几个约定俗成的做法:谁生成。通常由服务器,或者帧同步里的房主客户端,在对局开始前生成一颗种子。这颗种子怎么来的可以是真随机(采物理噪声,保证每局不一样,防止玩家背板预判),但一旦生成,就作为这局的一部分广播给所有人,之后大家都用这颗固定的种子演算,不再有任何随机性。开局下发,写进对局初始状态。种子和地图、玩家列表、初始位置这些一起,构成这局游戏的初始状态。所有客户端在开打前就拿到了同一颗种子,用它初始化本地的确定性 RNG。// 对局开始,所有客户端收到同一份初始数据structMatchInitData{ulongrandomSeed;// ← 这颗种子,全局命脉intmapId;PlayerInfo[]players;// ...}voidStartMatch(MatchInitDatainit){// 所有客户端用同一颗种子初始化 RNG,从此这局随机全端一致gameRandomnewDeterministicRandom(init.randomSeed);// ...}RNG 状态跟着走帧,进快照。上一篇说过,RNG 的 state 是逻辑状态的一部分。在帧同步里,它跟着每一帧的推进而变化,也和其他状态一样参与校验、参与回放恢复。你甚至可以每隔若干帧把整个世界状态(包括 RNG 的当前 state)做个哈希,和别的客户端对一下——哈希一致说明还同步,一旦哈希分叉,立刻知道 desync 了、发生在哪一帧,这是帧同步排查 bug 的标准手段。RNG 的 state 是这个哈希里很重要的一项。光有种子不够:消耗顺序也得锁死这里必须重提上一篇最后那条最隐蔽的铁律,因为它在帧同步里尤其致命。伪随机是一条有顺序的链,第 N 次调用Next()拿第 N 个数。种子一致,只保证了这条链本身一致;但如果两台机器消耗这条链的次数或顺序不一样,照样立刻分叉——你这台机器某处多调了一次Next(),从此你的第 N 个随机数,对应的是别人的第 N1 个,整条链错位,后面全错。帧同步里最容易踩的坑,就是表现层不小心动了共享 RNG。比如某个客户端的画面特效、音效随机、UI 抖动,图省事拿了那个参与同步的gameRandom来随机一下。这个客户端就比别人多消耗了随机数,链错位,desync。所以规矩是:参与同步的 RNG,只能被参与同步的逻辑消耗,而且所有客户端消耗的次数和顺序必须严格一致。表现层要随机,另开一个独立的、不参与同步的 RNG,爱怎么调怎么调,井水不犯河水。DeterministicRandomgameRandom;// 参与同步:全端种子一致、消耗顺序一致System.RandomvisualRandom;// 纯表现:各端随便,不影响逻辑// 逻辑:暴击判定,必须用 gameRandom,全端一致boolisCritgameRandom.Next()%100critRate;// 表现:命中特效的随机抖动,用 visualRandom,动了也不影响同步floatshakevisualRandom.Next()*0.01f;这条线划不清,种子发得再对也白搭——同一颗种子,不同的消耗顺序,结果照样两个世界。收尾帧同步为什么离不开初始随机种子?把逻辑链条捋直:帧同步的模式是只传输入,每台机器本地独立把整局算一遍,这决定了它的命根子是所有机器给定相同输入必须算出逐比特一致的结果,否则 desync、整局崩溃,而且没有服务器状态来当裁判救场。随机是游戏里无处不在的不确定源,在帧同步下每一次随机的结果都必须全端一致,否则伤害、掉落、血量立刻分叉。而伪随机同种子同算法必吐同一条链的特性,正好是解药——只要所有机器从同一颗初始种子出发、用同一个算法,整局的随机就全端逐比特一致。所以开局第一件事,就是服务器或房主生成一颗种子、广播给所有人、写进对局初始状态,让每台机器用它初始化本地 RNG。种子丢了或不一致,随机立刻各算各的,几秒就 desync;光有种子还不够,消耗随机数的次数和顺序也得全端锁死,表现层的随机必须另开一个独立 RNG,绝不能碰参与同步的那条链。绕回这个系列的主线你会看得更透:初始随机种子,和定点数、和自己查三角表、和逻辑帧计时,是同一件事——都是为帧同步那条逐比特一致的生命线服务的。种子只是这条生命线上,专门锁住随机的那一环。守住每一环,平行的机器们才能演算出同一个世界。