How Factorio Syncs A Million Objects over the network
摘要
The article analyzes Factorio's deterministic lockstep architecture, explaining how it synchronizes millions of entities by transmitting inputs rather than state snapshots, utilizing custom math libraries and PRNG normalization to ensure bitwise consistency across platforms.
<p><a href="https://lobste.rs/s/2r03i7/how_factorio_syncs_million_objects_over">Comments</a></p>
查看缓存全文
缓存时间: 2026/05/11 07:26
TL;DR: Factorio 采用确定性锁步(Deterministic Lockstep)架构而非传统的快照同步,通过将计算负载从网络带宽转移至 CPU,实现了百万级实体的高效同步;本文解析了其通过自定义数学库、PRNG 规范化、延迟状态缓冲区及服务器端“超级数据包”等技术手段解决非确定性与网络延迟问题的实现细节。
## 从快照架构到锁步架构
在传统的 FPS 游戏中,服务器通常采用“快照架构”(Snapshot Architecture),每毫秒通过差分压缩(Delta Compression)向客户端发送游戏状态的变化。为了节省带宽,服务器会计算潜在可见集(PVS, Potentially Visible Set),仅发送玩家视野内的数据。这种架构旨在应对带宽、延迟和可靠性之间的网络三难困境。
然而,《异星工厂》(Factorio)采用了截然不同的**确定性锁步架构**(Deterministic Lockstep Architecture)。
在此架构下,玩家仅向服务器或其他玩家发送**输入操作**(如鼠标点击),而非游戏状态。会话中的所有成员随后基于相同的输入自行模拟游戏进程。这意味着:
1. **极低带宽占用**:服务器不关心十万台机器中每一台的位置或状态,只关心玩家的输入指令。
2. **负载转移**计算负担从网络带宽和服务器负载转移到了玩家的 CPU 上。
3. **输入延迟**:由于所有成员必须在模拟每一帧前收集所有输入,画面会滞后于最近的操作,产生固有的输入延迟。
## 确定性的前提:严格的一致性
锁步架构的核心前提是**确定性**(Determinism)。所有参与者在给定相同输入的情况下,每一 tick(游戏逻辑帧)必须计算出完全相同的结果。若出现差异,模拟将不同步,导致玩家看到完全不同的游戏世界。
### 浮点运算的挑战
浮点运算在不同平台、编译器及指令集之间往往存在差异,尤其是涉及向量化指令时。例如:
* **RCPPS 指令**:一次性计算四个浮点数的倒数。
* **RSQRT 指令**:计算一组数字的平方根倒数。
尽管 Intel 和 AMD 均声明其相对误差小于 $1.5 \times 2^{-12}$,但这些近似实现仍可能导致微小偏差。在长时间模拟中,这些微小差异会累积并导致严重的不同步。虽然 AMD 的平均精度通常更高,但这种不确定性对于锁步架构是致命的。
许多游戏通过开启编译器的“严格模式”来限制激进优化,或完全切换到定点算术(如《星际争霸 II》)来解决此问题。但 Factorio 选择了更复杂的路径:**保留浮点数以获取其精度和范围,同时标准化每一个数学操作**。
开发团队实现了自定义的三角函数库,并确保所有数学计算严格按照定义的顺序执行,绕过了系统标准库。这使得无论是在 x86 还是 ARM 架构上,计算结果都能达到**逐位(bitwise)一致**。这是一项巨大的工程投入,但保证了大规模模拟所需的精度与确定性。
### 随机数的规范化
另一个非确定性来源是伪随机数生成器(PRNG)。计算机无法生成真正的随机数,而是基于种子(Seed)生成看似随机的数字序列。只要所有玩家从相同的种子开始,他们应提取相同的随机数序列。
然而,实现细节可能导致问题。在许多编程语言中,函数参数的求值顺序未严格定义。例如,一个接收两个参数的 PRNG 调用,不同编译器可能从左到右或从右到左求值。这种细微的序列偏移会导致 PRNG 状态发散,最终导致模拟不同步。Factorio 通过严格规范代码逻辑,消除了这类编译器行为带来的不确定性。
## 固定 Tick 率与延迟处理
为了匹配玩家间的模拟节奏,Factorio 采用固定的 **60 UPS**(Updates Per Second,每秒更新次数),这也对应了 60 FPS 的上限。即使硬件性能允许更高帧率,游戏也被锁定在此限制下。对于不需要极速反应的自动化游戏而言,这是一种可接受的权衡,旨在确保网络同步的稳定性。
为了解决锁步架构带来的输入延迟,Factorio 使用了**延迟状态缓冲区**(Latency State Buffer)。
* **客户端预测**:通过复制当前游戏状态并应用本地输入,向玩家展示即时反馈,从而掩盖网络延迟。
* **缓冲区溢出**:若缓冲区满,可能意味着 CPU 无法跟上模拟速度,或同步时间过长导致网络吞吐量不足。
## 从 P2P 到服务器-客户端模型
Factorio 早期采用点对点(P2P)架构,但这带来了诸多挑战:
1. **拓扑维护困难**:需要处理 NAT UDP 打洞(NAT Traversal)、中继服务器连接以及动态加入/断开连接的事件。
2. **代码复杂性**:正如特里·戴维斯(Terry Davis)所言:“傻瓜欣赏复杂性,天才欣赏简洁。”复杂的 P2P 代码容易引入难以调试的 Bug。
3. **状态同步负担**:在新玩家加入时,P2P 难以高效存储和传递完整的游戏状态。
4. **广播效率低下**:在 P2P 中,每个玩家产生的输入需广播给所有人,随着玩家数量增加,数据包数量呈指数级增长,违背了“低带宽”的初衷。
在 FFF 147 更新中,Factorio 重写了多人游戏系统,转向**服务器-客户端模型**(尽管仍保留了一些 P2P 特性,如玩家托管服务器)。新架构引入了以下关键机制:
### 超级数据包(Mega Packet)
服务器在每一 tick 中收集来自所有玩家的输入,将其打包成一个优化的数据结构,然后一次性向所有客户端广播这个“真相之源”。这将混乱的网状连接转化为干净、同步的星型结构。无论有多少工程师在线,每个客户端只需与服务器通信。
### 连接握手与心跳
* **心跳机制**:玩家定期发送心跳包以保持连接活跃。
* **握手流程**:包括连接请求、连接响应、客户端确认收到响应、服务器最终确认。这一过程不仅是建立连接,也是抵御 DDoS 攻击的基本措施。
### Tick 闭合(Tick Closures)
Tick 闭合允许客户端声明其在当前 tick 内生成了固定数量的输入。这为服务器提供了一个清晰的验证窗口,确认客户端是否完成了模拟任务。
### 循环冗余校验(CRC)
为了防止作弊或同步错误,每个客户端需计算当前游戏状态的哈希值(校验和)。
* **启发式优化**:由于遍历数百万实体的完整状态耗时过长,CRC 仅计算关键部分(如逻辑网络、库存状态)。
* **失败处理**:若校验失败,客户端必须像新加入的玩家一样重新下载整个游戏状态。
## 结论:沉默的网络
Factorio 的网络架构证明了“数学支持的信任”的力量。它不通过网络传输庞大的状态数据,而是传递清晰的**意图**(输入)。这种纪律性使得游戏能够处理数千个活跃实体而不压倒网络连接。
在一个通常因规模而导致不同步和滞后的游戏类型中,Factorio 表明,只要基础足够确定,大规模模拟可以在数百小时内保持完美同步。其核心哲学正如视频结尾所言:**有时,最有效的网络是尽可能保持沉默的网络。**
Source: [How Factorio Syncs A Million Objects over the network](https://www.youtube.com/watch?v=0FHSZ1hani0)
相似文章
Factorio 2.1 实验版本发布
Factorio 发布了 2.1 实验性更新,包含新改动和详细的更新日志。玩家可通过 Steam 或官网参与测试,同时需注意存档兼容性及可能的模组问题。
使用AI编写10万行Rust代码的心得(2025)
一位开发者分享了使用AI编程助手构建一个基于Rust的10万行多Paxos共识引擎的心得,实现了显著的生产力提升和性能改进。
广义同步的局限:架构、权衡与决策因素的分类体系
本论文来自阿尔托大学,提出了同步架构的分类体系,分析了权衡与决策因素,以指导广义同步引擎的设计。
Linear 为何如此快速?技术剖析
本文对项目管理工具 Linear 如何实现快速性能进行了技术剖析,通过使用浏览器端数据库(IndexedDB)、本地优先变更和同步引擎,消除了用户交互中的网络延迟。
你的AI编程代理是否会互相干扰?
AvailSync 是一个小型 MCP 工具,通过在工作前检查仓库可用性,防止多个 AI 编程代理发生冲突。