BLOGブログ

2026.09.09UEUE/ C++

[UE5] Subsystemにおける初期化順と依存関係

執筆バージョン: Unreal Engine 5.8

はじめに

皆さん、Unreal EngineのSubsystemは活用していますか?

Subsystem は楽に作れて便利な反面、つい油断してしまうと管理クラスである Subsystem(いわゆるシングルトンの〇〇Managerクラス)が1つのプロジェクトに大量に出来てしまうといった事態が発生してしまいますよね。

そのため、クラスを用意する場合は本当に必要かどうかきちんと設計を考えてから作成していきましょう。

さて、今日はそんな Subsystem の初期化についてのお話です。

Subsystem同士の依存について

1つのプロジェクトに同じ Subsystem 種別( GameInstanceSubsystem や WorldSubsystem 等)の Subsystem が複数存在する場合、それぞれの Subsystem 同士が独立した設計になっていればとくに問題は発生しません。しかし、同じ種別同士の Subsystem に依存関係が存在している場合、実装において注意すべき点が発生します。

例えばRPGを作成する場合…

プレイヤーのレベルや所持金、所持アイテムなどプレイヤーのデータを管理する UPlayDataSubsystem という GameInstanceSubsystem を用意したとします。

次に、セーブデータを保存したり読み込んだりする機能をまとめたセーブデータ管理用の USaveGameSubsystem という GameInstanceSubsystem を作成したとします。

この時、セーブデータを管理する USaveGameSubsystem が、 UPlayDataSubsystem が保持しているデータをセーブしたり、ロードしたデータを UPlayDataSubsystem に反映したりしたいとなった時、この2つのクラス間に依存関係が発生することになります。

 

具体的な実装として、もし USaveGameSubsystem の初期化時に「まず最初にセーブデータをロードして、次に UPlayDataSubsystem にセーブデータの内容を反映したい!」となった場合、実装において注意すべきことが発生します。それは初期化順についてです。

USaveGameSubsystem の初期化時に UPlayDataSubsystem を取得し利用する必要があるということは、つまり先に UPlayDataSubsystem の初期化、厳密にはインスタンスの生成と初期化が完了し、UPlayDataSubsystem が利用できる状態になっている必要があるということです。

 

初期化順の解決

同じ種別の Subsystem の初期化順に関して、とくに依存関係を明示的に指定しなかった場合、Unreal Engine はその順番を保証していません。

そのため、ある環境でたまたま UPlayDataSubsystem が先に初期化されていたとしても、その順番を前提として実装をしてはいけません。

そこで、Subsystem の依存関係を明示的に指定するために利用できるのが、USubsystem クラスの Initialize関数の引数として渡される FSubsystemCollectionBase クラスです。

下記がエンジン側の Subsystem クラスのソースコードで、初期化処理を抜き出したものになります。

 

 

FSubsystemCollectionBase クラスは、複数存在する Subsystem を管理するクラスです。

この FSubsystemCollectionBase は、GameInstance クラスや World クラスがそれぞれで保持しており、例えば GameInstance であれば GameInstanceSubsystem を派生した全ての Subsystem をこの FSubsystemCollectionBase で管理しています。

そこで、この FSubsystemCollectionBase の InitializeDependency関数を利用することで、依存先の Subsystem がまだ初期化されていなかった場合、その Subsystem を初期化してから、現在の Subsystem の初期化を続行することができます。

つまり、「USaveGameSubsystem の初期化には UPlayDataSubsystem が必要なので、先に UPlayDataSubsystem を初期化してください」と引数の Collection を経由し、GameInstance の FSubsystemCollectionBase に伝えるわけです。

実装

実際に FSubsystemCollectionBase を利用し、USaveGameSubsystem の初期化中に UPlayDataSubsystem の初期化を呼び出す実装方法を見ていきましょう。

まずは依存元である UPlayDataSubsystem のhファイルとcppファイルです。

 

そして USaveGameSubsystem のhファイルとcppファイルです。

USaveGameSubsystem::Initialize関数内で、最初に Collection.InitializeDependency<UPlayDataSubsystem>() を呼び出しています。

これにより明示的に USaveGameSubsystem が UPlayDataSubsystem に依存している事を FSubsystemCollectionBase に伝え、必要であればこの時点で UPlayDataSubsystem が初期化されます。

