0x00 はじめに
ゲームエンジンは、最初はレンダリングや入力を作るのが山場だと思っていました。でも一度動き始めると、本当に難しいのは“つなぎ方”でした。何か一つの技術でできているものではなく、いくつもの専門領域をまとめ上げて、「ゲームを動かすための実行基盤」 を作る総合システムだと感じています。
中身を分解すると、たとえば
- レンダリング(Rendering/レンダリング)
- シェーダ(Shader/シェーダ)
- アセット管理(Asset Management/アセット・マネジメント)
- シーン管理(Scene Management/シーン・マネジメント)
- 入力(Input/インプット)
- 衝突判定・物理(Collision/Physics/コリジョン/フィジックス)
- アニメーション(Animation/アニメーション)
- オーディオ(Audio/オーディオ)
- デバッグ/ツール(Debug/Tools/デバッグ/ツール)
みたいに、いろいろなモジュールに分かれています。
自分が作ってみて特に難しいと感じたのは、「モジュールを用意すること」 よりも、それらをどう繋げて破綻しない形にするかでした。
結局のところ、依存関係・更新順序・データの流れ(Data Flow) の作り方が、エンジン全体の手触りや品質を大きく左右します。
レンダリングで言えば描画パイプラインやリソースの生存期間管理など、細部の判断が積み重なって全体の構造に跳ね返ってきます。
アセット管理も同様で、ロード方式(同期/非同期)、キャッシュ、参照管理、データ駆動(Data-Driven/データ・ドリブン)まで含めて考えないと、実運用で耐えられる形になりません。
このシリーズは、そうした自分の 「分からなさ」 や 「詰まりどころ」 を出発点にして、ゲームエンジンを学習のために分解して理解する記録として書いていきます。参照するのは、Unity のような商用エンジンの思想と、Godot のようなオープンソースエンジンで実装まで追える事例です。更新ループ、シーン/ノード構造、リソース管理、描画パイプラインといった仕組みを見比べながら、「なぜこの形が選ばれているのか」を自分なりに追っていきます。
整理の軸にしたいのは、機能の網羅ではなく、なぜその設計にするのか/何を捨てて何を取る のかという判断の理由です。拡張性と複雑さ、実装コストと表現力、汎用性と最適化といったトレードオフを、具体例に照らして言語化してみる。そして、その整理を土台に、自作エンジンのアーキテクチャ設計と方法論(設計判断の基準、分割、依存、データフロー) を少しずつ更新していく――それがこのシリーズの目的です。
0x01 全体のアーキテクチャ構成
以上の考えを踏まえると、僕の設計では「まず全体を分割して見通しを作る→次に依存関係と更新順序を固定する」という順番で、アーキテクチャを組んでいく方針になります。現時点の大枠は、だいたい次のような形を想定しています。
+======================================================================+
| A) apps/runtime (Entry) |
+----------------------------------------------------------------------+
| main(): |
| svc = B) PlatformFactory.CreateServices() [A->B] |
| eng = C) EngineCore(svc.IFs...) [A inject C] |
| app = D) GameApp(eng.Services...) [A create D] |
| eng.Run(app) [A->C] |
+======================================================================+
|
| [C runs main loop]
v
+======================================================================+
| C) EngineCore (engine/) |
+----------------------------------------------------------------------+
| Run(app): |
| while (running) { |
| E) InputSystem.Update() (uses K) IInputDevice) |
| F) SceneManager.Update(dt) (drives scenes/world) |
| |
| G) RenderManager.BeginFrame() |
| F) SceneManager.Render( H) RenderContext ) |
| G) RenderManager.EndFrame() -> (uses J) IRenderAPI) |
| } |
+======================================================================+
| uses | uses | uses
v v v
+-----------------------+ +-------------------------+ +-----------------------+
| E) General Systems | | F) Scene/World Layer | | I) Asset Pipeline |
| (systems/) | | (SceneManager + World) | | (engine Asset + |
| - Input/Collision/... | | - SceneStack | | importers) |
+----------+------------+ +-----------+-------------+ +----------+------------+
| | |
| Update uses I/F only | Render submits draw reqs | Load bytes
| | (no DX/Vulkan) | via IFileSystem
v v v
+-----------------------+ +-------------------------+ +-----------------------+
| K) IInputDevice | | H) RenderContext | | L) IFileSystem |
| (engine IF) | | (engine IF) | | (engine IF) |
+----------^------------+ +----------^--------------+ +--------^--------------+
| implements | provided by | implements
| | |
| | |
+======================================================================+
| B) PlatformFactory + Platform Adapters (platform/) |
+----------------------------------------------------------------------+
| CreateServices(): |
| returns { |
| J) IRenderAPI <- M) Dx12RenderAPI / VulkanRenderAPI / ... |
| K) IInputDevice <- N) WinInputAPI / SdlInputAPI / ... |
| L) IFileSystem <- O) StdFileSystem / WinFileSystem / ... |
| (optional) IAudioDevice <- P) XAudioDevice / ... |
| } |
+======================================================================+
|
| only here touches external SDKs
v
+======================================================================+
| Q) external/ + OS/SDK (3rd party) |
+----------------------------------------------------------------------+
| DX12 / SDL / Win32 / Vulkan / XAudio2 / ... |
+======================================================================+
A) apps/runtime は Platform を「呼び出して描画」しません。やるのは B) を呼んで IF 群を作り、C) に注入するだけです(DI)。
B) PlatformFactory は Engine が定義した IF(J/K/L…)の実装を返す窓口です。ここだけが Q) 外部SDKに触れます。
C) EngineCore は IF だけを使って、Update/Render の流れを統制します。
E) Systems は K) IInputDevice 等の IF にだけ依存し、OS/SDK を知りません。
F) Scene/World はゲーム側の実体を更新し、描画は H) RenderContext に投げるだけ(DX12直呼びなし)。
G) RenderManager はキュー集約・DrawCommand化を担当し、最後に J) IRenderAPI へ Submit/Present を委譲します。
I) Asset Pipeline は L) IFileSystem 経由でバイト列を取得し、Importer(ゲーム側登録)で JSON を解釈します。
0x02 なぜそこまで複雑に設計する必要なのか
もちろん、1本ゲームを作るだけ、あるいは GameJam のような短期開発が目的なら、ここまで複雑な設計は必須ではありません。「初期化→入力→更新→描画→終了処理」 という最小ループでも、小さなゲームは十分に成立します。
ただし、開発規模が上がるほど重要になるのは、機能を増やすことではなく、複雑さを“増やさずに扱える形”にすることです。
エンジンの役割は、レンダリングや衝突判定のような技術要素を単に隠すというより、責務を分離し、安定した入口(API)とデータの流れを用意して、開発者が考えるべき範囲を狭めることにあります。結果として、ゲーム側は「どう描くか/どう当てるか」の細部を毎回ゼロから抱えずに済み、より本質的なゲームプレイや体験設計に集中しやすくなります。
さらにチーム開発では、アセット形式やツールのワークフローまで含めて「どの成果物を、どの形で受け渡すか」という“契約”を揃えることで、作業の噛み合わせが良くなり、変更にも耐えやすくなります。(※ただし、このシリーズの主題は協業論そのものではなく、設計判断の軸と依存関係の整理に置きます。)
0x03 最後
まだ開発途中ですが、設計の試行錯誤も含めてGitHubで公開しています。
完成品の提示というより、「どう壊れて、どう直したか」を追える形にしたいので、履歴(commit)も含めて見てもらえると嬉しいです。