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といったように複数のモジュールが配置されることもあります。 しかし、モジュールのルートディレクトリ上の例ではModuleAModuleBなど)の下の構造は、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

このモジュール構造のよく見る要素は、PrivatePublicと言ったディレクトリ名だと思います。UBT 内部では、UEBuildModuleCPPというクラスにおいて、これらの特別扱いされるディレクトリが定義されています。

  • Classes
  • Public
  • Internal
  • Private

これらのフォルダは UBT のソース内ではデフォルトインクルードパスと呼称されているため、本記事でも今後はこれに倣います。 デフォルトインクルードパスはそれぞれ、以下のような効果を持っています。

デフォルトインクルードパス効果
ClassesClasses ディレクトリを参照してきたモジュールにインクルードパスとして公開する。Classes 内のヘッダは依存元モジュールから参照可能になる。
PublicClasses ディレクトリと同様。ただし、bNestedPublicIncludePathsオプションがtrueになっている場合においては、Publicディレクトリ内部のディレクトリが再帰的に公開リストに追加される。
Internal参照してきたモジュールのスコープが、自分属するスコープと同じか、いずれかの親スコープに含まれている(つまり、自分と同じか、より内部のモジュールである)場合にのみ、Internalディレクトリをインクルードパスとして公開する。
PrivatePrivateは、このディレクトリを含むモジュール自身をコンパイルする際、モジュール内部からインクルード可能なインクルードパスとしてプライベートなコンパイル環境に追加される。
他のモジュールに依存されたとしても、Privateがインクルードパスとして他のモジュールに公開されることはない。

デフォルトで用意されている構造を利用する場合、以上のフォルダを利用すれば、依存関係を適切に制御したモジュールを構築することができます。

UBT の内部でのインクルードパスの追加処理

ここまででデフォルトインクルードパスの働きをは掴めましたが、インクルードパスについてはもうちょっと書けることがあります。 まず、デフォルトインクルードパスの UBT ハードコード部分がどういった処理を行っているかを以下に示します。

デフォルトインクルードパス具体処理
Classes自身のモジュール定義クラスのメンバ PublicIncludePaths に、Classes のパスを追加する。
Public自身のモジュール定義クラスのメンバ PublicIncludePaths に、Public のパスを追加する。また、そのサブディレクトリをLegacyPublicIncludePathsに再帰的に追加する。
Internal自身のモジュール定義クラスのメンバ InternalIncludePathsに、Internalのパスを追加する。
Private自身のモジュール定義クラスのメンバ PrivateIncludePaths に、Private のパスを追加する。

これらは、特定の名称のディレクトリを、モジュール定義クラスの特定のメンバに追加するという処理を行っているだけです。 重要なのはここからで、これらが追加されている追加先のメンバには、普段モジュール定義として記述している.Build.csModuleRulesからも間接的にパスの追加が可能なのです。

すごそうに言いましたが、これ自体はすでにやったことがある人も多いかと思います。重要なのはむしろ逆で、デフォルトインクルードパスは予め定義されているというだけで、それほど特別なものではなく、適切に定義すれば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[] {
			/* このモジュール自身のビルドでインクルードパスとして利用したい内部のディレクトリを指定 */
		});
		// 略
	}
}

これらを適切に設定すれば、デフォルトインクルードパスの構造を完全に無視して、独自の構造のモジュールを定義することも可能だと思います。

おわりに

なんでこの情報こんなに無いんだろう?

share