HN 日本語サマリー

← 一覧へ戻る
プログラミング

16 MHzでシティビルダーを動かす方法

How to run a city builder on 16 MHz (larstofus.com)

79 pointsby rex6423 コメント

要約

この記事では、現代のハードウェアと比較して非常に低速な16 MHzのCPUでシティビルダーがどのように動作していたのかを解説しています。オリジナルのSimCityが、フレームごとにシミュレーションの全要素を更新せず、一部をスキップするという巧妙な最適化手法を用いていたことを、公開されたソースコードMicropolisを分析することで明らかにしています。このフレームごとの処理分散は、現代のゲーム開発におけるレンダリング最適化などでも応用されている技術です。

全文翻訳

16 MHzでシティビルダーを動かす方法

現代のハードウェアに甘やかされていると感じますか?私はそう感じます。今日の最も安価なCPUでさえ、少なくとも4コアを持ち、毎秒数十億回の演算を実行します。1 GHz未満でソフトウェアを実行できるという考えは、まるでフィクションのように思えます。しかし、かつてはハードウェアがその千倍も非力な時代に、ゲーム全体が動作していました。

[画像: 昔のコンピューターの画像]

レトロゲームにおける印象的な技術的成果について考えるとき、多くの人はまずDoomやQuakeのようなアクションゲームが設定したグラフィカルなマイルストーンを挙げるでしょう。それはもっともですが、少なくとも私個人としては、シティビルダーというジャンルの初期の例に、より感銘を受けています。今日のパワーのほんの一部で大規模な経済をシミュレートすることは、現代のビルダーゲームでさえ最新のマシンで苦労していることを考えると、不可能に思えます。

もちろん、これは過去の開発者が著しく非力なハードウェアでシティビルダーゲームを開発・実行することを妨げませんでした。例えば、オリジナルのSimCityのシステム要件には、Intelの386SXというCPUが記載されています。このCPUは、発売当時、素晴らしい16 MHzで動作していました。

(注:このテキストの初期バージョンでは、12.5 MHzで動作すると誤って想定していましたが、それはCPUのベースバージョンのクロックレートであり、SXバージョンではありませんでした。やれやれ。)

フルゲーム、特にシティビルダーという、伝統的にCPU負荷の高いジャンルのゲームを動作させるという考え?現代の視点から見れば、純粋な魔法です。

では、なぜMaxisは、今日では単純なアイドルゲームでさえ数ギガヘルツのCPUパワーを必要とするのに、この古いハードウェアで完全な都市シミュレーションを実行できたのでしょうか?

もちろん、これには多くの理由がありますが、そのほとんどは外部的なもの(OSエコシステムなど)です。しかし、これはゲームコード自体に巧妙な最適化が施されていないという意味ではありません。幸いなことに、このソースコードは2008年に公開され、Micropolisという名前でGPLライセンスの下でリリースされました。名前で混乱しないでください。これはオリジナルのゲームのコードですが、ソースコードのみがオープンソース化され、商標はそうではなかったため、異なる名前で呼ばれる必要があります。しかし、異なる名前であっても、これは貴重なリソースです。なぜなら、ゲームがどのようにしてうまく動作できたのかを正確に見ることができるからです。

ソフトウェアのパフォーマンスを分析する際によくあるように、コードの90パーセントは無視できます。私たちの焦点は、都市シミュレーションのコア、つまり交通、火災、電力、犯罪などのシステムを更新するコンポーネントにあります。

そして、これらすべてを実行する責任を負う単一の関数があります:s_sim.c の Simulate() 関数です。

[画像: s_sim.c の Simulate() 関数のコードスニペット]

コメントが示唆するように、この関数は各フレームで呼び出され、その後、CrimeScan()、FireAnalysis()、CollectTax などのさまざまなゲームシステムの更新関数を呼び出します。

すべての関数を毎フレーム呼び出すと、貧弱な1989年のCPUに過負荷がかかるという問題があります。その解決策は、すべての関数を毎フレーム呼び出さないことです。

そうです、SimCityはシミュレーションのほとんどをスキップしていました。Simulate 関数の mod16 パラメータを見ましたか?通常、deltaTime を期待するところです。この数値は0から15までの数値をループし(おそらく名前からすでに知っていたでしょう?)、この特定のフレームでシミュレーションのどの部分が実行されるかを決定するために使用されます。コードは次のようになります。

[画像: mod16 パラメータを使用した条件分岐のコードスニペット]

(注:これは正確なソースではありません。説明を分かりやすくするためにいくつかの部分を削除しましたが、非常に近いです)。

明らかに、これによりシミュレーションの計算がはるかに速くなりますが、ゲームプレイにはどのように影響するのでしょうか?SimCityの場合、ほとんど影響はないと言えるでしょう。平均して、ある側面をシミュレートするには8フレームかかり、最悪の場合は15フレームかかります。シューターの入力遅延としては明らかに災害ですが、都市のシミュレーションとしてはどうでしょうか?むしろ、即時反応よりも良く感じられるかもしれません。

これまでのところ、この最適化は一種のゲームタイプに非常に特化しているように見えますが、実際には多くの最適化の出発点となります。

別の例を見てみましょう。今回はAssetto Corsa(2014年)からです。

[画像: Assetto Corsa のゲーム画面]

レーシングゲームとして、車の光沢のあるレンダリングはその視覚的な要件の一つです。これらの反射をレンダリングするために、ゲームは定期的に更新されるキューブマップという非常に一般的な技術を使用しています。キューブマップが何であるかについての詳細な説明は、私の反射レンダリングに関する記事をご覧ください。

すべてのフレームでキューブマップ全体を更新するのはコストが高すぎるタスクであるため、ゲームはSimCityと非常に似たトリックを使用しています。それは、反射品質設定に応じて、キューブマップの6つの面すべてを一度に更新するのではなく、数面だけを更新します。

設定UIには明示されていませんが、.iniファイルを見ると、FACES_PER_FRAME という名前の変数にその影響が見られます。

[画像: FACES_PER_FRAME 変数に関する .ini ファイルの抜粋]

これまでのところ、長いタスクを固定サイズのポーションに分割することで、複数のフレームにわたって作業を分散させるという概念に固執してきました。他のタスクでは、これらのポーションのサイズと内容をランタイムで決定する方が賢明かもしれません。レンダリングタスクの場合、これは画面サイズや前回の更新からの時間などの要因によって決定される可能性があります。これがUnrealのLumenが非常に良く見える理由です。毎フレームすべてのライティングを再計算する必要がないからです。これも、例えば反射が完全に更新されていない場合など、結果が奇妙に見えることがある理由であると言えます。

[画像: Lumen の反射が不完全な例]

このようなフレーム分散最適化はレンダリングコードで最も一般的ですが、SimCityの例が示すように、他の分野でも機能する可能性があることを願っています。次にゲームプレイコードに取り組むときは、自問自答してみてください。「本当にこのコードを毎フレーム実行する必要があるのか?」

この記事を楽しんだら、Mastodon、Bluesky、またはLinkedInでフォローするか、この記事のすぐ下でこのブログを直接購読することを検討してください。私はゲームプログラミング、Unreal、ゲーム開発に関する新しい記事を月に約1回公開しています。