
打完一局帝国时代2:征服者,习惯性地点了"保存录像"。
文件大小是几百 KB。
而这局打了将近一个小时。
同样的时长,如果用录屏软件录下来,几个 GB 是起步。
差了四个数量级。
为什么?
因为那个文件里装的根本不是画面。
先对比一下两种做法的差别。
录屏软件做的事,是每秒钟把整个屏幕的像素抓下来几十次。
1920×1080 的画面,单帧就是 200 多万个像素——每个像素 3 个字节表示颜色。
一秒钟 30 帧,数据量就是 180MB 量级。
即使上压缩算法,也压不掉多少——因为每一帧的画面都不一样。
而游戏录像记录的东西完全不同。
它记的是:你在第几秒,选中了哪些单位,下达了什么命令。
一条指令大概长这样:
几十个字节。
一局比赛下来,玩家总共下达成百上千次操作。
乘起来,也就是几百 KB 的量级。
这就是体积差的来源——一个记的是结果,一个记的是原因。

这是这套方案里最关键的一环。
录像文件里没有画面,但播放时你确实看到了画面。
那些画面是播放的时候现算出来的。
游戏引擎重演一遍比赛:
第一步,按录像里的地图和参数,把初始局面搭出来。
第二步,按时间顺序把指令一条条喂给游戏逻辑。
第三步,逻辑算出每个单位该在哪、在干什么,然后渲染出来。
所以你看到的不是"播放录像",是用引擎重新打了一遍。
只不过这次的操作输入,来自文件而不是键盘。
理解了"现算",有些平时觉得理所当然的功能,就有了技术依据。
比如可以从任意玩家视角观看。
因为每个单位的真实位置在逻辑层都是已知的——包括当时被战争迷雾遮住的部分。
迷雾只是渲染时的一层遮挡,数据本身一直都在。
再比如可以随时暂停、快进、跳转。
因为只需要算出那一时刻的状态——不需要"解码"到某帧才能显示。
而录屏文件想跳到中间,就得先解码前面所有帧。
到这里,一个隐含的要求浮现出来了。
既然画面是现算的,那不同机器算出来的结果必须完全一致。
否则就会出现:录像是按 A 的机器算的,你在 B 的机器上播放,单位跑到了别的位置。
这个要求有个名字,叫确定性。
它的含义是:给定相同的初始状态和相同的输入序列,任何一台机器都必须算出完全相同的结果。
注意"完全相同"这四个字。
不是"差不多",不是"误差在可接受范围内"。
必须一个比特都不差。
因为游戏是连续演进的——第 1 帧的微小差异会传导到第 2 帧,然后被放大,几十秒后就面目全非了。
这也是联网对战时必须满足的条件——两个玩家的屏幕上,战场必须一模一样。
而这个要求,直接决定了游戏逻辑该怎么写。
这是最违反直觉的一条。
日常写程序,小数都用浮点数——方便、通用、有硬件加速。
但在需要确定性的游戏逻辑里,浮点数是不能用的。
原因在于:同一个算式,在不同环境下的结果可能不一致。
差异来自好几处:
这些差异通常在最后几位有效数字上。
单看一次计算,完全可以忽略。
但问题是游戏里位置是累加出来的——每帧加上一点速度。
误差会累积。
一场比赛跑几千帧,前面那点可以忽略的偏差,最后可能变成一个单位跑偏了几十个像素。
表现就是:一台机器上单位已经到位了,另一台还差一点。
如果这个单位是关键单位——比如正在采资源的农民,或者正在逃跑的英雄——后续的整条剧情线都会分岔。
既然浮点数不可靠,就换成可靠的。
主流做法是定点数。
思路是:把小数放大若干倍,用整数来存。
比如约定所有数值都放大 1000 倍——那么"1.5"在程序里就是整数 1500。
加减法直接算整数。
乘除的时候多一步:乘完要除回那个倍数,除的时候按整数取整。
关键就在于取整规则是明确的、和平台无关的——比如统一用向下取整。
这样同一组输入,在任何机器上都会得到同一个整数结果。
除了数值类型,还有几处也得改:
最后一条容易被忽略。
如果代码里遍历一个哈希表——不同平台、不同版本的元素顺序可能不一样。
那遍历过程中做的计算,顺序就不同了。
而浮点加法不满足结合律——先加 A 再加 B,和先加 B 再加 A,结果可能差最后一位。
虽然换成定点数之后这个问题会缓解,但顺序问题本身仍需注意。
所以游戏逻辑通常要求使用有序容器,或者在遍历前排序。
玩过的人可能记得:游戏更新之后,旧录像可能打不开了。
或者能打开,但播放到一半就乱套——单位乱跑、建筑造错位置。
这不是文件损坏,而是这套机制本身的副作用。
原因是:录像里只有指令,没有画面。
要还原那场比赛,得用和当时完全相同的游戏逻辑去重演。
如果补丁调整了某个单位的移动速度——哪怕只改了一点点,重演出来的结果就完全不同了。
农民走到资源的路径变了,采集节奏变了,后续的建造顺序也跟着变。
从某个时间点开始,重演的战场就和当年的真实战场分道扬镳了。
所以每次改动了影响游戏逻辑的内容,旧录像就不再可用。
这也是为什么这类游戏通常会把录像和版本号绑定——打开时先校验版本是否匹配。
顺带说一句,这套机制和联网对战是共用的。
对战时的做法叫帧同步:
每个客户端只把自己的操作发给服务器。
服务器收齐所有玩家的操作,打包广播出去。
每个客户端拿到所有人的操作,各自在本机跑一遍游戏逻辑。
注意——服务器不计算游戏状态,它只负责转发和收集指令。
这和很多人的直觉相反——RTS 的服务器并不"模拟这场战斗",它只是传递操作。
这么设计的原因是:如果服务器要实时计算几百个单位的战斗,负载会非常大。
而只转发指令,成本极低——一局帝国时代2:征服者的对战,数据量和你本地的录像文件差不多。
代价是:所有客户端必须保证算得一样。
也就是前面说的确定性。
一旦某个客户端算出了不同的结果,它和别人的战场就对不上了——这就是所谓的"不同步"。
表现是:你的单位在打敌人,队友屏幕上你的单位在发呆——两边看到的已经不是同一场战斗了。
帝国时代2:征服者 的录像文件小,是因为它记录的不是画面,是操作。
播放时由游戏引擎重新演算一遍,画面是现场渲染出来的。
而支撑这套方案的前提是确定性——相同的初始状态加相同的指令序列,必须算出完全相同的结果。
为了满足这一点,游戏逻辑不能随便用浮点数,要换成定点数;随机数要用固定种子;时间要用帧计数。
这些约束不是性能优化,是正确性要求。
它们同时也解释了另外两件事:为什么版本更新会让帝国时代2:征服者的旧录像失效,为什么联网对战可以让服务器只转发指令、不承担计算。
三者是同一套设计的不同侧面。
安装包地址:
https://www.ijinshan.com/software/diguo2.html?channel=4018
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。