Unreal Engine におけるモジュール構造とUBTの挙動について
に公開
概要
Unreal Engine で C++を用いた開発を行うとき、まずはじめに出会う概念がモジュールです。Unreal Engine では巨大なプロジェクト(エンジン本体を含む)での機能別の依存関係を適切に管理するために、モジュールと言う仕組みを導入しています。 この仕組みは、Unreal Build Tool という C#製のツールによって実装されており、UBT が開発者の追加したモジュールやエンジンが持っているモジュール、マーケットプレイスえ購入してきたプラグインのモジュールなどを認識し、依存関係を解決することで、Unreal Engine でのビルドを実行しています。
C++ソースを含むモジュールを作成する場合、以下のような構造が基本となります。 (C++モジュールに関連するものだけを記載しています。)
Source
├── ModuleA
│ ├── ModuleA.Build.cs
│ └── [ソースを置く場所]
└── ModuleB
├── ModuleB.Build.cs
└── [ソースを置く場所]
Source
の下には複数のモジュールを配置することも可能であるため、含むモジュールの個数や分け方によってSource
から実際のモジュールのディレクトリまでの経路は異なる場合があります。
Source
直下にモジュールの中身が置かれる場合もありますし、上記のように Source
の下に ModuleA
, ModuleB
といったように複数のモジュールが配置されることもあります。
しかし、モジュールのルートディレクトリ上の例ではModuleA
やModuleB
など)の下の構造は、C++モジュールであれば、どの種類のモジュールでも同じルールで認識されます。
そんな重要なモジュールディレクトリですが、内部のソースファイルの配置をどのようにすればよいかという点において、まとまった情報が皆無です。というか、UBT がモジュール内部のソースをどう認識しているのかのドキュメントが見当たりません。 これまでエンジンのモジュール等を見ながら雰囲気で配置を行っていたのですが、そろそろきちんとした理解を得ようと思い立ち、UBT のソースコードから調べてみました。
検証環境&注意
- UE5.0
ざっとソースを眺めてわかったつもりになってる情報を書いているだけなので、間違っているかもしれません。誤りを発見された方はご指摘ください。
また、本記事で記載しているのはディレクトリ構造に対する UBT の挙動についてであり、モジュールにおけるディレクトリ構造のベストプラクティスなどではないことにご留意ください。
モジュールに対する UBT の挙動
モジュールのスコープ
本題の前に、モジュールのスコープという概念に触れておきます。UBT 内部では、すべてのモジュールを以下のいずれかのスコープに分類しているようです。
スコープ | なにそれ |
---|---|
Engine | エンジンが直接保持しているモジュール。 Engine/Source 以下の、 Runtime , Developer , Editor , ThirdParty などに配置されたモジュールはこのスコープに属する。 |
Engine Plugins | エンジンがビルトインしているプラグインが保持するモジュール。 Engine/Plugins 以下にあるプラグインが持つモジュールはこのスコープに属する。 |
Engine Programs | エンジンが保持しているスタンドアロン系のツールが保持するモジュール。 Engine/Source/Programs 以下にあるモジュールはこのスコープに属する。 UnrealHeaderTool などはこれ。 |
Marketplace | マーケットプレイスから入手され、ランチャーによって追加されたプラグインが保持するモジュール。 Engine/Plugins/Marketplace 以下にあるプラグインが持つモジュールはこのスコープに属する。 |
Project | プロジェクト側で直接保持するモジュール。 [ProjectName]/Source 以下にあるプロジェクトのモジュールと、 [ProjectName]/Plugins 、 .uproject ファイルで AddtionalPluginDirectories に指定されたディレクトリにあるプラグインのモジュールはこのスコープに属する。 |
Plugin | 外部プラグイン(Foreign plugin)が保持するモジュール。例えば UBT に -Plugin オプションで特定の .uplugin を指定して渡したようなプラグイン) が保持するモジュールはこのスコープに属する。 |
このモジュールのスコープという概念が一体何に使われているのかというと、モジュール間の依存関係の制約に利用されているようです。 スコープはそれぞれが独立して存在しているわけではなく、包含関係(親子関係)を持っています。具体的には、以下のような関係です。
Engine (すべての親)
↑
Engine Plugins
↑
Engine Programs
↑
Marketplace
↑
Project
↑
Plugin (すべての子)
実際の使われ方は後で登場します。
ディレクトリ構造がヘッダのインクルードにもたらす効果
モジュールのディレクトリ構造がもたらす効果の中で最も大きいのは、他のモジュールに対するヘッダファイルの公開/非公開の制御です。この制御はモジュール同士のビルド時のリンケージ(リンク範囲)とも連動しており、モジュールシステムにおける依存関係管理の要となっています。
たとえば、以下のように .Build.cs
ファイルの ModuleRules
を記述したとします。
using UnrealBuildTool;
public class ExampleModule: ModuleRules
{
public ExampleModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine"
});
}
}
このモジュールは、 Core
, CoreUObject
, Engine
という 3 つのモジュールに依存することになります。
これを行うことで、依存指定したモジュールで公開されているヘッダファイルにアクセスできるようになり、バイナリのリンクも通るようになります。
しかし、公開されたヘッダファイルとはどのように指定されるものなのでしょうか? よく見るモジュール構造は以下の様なものです。この例はエンジンの UMG モジュールから借りてきました。
UMG
├── Private
│ ├── Animation
│ │ └── ...
│ └── ...
├── Public
│ ├── Animation
│ │ └── ...
│ └── ...
└── UMG.Build.cs
このモジュール構造のよく見る要素は、Private
やPublic
と言ったディレクトリ名だと思います。UBT 内部では、UEBuildModuleCPP
というクラスにおいて、これらの特別扱いされるディレクトリが定義されています。
- Classes
- Public
- Internal
- Private
これらのフォルダは UBT のソース内ではデフォルトインクルードパスと呼称されているため、本記事でも今後はこれに倣います。 デフォルトインクルードパスはそれぞれ、以下のような効果を持っています。
デフォルトインクルードパス | 効果 |
---|---|
Classes | Classes ディレクトリを参照してきたモジュールにインクルードパスとして公開する。Classes 内のヘッダは依存元モジュールから参照可能になる。 |
Public | Classes ディレクトリと同様。ただし、bNestedPublicIncludePaths オプションがtrue になっている場合においては、Public ディレクトリ内部のディレクトリが再帰的に公開リストに追加される。 |
Internal | 参照してきたモジュールのスコープが、自分属するスコープと同じか、いずれかの親スコープに含まれている(つまり、自分と同じか、より内部のモジュールである)場合にのみ、Internal ディレクトリをインクルードパスとして公開する。 |
Private | Private は、このディレクトリを含むモジュール自身をコンパイルする際、モジュール内部からインクルード可能なインクルードパスとしてプライベートなコンパイル環境に追加される。他のモジュールに依存されたとしても、 Private がインクルードパスとして他のモジュールに公開されることはない。 |
デフォルトで用意されている構造を利用する場合、以上のフォルダを利用すれば、依存関係を適切に制御したモジュールを構築することができます。
UBT の内部でのインクルードパスの追加処理
ここまででデフォルトインクルードパスの働きをは掴めましたが、インクルードパスについてはもうちょっと書けることがあります。 まず、デフォルトインクルードパスの UBT ハードコード部分がどういった処理を行っているかを以下に示します。
デフォルトインクルードパス | 具体処理 |
---|---|
Classes | 自身のモジュール定義クラスのメンバ PublicIncludePaths に、Classes のパスを追加する。 |
Public | 自身のモジュール定義クラスのメンバ PublicIncludePaths に、Public のパスを追加する。また、そのサブディレクトリをLegacyPublicIncludePaths に再帰的に追加する。 |
Internal | 自身のモジュール定義クラスのメンバ InternalIncludePaths に、Internal のパスを追加する。 |
Private | 自身のモジュール定義クラスのメンバ PrivateIncludePaths に、Private のパスを追加する。 |
これらは、特定の名称のディレクトリを、モジュール定義クラスの特定のメンバに追加するという処理を行っているだけです。
重要なのはここからで、これらが追加されている追加先のメンバには、普段モジュール定義として記述している.Build.cs
のModuleRules
からも間接的にパスの追加が可能なのです。
すごそうに言いましたが、これ自体はすでにやったことがある人も多いかと思います。重要なのはむしろ逆で、デフォルトインクルードパスは予め定義されているというだけで、それほど特別なものではなく、適切に定義すればModuleRules
からでも同等の効果を得る可能なディレクトリであるということです。
ModuleRules によるモジュールのインクルードパスの制御方法
さて、ここからの情報はインターネットにも落ちているものだと思いますが、 .Build.cs
こと ModuleRules
からモジュールが公開するインクルードパスの制御を行う方法を紹介します。
using UnrealBuildTool;
public class ExampleModule: ModuleRules
{
public ExampleModule(ReadOnlyTargetRules Target) : base(Target)
{
// 略
PublicIncludePaths.AddRange(new string[] {
/* 参照してきたモジュールに公開したいディレクトリを指定 */
});
InternalncludePaths.AddRange(new string[] {
/* 参照してきたモジュールが自分と同じスコープか、
より内部(親方向)のモジュールであれば公開したいディレクトリを指定 */
});
PrivateIncludePaths.AddRange(new string[] {
/* このモジュール自身のビルドでインクルードパスとして利用したい内部のディレクトリを指定 */
});
// 略
}
}
これらを適切に設定すれば、デフォルトインクルードパスの構造を完全に無視して、独自の構造のモジュールを定義することも可能だと思います。
おわりに
なんでこの情報こんなに無いんだろう?