全部项目
进行中2026 — 至今

Helix Empire —— 一万名玩家共处同一个实时世界

一款浏览器端的实时太空策略游戏,核心是遗传学:玩家通过编辑 DNA 来培育各类生物种群。难点在引擎——让一万名玩家在同一台服务器上共享一个世界,并实时更新。我把架构设计在真正的瓶颈上(出站流量,而非 CPU),并用一次实测的压力测试证明了它:一万玩家时 237.6 Mbit/s,占预算的 24%。单人独立完成,目前处于内测阶段。

角色
独立架构师兼工程师
技术栈
Rust · WebAssembly · WebTransport / QUIC · Event sourcing · Area of Interest · Hetzner bare-metal
时间
2026 — 至今

问题

Helix Empire 是一款实时的浏览器端太空策略游戏。它的核心不是建造—— 而是遗传学:玩家通过编辑 DNA 来培育各类生物种群(这正是「Helix」的由来—— 双螺旋)。基因决定性状,性状决定生物在不同环境下的表现,这驱动经济和军队,进而驱动 科技,而科技又解锁对基因组的新编辑。一个闭环:基因 → 性状 → 经济 → 科技 → 实验室 → 再次回到基因。 帝国实力 = 基因 × 行星 × 科技。

从技术上讲,这意味着数千名玩家共处同一个世界,并实时存活——经济在跳动、生物在 繁殖、舰队在移动、资源每秒都在变化——而这一切都必须毫无延迟地送达每位玩家的浏览器。

大多数在线游戏都在掩盖自己的规模:把玩家拆成 20–50 人的小房间,或者拆成分片。我故意 设了一个更难的目标——一万名玩家同时在线,处于同一台服务器上的同一个世界——因为 有趣的架构恰恰就藏在这个约束里。

四堵墙

把「一万人在同一个世界」说出口,就会冒出四堵墙,naive 的设计会一头撞上去。

  1. 「人人对人人」的广播是 O(N²)。 如果每个玩家都必须知道其他每个玩家,一次 tick 就是 10,000 × 10,000 = 100,000,000 对——而且每秒要算好几次。物理上不可能。玩家 翻倍,工作量翻四倍。
  2. 主要开销是流量,不是 CPU。 一个反直觉的托管事实:出站流量(egress)才是最贵 的部分——在超大规模云厂商上比裸金属贵几十倍。如果每个 tick 都向每个玩家发送大量 字节,流量账单会在 CPU 顶不住之前就先把项目拖垮。所以架构必须围绕最小化流量来设计。
  3. 一个世界想跑在一台服务器上。 把一个世界铺在多台机器上,就会被迫引入分布式共识 (哪台服务器持有真正的状态?)——又慢又复杂。我选了 single-writer:恰好只有一个 进程可以修改一个世界。没有竞态,没有共识。(它有一个讨厌的陷阱——下文细说。)
  4. 瘦客户端跟不上。 如果服务器算一切、浏览器只负责画,服务器就成了瓶颈。客户端必须 是厚的——能从最少的数据里重建出整幅画面。

这套技术栈——针对墙而选,不追潮流

架构的意义在于,技术服务于约束,而非时髦。 这里的每一个选择都回应了某一堵具体的墙。

Rust + 编译到 WebAssembly 的确定性内核

