独自Engine設計|Part 3:EngineとSubsystemの責務分離
EngineがSubsystemをどう所有し管理するか。参照の返し方と、Game側からRenderingへ渡す情報の形を整理した。
背景
Part 2では、ApplicationがEngineとGameLogicをどの順序で実行するかを整理した。
今回は、Engineが各Subsystemをどのように所有し、初期化・終了・参照提供を行うかを考える。
Engineの役割
Engineは、Input、Physics、Rendering、Time、WindowなどのSubsystemを所有する。現時点では、次のような構成を想定している。
class Engine
{
public:
void Initialize();
void Shutdown();
InputSystem& Input();
LogSystem& Logger();
PhysicsSystem& Physics();
RenderSystem& Renderer();
TimeSystem& Time();
WindowSystem& Window();
private:
InputSystem m_inputSystem;
LogSystem m_logSystem;
PhysicsSystem m_physicsSystem;
RenderSystem m_renderSystem;
TimeSystem m_timeSystem;
WindowSystem m_windowSystem;
};
Engineの主な責務は、各Subsystemそのものの処理を実装することではなく、
- Subsystemを所有する
- 正しい順序で初期化する
- 使用できる期間を管理する
- 必要なSubsystemへの参照を提供する
- 適切な順序で終了する
ことにある。
例えば、EngineはRenderSystem::Initialize()を呼び出すが、DirectX 12のPipeline State ObjectをEngine自身が作るわけではない。Renderingの具体的な処理はRenderSystem側に置く。
初期化と参照提供をまとめる
Engineには、次の2種類の機能がある。
void Initialize();
void Shutdown();
そして、
RenderSystem& Renderer();
PhysicsSystem& Physics();
一見すると、初期化・終了と参照提供は別の責務にも見える。ただし、現在の規模ではどちらも次の役割に含めている。
Engineが所有しているSubsystemを管理する
所有しているから初期化し、所有しているから終了させ、利用側に必要な参照を提供する。
一方で、Engine自身が、
- DirectX 12 Deviceを作成する
- Collision Detectionを実装する
- Input Stateを直接更新する
- ゲーム固有の状態を管理する
ところまで担当すると、責務が広がりすぎる。具体的な処理は、それぞれのSubsystemやGameLogicへ置く。
Subsystemを参照で返す
Subsystemへのアクセスは、参照で返す形にしている。
RenderSystem& Engine::Renderer()
{
return m_renderSystem;
}
利用側は、
m_engine.Renderer().Render();
と書ける。
RenderSystemはDirectX 12のResourceや内部状態を所有するためコピーして使うものではなく、またEngineが有効である間は必ず存在する。参照を返すことで、その2つの前提をInterfaceに表せる。
値返しやPointer返しと比べてなぜこの形にしたのかは、EngineのAccessorはなぜ参照を返すのかで整理している。
Game側からRenderingへ何を渡すか
Game側がDirectX 12のGPU Resourceを直接扱うと、ゲームロジックとRendering APIが強く結びついてしまう。そのため、Game側からは次の情報だけを渡す形を考えている。
何を、どのTransformで描画するか
例えば、次のようなRenderObjectを使う。
struct RenderObject
{
MeshId meshId;
Transform transform;
};
meshIdは描画するMeshを示し、transformは位置・回転・Scaleを示す。Game側は、
RenderObject renderObject{};
renderObject.meshId = meshId;
renderObject.transform = transform;
までを決める。その後、RenderSystemがMeshIdから対応するGPU Resourceを取得し、実際の描画命令を発行する。
これにより、Game側はVertex BufferやCommand ListなどのDirectX 12固有情報を知る必要がなくなる。
RenderSystemも必要に応じて分ける
Subsystemについても、一つのClassへすべての処理を集めないようにする。例えばRenderSystemが大きくなってきた場合、
- GraphicsPipeline
- SwapChain
- CommandContext
- GpuSynchronizer
- MeshResource
といった単位へ分けることができる。
ただし、細かく分割すること自体が目的ではない。数行の処理を別Classへ移しただけでは、ファイル間の移動が増えて、かえって処理を追いにくくなる場合もある。
どこまで分けるかの判断基準は、Subsystemをいつ別クラスとして分けるべきかで整理している。
まとめ
現在は、Engine内部の役割を次のように整理している。
Engine
Subsystemを所有し、Lifetimeを管理する
InputSystem
Inputを扱う
LogSystem
Logを出力する
PhysicsSystem
Physics Simulationを扱う
RenderSystem
Renderingを扱う
TimeSystem
時間を管理する
WindowSystem
Windowを管理する
Engineは各Subsystemをまとめて管理するが、具体的な処理までは担当しない。また、Game側からはDirectX 12などの低レベルAPIを直接扱わず、MeshIdやTransformのようなゲーム側で扱いやすい情報を渡す形にする。
この構成をベースにしながら、実際の3Dアクションゲームを作る中で、Subsystemの責務や分割単位が適切かを検証していく。