联机不卡却总被踢出重下地图?Factorio 掉同步真正的原因

Factorio 联机用确定性锁步:每个玩家在本地完整模拟整局游戏,联网只传操作,任何微小差异就是掉同步。第 340 期追踪了两个深层案例:Serpent 用 nil 当占位值改了表顺序,以及单位组速度值加载后被重算;再加上隐藏状态、哈希顺序与不变量的老规律。

联机不卡却总被踢出重下地图?Factorio 掉同步真正的原因
本篇导航6 个章节

你和朋友联机建厂,一点都不卡,然后某个人突然被踢出游戏,还被要求重新下载整张地图。网络明明没问题。在 Factorio 里,这就是掉同步的典型表现,而且几乎从来不是网络问题。

Factorio 主视觉图:标志下方是机器、传送带和火车组成的工厂。Steam 商店官方图。

快速信息

  • Factorio,Wube Software 的建厂游戏,用定制确定性引擎,不是 Unity 或 Godot。

  • 模型:确定性锁步。每个玩家都在本地完整模拟整局游戏,联网传的只是玩家操作,不是世界状态。

  • 定义:两个玩家模拟结果有任何微小差异就是掉同步,被踢出的玩家必须重连并重新下载整张地图。

  • 两个深层案例,都出自 Factorio Friday Facts 第 340 期:一个 Serpent 占位值,和一个从没进存档的单位组速度值。

  • 反直觉的规则:网络、延迟、性能问题不会导致掉同步。变慢和掉同步是两个问题,修法完全不同。

  • 来源:Factorio Friday Facts 第 76、188、63、70、242、340 期,均在 factorio.com。

  • 最后核实:2026年10月27日。

不是卡:每个玩家都在重演同一局游戏,其中一局演偏了

Factorio 的联机模型写在 Friday Facts 第 76 期,和大多数玩家的直觉正好相反:没有服务器给每个人发当前世界。每个联网节点都在完整模拟世界上发生的一切,玩家行动时,传给别人的只有这个行动。带宽因此极小——键盘每秒只能产生几百字节,几百个物体的地图和几百万个物体的地图玩起来感觉一样。代价是小误差无法被纠正,只会累积,直到两个玩家看到的游戏完全不同,这就是 Factorio 玩家说的掉同步。同一篇还说了另一个代价:游戏速度受最慢玩家限制,因为一帧要等所有其他节点的输入处理完才能结束,而缓解这个问题的延迟缓冲又会拖慢本地操作。换句话说,变慢是模型自带的,掉同步不是变慢,是跑偏。

Friday Facts 第 188 期写于 0.15 实验版需要玩家帮忙修掉同步的时候,给出了可操作的定义:所有玩家必须每一 tick 都模拟得完全相同,任何一台电脑哪怕只差一点点,就会掉同步,被踢出的玩家要重连并重新下载地图。这篇还给出了让这类 bug 能被修掉的上报方法:掉同步报告里有服务器和客户端的状态可供对比,而最有用的上报要附上玩家当时在做什么、以及复现步骤,比如加载这个存档、等朋友进来、点一下这个机械臂、就掉同步。光一个报告文件、没有上下文,线索价值很弱。

Friday Facts 第 340 期示意图:玩家和建筑无人机互相引用,这正是 Serpent 掉同步核心的循环引用。Factorio 官方博客图。

深层案例一:一个意思等于删除的占位值

Friday Facts 第 340 期调查了两起带 mod 的掉同步报告,那一周玩家上报比平时多,背景是 Krastorio 2 刚发布。第一个案例的起点是个合理的设计:在 Construction Drones 风格的 mod 里,玩家记录自己拥有的无人机,每架无人机也记录属于哪个玩家。运行时互相引用完全没问题,存档时问题来了:Serpent 序列化器遍历 global 里的全部数据,遇到循环引用就先写一个占位值、之后再回来修正。问题在于 Serpent 选了 nil 当占位值,而在 Lua 里把表值写成 nil 就等于删除这个键。占位值在位期间,遍历根本看不到这个键,等 Serpent 回来修正时,由于 Factorio 定制版 Lua 更深层的细节,每个节点存下来的表顺序各不相同。Boskid 的修复实际只有一词之改:把占位值从 nil 改成 0,让迭代顺序在保存和加载后保持一致。原文说问题很深、很难找,修复很简单。两半都重要:一词之改不等于浅 bug。

虫群正走上 Krastorio 2 的 creep 地形,正是这块地面让单位组速度在加载后被重算。Factorio 官方博客截图。

深层案例二:一个从没进存档的缓存速度

第二起报告来自同时用 Krastorio 2 和 Rampant mod 的玩家,表面看很像 Rampant 的错,因为差异全在单位位置,而 Rampant 就是管单位的。真正的原因安静得多:一个单位组会按组内最慢单位缓存一个最大速度值,但这个值没有随存档保存,而是每次加载时重新计算。这条逻辑一直都在,但 0.17 之前单位最大速度只由原型决定,重算结果永远不变。0.17 新增了两项 mod 能力——单位受地形影响、脚本可直接改单位速度——而基础游戏本身不用这两项,问题就一直藏着,直到有 mod 用上。在 Krastorio 2 的 creep 地形上走(这种地形会惩罚单位速度),加载后组算出的最大速度更低,两局模拟就此分叉。修复是让单位在必要时通知组重新计算,而不是信任一个加载时被悄悄改掉的值。两个案例背后的规律是一句话:bug 很少在你看得见的、正在动的逻辑里,而在那些你以为加载后自然会算对的值和顺序里。

更早的规律:隐藏状态、哈希顺序和不变量

第 340 期的两个案例,踩在 Friday Facts 反复讲的更老规律上。第 63 期说,大多数掉同步来自没有正确初始化或保存的隐藏状态,并描述了用 heavy mode 跑可疑存档来猎杀这类 bug 的做法——本文只讲这个思路,因为原文没有给出可照抄的命令或步骤。第 70 期报告了一次动到 Factorio 所用 Lua 深层内部的修复,起因是 mod 按不确定顺序遍历哈希表的值导致掉同步。第 242 期补充了不变量:墙上永远不能再叠一堵墙,因为相邻墙块可能在不同玩家那里连到不同的墙上而掉同步,传送带、管道、铁轨有类似不变量。Factorio 对此的回答是在版本迁移时全量扫一遍地图的一致性检查。对单人开发者来说,能带走的不是 Factorio 的任何工具——那些只属于 Factorio——而是这张清单:存档里的值、固定的顺序、占位值的语义、变化时的通知、写出来并主动检查的不变量,而不是默认它们成立。

周一早上照着做

  • 先给症状分类:变慢、丢包、低帧率都不是掉同步。玩家被踢出且必须重下地图,才怀疑确定性。

  • 拿到可复现的存档和掉同步前玩家确切的操作,按第 188 期要的上报格式来。

  • 把加载当成第一怀疑区间:对比加载前后的状态,猎杀那些靠重算而不是靠存档恢复的值。

  • 凡是顺序有影响的地方都固定迭代顺序,并查清占位值在你的语言里是什么意思:等于删除的占位值就是等着发生的掉同步。

  • 值变了就通知所有缓存过它的地方,不要让依赖方留着过期副本。

  • 写下一条你的游戏绝不能违反的不变量,在加载或迁移时主动检查,别等玩家在联机里撞上。