仿真内核用 Rust 写一次,既编译成原生服务器二进制,也编译成浏览器用的 WASM。同一份代码 在两端运行,于是厚客户端能从紧凑的「种子」重新算出世界并向前预测——把负载从服务器上挪走 (第 #2、#4 堵墙)。Rust 可预测的内存、没有垃圾回收器,意味着每台服务器能承载更多玩家。

为什么: 仿真只有一个事实来源,意味着客户端和服务器在物理上不可能产生分歧——收敛是 结构性的,而不是「但愿能通过的测试」。

WebTransport / QUIC、single-writer 世界、event sourcing

实时帧通过 WebTransport / QUIC 传输(一种跑在 UDP 之上的快速二进制流,没有 TCP 的 队头阻塞),并带 WebSocket 回退。每个世界只有一个写者,它的历史是一条事件流—— 所以世界内部没有共识,状态可以从事件中重建或审计(第 #3 堵墙)。

为什么: single-writer 免费换来一致性;event sourcing 换来可恢复性。代价是读取争用 这个陷阱,下文的读模型缓存会把它抵消掉。

我一步步建了什么——每一步都在测量

我不是一次建好的。我是分层推进,在每一层都做测量——因为你没法优化你没测量过的东西。

第 0 步——基线。 在动手修任何东西之前,我先测了它有多糟:状态读取会飙到 4–10 秒,向客户端推送大约每 20 秒一帧。这给了我一个要打败的数字。

第 1–3 步——消除 single-writer 争用。 陷阱在这里。既然一个进程在修改世界,那么在它 应用一次 tick(写入存储——很慢)期间,所有读者都在等同一把锁。打开防御界面就卡 10 秒, 因为服务器正卡在存盘中。修法:RAM 里的无锁读模型缓存,增量更新,读取时不需要写锁。

0.002s
防御读取,原本是 0.002–4.4s 并有尖峰
~0.006s
事件流,原本约 0.2s
read = write
已分离:读者不再等待写者

架构上的动作:把读和写分开。 一个写者仍然掌管一致性;读者读 RAM 投影,不再等待。 争用消失了,每个端点都稳定地降到一秒以内。

第 4–5 步——发送差异,而不是整份状态。 即便读取已经很快,每个 tick 仍然发送完整 状态——把成堆没变过的东西重新发一遍。修法:增量(deltas)。一个经过数学校验的 WireSessionStateDelta,其中 between(prev, next) 构造差异,apply(prev, delta) 把它 回放。一个收敛测试证明:客户端折叠的一连串增量精确重现了服务器快照——而且既然客户端和 服务器共享同一份 Rust 代码,它们就不会漂移。丢了一帧?客户端会察觉(ResyncRequired)并 请求一份完整快照。

第 6 步——Area of Interest 把 O(N²) 砍到线性。 一个玩家并不需要全部一万人——只需要他 在空间上和外交上的邻居。AreaOfInterest + filter_frame 把每个增量裁剪到订阅者真正在意 的内容,同时保留全殖民地范围的上下文(聊天、交易)。二次方广播变成线性:流量按 N ×(兴趣区大小)增长,而不是 N²。

第 7 步——诚实的最后一公里。 这是整个故事里最重要的部分。一次压力测试揭示,那个漂亮的 「320 Mbit/s」数字建立在一个硬编码的假设之上:每个玩家增量 32 字节。我接上真实的二进制 编解码器并测量:一个真实增量是 104 字节,因为我即便只有一个字段变了,也要发送一个玩家 的整份档案(11 个资源字段)。104 对 32 是超出 3.25 倍——在一万玩家时投影到约 1.04 Gbit/s。那一刻诚实的回答是:「不行,在真实流量下我扛不住一万。」

修法是一个按字段的增量WirePlayerDelta

player_id (4 字节) + 变更字段位掩码 (2 字节) + 仅变更的值

如果人口、食物和科技变了,线上就恰好携带这三个,再加掩码—— 4 + 2 + 4 + 8 + 8 = 26 字节,而不是 104。这个 16 位的掩码说明后面跟着哪些字段;一个没变的 字段不花任何成本。

它是如何被证明的——以及为什么这个数字值得信

没有证明的架构只是一个承诺。我用数字证明了它:往返与收敛单元测试、每个端口上的契约测试, 以及 42 个跑在真实 Chromium 上、走 WebTransport 的端到端测试(包括「在实时套接字上接收 服务器 tick,无需轮询」和「同一个世界里的两名玩家实时同步」)。

最核心的证明是一次一万机器人的蜂群测试,它用真实的二进制编解码器测量 egress——没有任何 硬编码常量。它在一个世界里构建一万个机器人客户端,跑一次真实的权威 tick,让每个机器人的可见 增量都经过真实的 AoI 过滤,再把测出来的字节加总。

26 字节
典型玩家增量,从 104 降下来
237.6 Mbit/s
一万 × 每秒 5 tick 时的 egress
24%
1 Gbit/s 预算的占比——约 76% 余量
≤ 200 ms
一万玩家时的 tick 扇出延迟

这个数字由一道 check:load 关卡守护:每次运行都会校验报告,而投影是从实测值推导出来的, 不是来自假设。如果有人不小心把增量格式撑大了,这道关卡就会失败。

诚实的边界

架构上的成熟,不只是拿到一个好数字——还包括对它的局限保持诚实。

已证明: 就算力和延迟而言,向一万机器人的扇出稳稳保持在 200 ms 以内,余量很大;就 egress 而言,237.6 Mbit/s(1 Gbit/s 预算的 24%),用真实编解码器测量得出,不是假设出来的。

尚未证明: 这是从一万玩家时的一次实测 tick(机器人在同一个进程里)做出的投影,而不是一个 有一万个真实 QUIC 套接字、连续数小时承压的实时集群。在持续数分钟的数据流下 CPU/RAM 的表现, 以及面向厚 WASM 客户端的逐订阅者 AoI 通道,是下一层要做的工作。但最大的风险——egress,也就是 我自己主动标记为「在真实格式下超预算」的那一项——已经关闭并被测量。

这里属于架构师、而不只是开发者的部分

  1. 点中了真正的瓶颈: egress,而不是 CPU。整套技术栈都服务于这一个结论。判断错了,你就会 去优化错误的东西。
  2. 把读和写分开: 用 single-writer 保证一致性 + 无锁 RAM 投影服务读取。在不牺牲正确性的 前提下消除了争用。
  3. 把 O(N²) 降到线性,靠的是 Area of Interest——没有它,一万人原则上就不可能。
  4. 用增量代替快照,服务器和客户端共用一份代码,于是收敛由架构保证,而非靠运气。
  5. 没测量之前不轻信那个漂亮数字。 发现「32 字节」是个假设,测出真实的 104,设计了按字段 的增量,把它压到 26,并用测量证明了它。
  6. 用一道关卡锁住结果,让回归没法悄悄溜过去。

一个能经过测量、在 24% 流量预算下于同一个世界服务一万名玩家的引擎,不是运气,也不是单个 小把戏。它是一连串架构决策,每一个都回应一堵具体的墙,每一个都有数字背书。

技术栈速览

选择回应的墙
仿真内核Rust,确定性,编译为原生 + WASM厚客户端,每台服务器更多玩家(#2、#4)
传输WebTransport / QUIC,WebSocket 回退低延迟二进制流(#2)
世界模型Single-writer + event sourcing无共识,可恢复(#3)
读取无锁 RAM 读模型投影无争用、一秒以内的读取
更新按字段二进制增量 + Area of InterestO(N²) → 线性,egress 最小化(#1、#2)
托管Hetzner bare-metalegress 比超大规模云厂商便宜 20–40 倍(#2)

Helix Empire 处于内测,单人独立完成——产品设计、Rust 仿真内核、WASM 客户端、传输、读写分离、 增量 + AoI 流水线、压力测试框架,以及那份经过测量的证明。很快就能在 helixempire.com 上玩到。