そうして、以降の処理での GetSubsystem関数では初期化が完了して有効になった UPlayDataSubsystem のインスタンスが取得できるようになるというわけです。

ちなみに InitializeDependency関数を使う場合は、必ず Subsystem の Initialize関数内で使用して下さい。他のタイミングでは意図通りに動かない旨がエンジン側のコメントに記載されています。

注意点

FSubsystemCollectionBaseを跨いだ依存関係には使えない

この FSubsystemCollectionBase を活用する方法ですが、InitializeDependency関数自体は、別にプロジェクト全体の Subsystem の初期化順を一律で固定する機能ではありません。

そもそも FSubsystemCollectionBase は GameInstance クラスや World クラス等がそれぞれで管理を行っているため、GameInstanceSubsystem と WorldSubsystem とで異なる FSubsystemCollectionBase 間での依存関係を解決することはできません。

あくまで同じ FSubsystemCollectionBase によって管理されている Subsystem 同士で、「この Subsystem を初期化するためには、あの Subsystem が先に初期化されている必要がある」という依存関係を FSubsystemCollectionBaseに伝えるための機能です。

GameInstanceSubsystem同士の初期化順の制御 OK
WorldSubsystem同士の初期化順の制御 OK
GameInstanceSubsystemとWorldSubsystemの初期化の制御 NG

循環依存の懸念

UPlayDataSubsystem に関し、現在の依存関係から更に実装を進めて行くうちに、循環依存になってしまった場合のお話です。

今の実装のまま、もし逆に UPlayDataSubsystem の初期化中に USaveGameSubsystem を呼ぶような実装をしてしまった場合どうなるでしょうか?

仮に初期化順は、USaveGameSubsystem の Initialize関数が先に実行されるとします。

すると、先ほどの実装通り初期化前に、依存している UPlayDataSubsystem の初期化処理が実行されます。

そして UPlayDataSubsystem の Initialize関数内では、USaveGameSubsystem はまだ初期化処理中にも関わらず、インスタンス自体は生成されているため、GetSubsystem<USaveGameSubsystem>()は nullptrではなく、初期化途中のインスタンスを返してしまうのです。

ここで重要なのは、「Subsystem のインスタンスが存在すること」と「Subsystem の初期化が完了していること」は別であるという点です。

つまり、GetSubsystem() が nullptr でないからといって、USaveGameSubsystem が初期化が完了し、利用できる状態であるとは限らないということです。

そのため、循環依存の関係になっている場合はとても注意が必要です。

循環依存への対応

まず、循環依存に関しては、InitializeDependency関数で解決しようとするのではなく、可能であれば設計そのものを見直すべきです。

それでもそれぞれの初期化中にそれぞれの Subsystem を使用したい場合、初期化処理を段階で分けるという手段があります。

ただし、これは Subsystem 自体に用意された標準の仕組みではなく、プロジェクト側で初期化フェーズを設計する方法です。

まず Initialize関数では、依存関係に関係のない初期化処理(プロパティの初期化など)を書き、別途 PostInitialize関数などを独自に用意し、それをプロジェクト側の任意のタイミングで呼び出すという手法です。

先述した通り、UGameInstanceSubsystem は UGameInstance の FSubsystemCollectionBase によって管理されており、GameInstance の初期化処理である Init関数内で各 Subsystem の Initialize関数を呼び出しています。

そのため、プロジェクト側で派生した独自の GameInstance の Init関数の中で、独自に用意した PostInitialize関数を、任意の順番で呼び出すことで問題を解決することが可能です。

以下が実際のソースコードになります。

ここでは省略して USaveGameSubsystem のみ掲載していますが、UPlayDataSubsystem にも同様に追加します。

そしてプロジェクト側で用意した GameInstance クラスの初期化処理で追加した PostInitialize関数を任意の順番で呼び出します

まとめ

Subsystem は便利な一方、きちんと設計を考えないと、気付くと大量に作られたり、思わぬ依存関係でトラブルを起こす可能性が潜んでいます。

まずは設計段階できちんとクラスの必要性や依存関係を整理しつつ、実装する際は初期化順を意識しながら実装していきましょう。