はじめに
Unreal Engine のモジュールシステムを理解することで、プロジェクト内のコードの再利用性や保守性を向上させることができます。この記事では、初心者から中級者向けに、Unreal Engine のモジュールシステムと依存関係管理について説明していきます。(間違いがあれば是非ご指摘くださいいい!)
モジュールと依存関係の基本
Unreal Engine では、プロジェクトやプラグインを構成するためにモジュールという単位が使われます。モジュールは、コードやアセットをグループ化し、他のモジュールと依存関係を持つことができます。
依存関係は、あるモジュールが他のモジュールの機能を利用するために必要です。依存関係を適切に管理することで、コードの再利用性や保守性を向上させることができます。
モジュールを参照しているというのは、あるモジュールが他のモジュールのコードや機能を利用していることを意味します。具体的には、以下のようなケースが該当します。
- あるモジュールのコードが、他のモジュールのクラスや関数を直接使用している場合。
- あるモジュールが、他のモジュールで定義されたインターフェースやデータ型を使用している場合。
- あるモジュールが、他のモジュールで定義されたマクロや変数を使用している場合。
などがあります。
モジュール同士の参照関係の記述は、ビルドシステムがリンクを正しく行い、コードが正常に動作するために必要です。Unreal Engine では、モジュール間の依存関係は、.Build.csファイルのPublicDependencyModuleNames
やPrivateDependencyModuleNames
などを使って定義されます。これにより、ビルドシステムはリンクやパスを適切に設定し、モジュール同士が正しく連携できるようになります。
モジュールのスコープによる依存関係制約
モジュール間の依存関係の制約は、スコープ間の包含関係によって決定されます。一般的なルールとして、子スコープから親スコープへの依存は許可されていますが、親スコープから子スコープへの依存は許可されていません。これにより、予期しない依存関係や循環参照の問題を防ぐことができます。
例えば、プロジェクト(Project)スコープのモジュールは、エンジン(Engine)、エンジンプラグイン(Engine Plugins)、エンジンプログラム(Engine Programs)およびマーケットプレイス(Marketplace)のモジュールに依存することができます。しかし、逆にエンジンスコープのモジュールがプロジェクトスコープのモジュールに依存することはできません。
モジュールのスコープとビルドプロセス
ビルドプロセスは、モジュールのスコープとその依存関係を考慮して実行されます。UBTは、スコープ間の包含関係に従って、以下の手順でモジュールをビルドします。
- エンジン(Engine)スコープのモジュールをビルド
- エンジンプラグイン(Engine Plugins)スコープのモジュールをビルド
- エンジンプログラム(Engine Programs)スコープのモジュールをビルド
- マーケットプレイス(Marketplace)スコープのモジュールをビルド
- プロジェクト(Project)スコープのモジュールをビルド
- 外部プラグイン(Plugin)スコープのモジュールをビルド
この順序に従ってビルドを行うことで、依存関係が正しく解決され、ビルドエラーやリンクエラーが発生しにくくなります。
2. Build.cs の役割と Dependency の種類
モジュールのビルド設定は、Build.cs ファイルに記述されます。このファイルでは、依存関係やインクルードパスの設定が行われます。
依存関係には、PublicDependency と PrivateDependency の2種類があります。PublicDependency は、他のモジュールに公開される依存関係で、自分のモジュールが他のモジュールを利用する場合に設定します。PrivateDependency は、モジュール内部でのみ使用される依存関係で、他のモジュールには公開されません。
具体的な例として、プロジェクト内で新しいカスタムアクターを作成し、このアクターが他のモジュールで使用される場合を考えます。この例を通して、どのような場合にPublicDependency と PrivateDependencyそれぞれを利用すれば良いかについて具体的な実装で確認を行います。
まず、新しい MyBaseModuleモジュールを作成し、その中でMyBaseClassを定義します。
MyBaseClass.h
#pragma once
#include "CoreMinimal.h"
#include "MyBaseClass.generated.h"
UCLASS()
class MYBASEMODULE_API UMyBaseClass : public UObject
{
GENERATED_BODY()
public:
UFUNCTION()
void Speak();
UPROPERTY()
FString Name;
};
.generated.hは、ヘッダーファイルのincludeの一番最後に配置する。
実装は、以下のように記述します。
MyBaseClass.cpp
#include "MyBaseClass.h"
#include "Core.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_GAME_MODULE(FDefaultGameModuleImpl, MyBaseModule, "MyBaseModule");
void UMyBaseClass::Speak()
{
UE_LOG(LogTemp, Warning, TEXT("Hello, my name is %s "), *Name);
}
次に、MyBaseModule.Build.csファイルで、このモジュールが他のモジュールで利用されることを想定して、依存関係を設定します。
using UnrealBuildTool;
public class MyBaseModule : ModuleRules
{
public MyBaseModule(ReadOnlyTargetRules Target) : base(Target)
{
PublicIncludePaths.AddRange(
new string[] {
"MyBaseModule",
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
"MyBaseModule",
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
同様にして、MyCharacterModuleモジュールを作成します。
MyCharacterClass.h を次のようにMyBaseModuleモジュールで定義したクラスを継承して作成します。
#pragma once
#include "CoreMinimal.h"
#include "MyBaseModule/MyBaseClass.h"
#include "MyCharacterClass.generated.h"
UCLASS()
class MYCHARACTERMODULE_API UMyCharacterClass : public UMyBaseClass
{
GENERATED_BODY()
public:
UFUNCTION()
void Jump();
};
実装は、MyCharacterClass.cppで以下のように記述します。
#include "MyCharacterClass.h"
#include "Core.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_GAME_MODULE(FDefaultGameModuleImpl, MyCharacterModule);
void UMyCharacterClass::Jump()
{
UE_LOG(LogTemp, Warning, TEXT("%s is jumping!"), *Name);
}
MyCharacterModule内では、MyBaseModuleのMyBaseClassを継承して利用していること、MyBaseModuleで実装したNameにアクセスしているので、MyCharacterModule.Build.csファイルでは、MyBaseModuleをPublicDependencyとして記述しておきます。(この時点では、PublicDependencyでもPrivateDependencyでも問題ありません)
MyCharacterModule.Build.cs
using UnrealBuildTool;
public class MyCharacterModule : ModuleRules
{
public MyCharacterModule(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","MyBaseModule" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
最後に、MyEnemyModuleモジュールを作成します。このMyEnemyModuleでは、MyCharacterClassを継承して、MyEnemyClassクラスを定義します。
MyEnemyClass.h
#pragma once
#include "CoreMinimal.h"
#include "MyCharacterModule/MyCharacterClass.h"
#include "MyEnemyClass.generated.h"
UCLASS()
class MYENEMYMODULE_API UMyEnemyClass : public UMyCharacterClass
{
GENERATED_BODY()
public:
UFUNCTION()
void Attack();
UPROPERTY()
float Health;
};
MyEnemyClass.cppは、以下のように実装します。
#include "MyEnemyClass.h"
#include "Core.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_GAME_MODULE(FDefaultGameModuleImpl, MyEnemyModule);
void UMyEnemyClass::Attack()
{
UE_LOG(LogTemp, Warning, TEXT("%s is attacking!"), *Name);
}
最後に、依存関係をMyEnemyModule.Build.csに以下のように記述します。
using UnrealBuildTool;
public class MyEnemyModule : ModuleRules
{
public MyEnemyModule(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { "MyCharacterModule" });
}
}
このように実装することで、依存関係を解決し正しくビルドができます。ファイル構造は、以下のようになっています。
Source
├── MyBaseModule
│ ├── MyBaseModule.Build.cs
│ └── MyBaseClass.h
│ └── MyBaseClass.cpp
└── MyCharacterModule
│ ├── MyCharacterModule.Build.cs
│ └── MyCharacterClass.h
│ └── MyCharacterClass.cpp
└── MyEnemyModule
├── MyEnemyModule.Build.cs
└── MyEnemyClass.h
└── MyEnemyClass.cpp
MyBaseModuleモジュールの実装を、SmapleControllerモジュールで利用しています。MyCharacterModule.Build.csでは、MyBaseModuleモジュールをPublicDependencyModule
もしくは、PrivateDependencyModule
として含める必要があります。(含めないとビルド時にエラーが出ます) 一方で、MyEnemyModuleでは、MyCharacterClassの実装を使っていますが、MyCharacterClassが継承している親クラスのMyBaseModuleの実装、すなわちMyBaseModuleモジュールの実装であるNameプロパティーにアクセスを行なっています。そのため、MyEnemyModuleのBuild.csでは、MyCharacterModuleをPublicDependencyModule
もしくは、PrivateDependencyModule
として含める必要がありますが、さらに継承元のモジュールであるMyCharacterModuleのBuild.csでMyBaseModuleモジュールをPublicDependencyModule
に含める必要があることになります。MyCharacterModuleのBuild.csでMyBaseModuleモジュールをPrivateDependencyModule
として含める、すなわち、
MyCharacterModule.Build.cs
using UnrealBuildTool;
public class MyCharacterModule : ModuleRules
{
public MyCharacterModule(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { "MyBaseModule" });
}
}
とすると、エラーでビルドができなくなります。
ビルド時のいくつかの補足
モジュールを新たにプロジェクトファイルに追加した場合、上記のファイルに加えていくつかのファイルで、モジュールを新たに追加したことを明示的に記述する必要があります。
まず、{ProjectName}.Target.csと{ProjectName}Editor.Target.csに追加したモジュール名を記載します。
ExtraModuleNames.AddRange( new string[] { "MyBaseModule","MyCharacterModule","MyEnemyModule" } );
次に、{ProjectName}.Build.csにも追加したモジュール名を追記しておきます。これをすることで、プロジェクト側のモジュールで新たに追加した三つのモジュールを参照できるようになります。
PrivateDependencyModuleNames.AddRange(new string[] { "MyBaseModule","MyCharacterModule","MyEnemyModule" });
最後に、.uprojectファイルにも追加したモジュール名を以下のように追記しておきます。これでプロジェクトが追加されたモジュールを認識できるようになります。
{
"Name": "MyBaseModule",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyCharacterModule",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyEnemyModule",
"Type": "Runtime",
"LoadingPhase": "Default"
}
BPで追加したモジュールの参照
BPで新しく追加したモジュールを間接的に利用する場合は、PublicDependencyModuleNamesにこれらのモジュールを追加する必要はありません。
一方で、上記のように、プロジェクトのSourceディレクトリーのC++やプラグインのC++から他のモジュールを利用する場合は、参照元のモジュールの.Build.csのPublicDependencyModuleNames
かPrivateDependencyModuleNames
に参照するモジュールを追加する必要があります。これにより、C++ファイルで両方のモジュールの機能にアクセスできるようになります。
Plguinで追加したモジュールをBuild.csに記述する場合
Build.csで依存関係を定義しているプラグインのモジュールを使用する場合、.uprojectファイルでそのプラグインの利用を明示的にtrueに設定しておくことが重要です。これを行わないと、以下のような問題が発生する可能性があります。
- プラグインが正しくロードされず、依存しているモジュールの機能が利用できなくなる。
- ビルド時にリンクエラーやコンパイルエラーが発生する。
- ランタイム時に不具合やクラッシュが発生する。
プラグインの利用を明示的に有効にすることで、エンジンはプラグインを正しくロードし、依存関係を解決し、ビルドや実行時に適切な動作が保証されます。.uprojectファイルでプラグインの利用を次のように有効にする必要があります。
{
"Plugins": [
{
"Name": "YourPluginName",
"Enabled": true
}
]
}
この設定を追加することで、プラグインのモジュールが正しくロードされ、依存関係が正しく解決されるようになります。
3. ヘッダーファイルの検索パスとインクルードパスの利用
ヘッダーファイルは、C++ コード内で定義されたクラスや関数の宣言が記述されています。ヘッダーファイルの検索パスは、コンパイラがインクルード指令を解決する際に参照されます。
PublicIncludePaths
と PrivateIncludePaths
は、ヘッダーファイルの検索パスを定義します。PublicIncludePaths
は、他のモジュールから参照可能なヘッダーファイルのパスを指定し、PrivateIncludePaths
は、現在のモジュール内でのみ使用されるヘッダーファイルのパスを指定します。
4. デフォルトインクルードパスとモジュール定義
モジュールのディレクトリ構造がもたらす効果の中で最も大きいのは、他のモジュールに対するヘッダファイルの公開/非公開の制御です。この制御はモジュール同士のビルド時のリンケージ(リンク範囲)とも連動しており、モジュールシステムにおける依存関係管理の要となっています。
デフォルトインクルードパスは以下のような効果を持っています。
- Classes: 他のモジュールから参照可能なインクルードパスとして公開されます。
- Public: Classes と同様ですが、
bNestedPublicIncludePaths
オプションが true の場合、Public ディレクトリ内部のディレクトリが再帰的に公開リストに追加されます。 - Internal: 参照してきたモジュールのスコープが、自分属するスコープと同じか、いずれかの親スコープに含まれている場合にのみ、Internal ディレクトリをインクルードパスとして公開する。
- Private: モジュール内部からのみインクルード可能なインクルードパスとして追加されます。
5. ModuleRules によるインクルードパスの制御
デフォルトインクルードパスは予め定義されているというだけで、それほど特別なものではなく、適切に定義すれば ModuleRules からでも同等の効果を得ることができます。以下のように、ModuleRules からモジュールが公開するインクルードパスの制御を行うことができます。
using UnrealBuildTool;
public class ExampleModule: ModuleRules
{
public ExampleModule(ReadOnlyTargetRules Target): base(Target)
{
// 略
PublicIncludePaths.AddRange(new string[] {
/* 参照してきたモジュールに公開したいディレクトリを指定 */
});
InternalIncludePaths.AddRange(new string[] {
/* 参照してきたモジュールが自分と同じスコープか、
より内部(親方向)のモジュールであれば公開したいディレクトリを指定 */
});
PrivateIncludePaths.AddRange(new string[] {
/* このモジュール自身のビルドでインクルードパスとして利用したい内部のディレクトリを指定 */
});
// 略
}
}
これらを適切に設定すれば、デフォルトインクルードパスの構造を完全に無視して、独自の構造のモジュールを定義することも可能だと思います。
補足
Unreal Engineには、上記で見てきたようにモジュールに関する設定ファイルが、Build.csとTarget.csと、.uprojectファイルの三つに分かれています。これらの違いは以下の通りです。
Build.cs (モジュールファイル)
モジュールのビルド設定を定義します。
依存関係やコンパイルオプションなど、モジュールのビルドに必要な情報を記述します。
Target.cs (ターゲットファイル):
ビルドターゲット(Game, Editor, Client, Server, Program)に関する設定を行います。
ビルド対象となるモジュールを指定し、ビルドタイプごとに異なる設定を適用できます。
.uprojectファイル:
プロジェクト全体の設定を記述するファイルで、モジュールの読み込み情報も含みます。
起動時の読み込み対象のモジュールや、その動作タイプ、読み込みタイミングを指定します。
これら3つのファイルは、モジュールのビルドや起動時の挙動を制御するためにそれぞれ異なる目的で使用されます。
まとめ
この記事では、Unreal Engine のモジュールシステムと依存関係管理について説明しました。モジュールと依存関係を適切に設定することで、プロジェクト内のコードの再利用性や保守性を向上させることができます。また、デフォルトインクルードパスや ModuleRules を使用して、インクルードパスの制御を行うことができます。
Reference
この記事は、基本的に以下の二つの記事をもとに書いております。