How to run a city builder on 16 MHz
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
How to run a city builder on 16 MHz. Do you feel spoiled by modern hardware? I know I do. Even the cheapest CPUs have at least 4 cores nowadays, running billions of operations per second. The idea that you could run any software on less than a GHz almost seems like fiction. Yet, there was a time when whole games ran at hardware a thousand times less powerful than that.
如何在 16 MHz 的频率下运行城市建造游戏?你是否觉得自己被现代硬件宠坏了?我知道我是。如今,即使是最便宜的 CPU 也至少拥有 4 个核心,每秒可执行数十亿次运算。在低于 1 GHz 的频率下运行任何软件的想法简直像是天方夜谭。然而,曾几何时,整个游戏都是在性能只有现在千分之一的硬件上运行的。
When thinking about impressive technical achievements in retro games, most people will first point to graphical milestones set by action games like Doom or Quake. Rightfully so, but at least I am personally far more impressed by the early examples of the city builder genre. Simulating whole economies on a large scale with a fraction of today’s power seems impossible if modern builder games even struggle on modern machines.
当想到复古游戏中的惊人技术成就时,大多数人首先会指出《毁灭战士》(Doom)或《雷神之锤》(Quake)等动作游戏所设定的图形里程碑。这确实无可厚非,但就我个人而言,我对城市建造类游戏的早期作品印象更为深刻。如果现代建造游戏在现代机器上运行都显得吃力,那么用今天一小部分的算力来大规模模拟整个经济系统似乎是不可能的。
Of course this didn’t stop developers in the past to develop and run city builder games on significantly weaker hardware. The original SimCity for example lists Intel’s 386SX as the CPU in it’s system requirements. A CPU which at the time of it’s launch ran at magnificent 16 MHz. (Note: an earlier version of this text mistakenly assumed that it ran on 12.5 MHz, which was the clock rate of base version of CPU, not the SX version. Whoops.)
当然,这并没有阻止过去的开发者在性能弱得多的硬件上开发和运行城市建造游戏。例如,初代《模拟城市》(SimCity)的系统需求中列出的 CPU 是 Intel 386SX。这款 CPU 在发布时的主频仅为 16 MHz。(注:本文的早期版本曾错误地认为它是 12.5 MHz,那是基础版 CPU 的时钟频率,而非 SX 版本。抱歉。)
The idea of running a full game, especially one in the traditionally CPU-heavy genre of city builders? Pure magic from a modern point of view. So, why could Maxis run a full city simulation on this ancient hardware, when nowadays even simple idle games require multiple gigahertz of CPU power? Of course there are a lot of reason for this, most of them external (OS ecosystem etc.) but this doesn’t mean that there are no clever optimizations done in the game code itself.
运行一款完整的游戏,尤其是属于传统上 CPU 密集型的城市建造类游戏,从现代的角度来看简直是纯粹的魔法。那么,为什么 Maxis 能够在如此古老的硬件上运行完整的城市模拟,而如今即使是简单的放置类游戏也需要数 GHz 的 CPU 算力呢?当然,这有很多原因,其中大部分是外部因素(操作系统生态系统等),但这并不意味着游戏代码本身没有进行巧妙的优化。
Luckily, this source code was made public and released under the GPL license in 2008, under the name Micropolis. Don’t be confused by the name, this is still the code of the original name, but since only the code was made open source, not the trademark, it has to be called differently. But even under a different name, this is a precious resource, since we can exactly see how the game managed to run so well.
幸运的是,该源代码于 2008 年以 Micropolis 的名称在 GPL 许可下公开。不要被这个名字迷惑,这仍然是原版游戏的源代码,但由于只有代码开源而非商标,所以必须改名。但即使换了名字,这也是一份宝贵的资源,因为我们可以确切地看到这款游戏是如何做到运行得如此流畅的。
As is often the case when analyzing software performance, we can ignore 90 percent of the code. Our focus is on the core of the city simulation, the components that update systems such as traffic, fire, power, and crime. And there is indeed one single function responsible for doing all of this: The Simulate() function in s_sim.c. As the comment implies, this function is called every frame and the function itself then calls the update functions of the various game systems, like CrimeScan(), FireAnalysis(), CollectTax etc.
在分析软件性能时,我们通常可以忽略 90% 的代码。我们的重点是城市模拟的核心,即更新交通、火灾、电力和犯罪等系统的组件。确实有一个单一的函数负责处理所有这些工作:s_sim.c 中的 Simulate() 函数。正如注释所暗示的那样,该函数在每一帧都会被调用,然后它会调用各种游戏系统的更新函数,如 CrimeScan()、FireAnalysis()、CollectTax 等。
The catch is that calling all those functions every frame would overwhelm our poor 1989 CPU. The solution: Not calling every function every frame. Yep, SimCity just skips most of the simulation. Did you see the mod16 parameter of the Simulate function? Where you would normally expect a deltaTime? This number loops through the numbers 0 to 15 (you probably knew this already because of the name, didn’t you?), and is then used to decided which part of the simulation is run in this specific frame.
问题在于,每一帧都调用所有这些函数会压垮我们可怜的 1989 年 CPU。解决方案是:不要在每一帧都调用每个函数。没错,《模拟城市》只是跳过了大部分模拟过程。你看到 Simulate 函数中的 mod16 参数了吗?在通常应该出现 deltaTime 的地方?这个数字在 0 到 15 之间循环(你可能因为名字已经猜到了,对吧?),然后被用来决定在这一特定帧中运行模拟的哪一部分。
So, obviously this makes the Simulation much faster to calculate, but how does this affect the gameplay? Well in case of SimCity, I would say barely. On average the simulation of an aspect takes 8 frames to react to the player’s action, 15 frames in the worst case. As an input lag for a shooter this would be obviously a disaster, but for the simulation of a city? I’d argue it might actually feel better than an instant reaction.
显然,这使得模拟的计算速度快得多,但这会如何影响游戏体验呢?在《模拟城市》的情况下,我认为几乎没有影响。平均而言,模拟系统对玩家操作的反应需要 8 帧,最坏的情况下需要 15 帧。对于射击游戏的输入延迟来说,这显然是一场灾难,但对于城市模拟呢?我认为它实际上可能比即时反应感觉更好。
So far this optimization seems very specific for one kind of type of game, but actually it is the starting point for a lot of optimizations. Let’s look at another example, this time from Assetto Corsa (2014). As a racing games one of it’s visual requirements is to render cars in a shiny way. To render those reflections the game uses the very common technique of regularity updated cubemaps. For a more detailed explanation what those are you can see my article on reflection rendering.
到目前为止,这种优化似乎非常针对某一类游戏,但实际上它是许多优化的起点。让我们看看另一个例子,这次来自《神力科莎》(Assetto Corsa,2014)。作为一款赛车游戏,其视觉要求之一是渲染出汽车的光泽感。为了渲染这些反射,游戏使用了非常常见的定期更新立方体贴图(cubemaps)技术。关于这些技术的详细解释,你可以查看我关于反射渲染的文章。
Since updating a whole cubemap every single frame would be a way to expensive task, the game uses a very similar trick as SimCity: It doesn’t update all 6 faces of the cubemap at once, but only a few, depending on the reflection quality setting. The settings UI doesn’t say so explicitly, but looking in the .ini file, we see the effect on the variable named FACES_PER_FRAME.
由于每一帧都更新整个立方体贴图是一项极其昂贵的任务,游戏使用了与《模拟城市》非常相似的技巧:它不会一次性更新立方体贴图的所有 6 个面,而是根据反射质量设置只更新其中几个。设置界面没有明确说明这一点,但查看 .ini 文件,我们可以看到名为 FACES_PER_FRAME 的变量所产生的影响。
So far, we have stuck to the concept of distributing work across multiple frames by cutting longer tasks into fixed-size portions. For other tasks, it might be smarter to also determine the size and contents of those portions at runtime. For rendering tasks, this could be determined by factors such as screen size or the time since the last update. This explains why Unreal’s Lumen looks so good: It doesn’t need to recalculate all the lighting every frame.
到目前为止,我们一直坚持通过将较长的任务切割成固定大小的部分,从而将工作分配到多个帧中的概念。对于其他任务,在运行时确定这些部分的大小和内容可能更明智。对于渲染任务,这可以由屏幕尺寸或自上次更新以来的时间等因素决定。这就解释了为什么虚幻引擎的 Lumen 看起来如此出色:它不需要在每一帧都重新计算所有光照。
This is arguably also the reason why its results can look weird sometimes, for example, when reflections are not fully updated, like here: Although such frame-distribution optimizations are most common in rendering code, I hope the SimCity example shows that they can work in other areas as well. The next time you work on gameplay code, ask yourself: “Do I really need to run all this code every frame?”
这可能也是为什么它的结果有时看起来很奇怪的原因,例如,当反射没有完全更新时,就像这里一样:虽然这种帧分配优化在渲染代码中最常见,但我希望《模拟城市》的例子能表明它们在其他领域同样有效。下次当你编写游戏逻辑代码时,问问自己:“我真的需要每一帧都运行所有这些代码吗?”