概要
UnrealEngine のプログラミングサブシステムについてのメモ書きです。
GameInstanceSubsystem
ImportSubsystem
EditorValidatorSubsystem
WorldSubsystem
についての記述になります。
更新履歴
日付 | 内容 |
---|---|
2021/02/24 | 初期化順序について修正、WorldSubsystemについて追記 |
2024/06/21 | サブシステムの拡張BPについて追記 |
環境
Windows10
Visual Studio 2017
UnrealEngine 4.25, 5.3
参考
以下を参考にさせて頂きました、ありがとうございます。
Unreal Engine : プログラミングサブシステム
猫でも分かるUE4.22から入ったSubsystem
UE4.23から入った「Editor Validator Subsystem」を使って、アセット保存時などで走るチェック処理(Validate Assets)を拡張しよう!
【UE4】Assetの参照をBPで取得する
UE4:オブジェクト
UE4:アンリアルでのオブジェクト処理
UObjectの動作原理
UE4プログラマー勉強会 in 大阪 -エンジンの内部挙動について
【UE4】ImportSubsystemについて
プログラミングサブシステムについて
特定のタイミングで管理(生成/破棄)されるインスタンスがつくられる仕組みです。ランタイムだけではなくエディター限定での使い方もできるようです。
ソースフォルダ
ランタイム
EngineSubsystem
, GameInstanceSubsystem
, LocalPlayerSubsystem
など。
"Engine\Source\Runtime\Engine\Private\Subsystems"
"Engine\Source\Runtime\Engine\Public\Subsystems"
エディタ
ImportSubsystem
など
"Engine\Source\Editor\UnrealEd\Private\Subsystems"
"Engine\Source\Editor\UnrealEd\Public\Subsystems"
▼ EditorValidatorSubsystem
"Engine\Plugins\Editor\DataValidation\Source\DataValidation\Public\EditorValidatorSubsystem.h"
GameInstanceSubsystem
ランタイムで使用可能です。
ゲーム開始時に生成され、終了時に破棄されるサブシステム。ゲーム中の何かを管理するマネージャ系クラスはこれを使うと良いと思われます。
作成テスト:C++クラス準備
UGameInstanceSubsystem を継承したクラスを用意します。
UCLASS()
class TEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 初期化
virtual void Initialize(FSubsystemCollectionBase& Collection);
// 初期化解除
virtual void Deinitialize();
// 呼び出しテスト用
UFUNCTION(BlueprintCallable)
void CallTest(){ UE_LOG(LogTemp, Log, TEXT("Test!")); }
};
ログ出力をするだけのメソッドです。
#include "MyGameInstanceSubsystem.h"
// 初期化
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Initialize()"));
}
// 初期化解除
void UMyGameInstanceSubsystem::Deinitialize()
{
UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Deinitialize()"));
}
実行確認
ゲーム起動と同時に UMyGameInstanceSubsystem::Initialize()
がアウトプットログに出力され、終了時に UMyGameInstanceSubsystem::Deinitialize()
が出力されます。
実装したメソッドにアクセスするには以下のようになります。
▼C++の場合
UGameInstance* _GameInst = GetWorld()->GetGameInstance();
auto _MySub = _GameInst->GetSubsystem<UMyGameInstanceSubsystem>();
_MySub->CallTest();
Tick処理を実装する
SubsystemにTick処理を実装するには、FTickableGameObject
か FTickableEditorObject
を継承することで実装できます。
▼FTickableGameObject
"Engine\Source\Runtime\Engine\Public\Tickable.h"
▼FTickableEditorObject
"Engine\Source\Editor\UnrealEd\Public\TickableEditorObject.h"
以下コード例。
#include "Subsystems/GameInstanceSubsystem.h"
#include "Tickable.h"
#include "MyGameInstanceSubsystem.generated.h"
UCLASS()
class TEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
GENERATED_BODY()
public:
// FTickableGameObjectからオーバーライド
virtual TStatId GetStatId() const;
virtual bool IsTickable() const { return(true); }
virtual void Tick(float DeltaTime);
};
#include "MyGameInstanceSubsystem.h"
TStatId UMyGameInstanceSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UMyGameInstanceSubsystem, STATGROUP_Tickables);
}
void UMyGameInstanceSubsystem::Tick(float DeltaTime)
{
UE_LOG(LogTemp, Log, TEXT("DeltaTime=%f"), DeltaTime);
}
ゲーム起動後、Tick処理が回ります。
ImportSubsystem
エディタオンリーです。
作成テスト:C++クラスを準備
UImportSubsystemを継承したUMyImportSubsystemを用意してみます。
UCLASS()
class APPEDITOR_API UMyImportSubsystem : public UImportSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection);
virtual void Deinitialize();
// 個別処理
virtual void OnAssetPreImportFunc(UFactory* InFactory, UClass* InClass, UObject* InParent, const FName& Name, const TCHAR* Type);
virtual void OnAssetPostImportFunc(UFactory* InFactory, UObject* InCreatedObject);
virtual void FOnAssetReimportFunc(UObject* InCreatedObject);
virtual void FOnAssetPostLODImportFunc(UObject* InObject, int32 inLODIndex);
};
#include "MyImportSubsystem.h"
void UMyImportSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::Initialize()"));
// 登録
OnAssetPreImport.AddUObject(this, &UMyImportSubsystem::OnAssetPreImportFunc);
OnAssetPostImport.AddUObject(this, &UMyImportSubsystem::OnAssetPostImportFunc);
OnAssetReimport.AddUObject(this, &UMyImportSubsystem::FOnAssetReimportFunc);
OnAssetPostLODImport.AddUObject(this, &UMyImportSubsystem::FOnAssetPostLODImportFunc);
}
void UMyImportSubsystem::Deinitialize()
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::Deinitialize()"));
}
// 個別処理
void UMyImportSubsystem::OnAssetPreImportFunc(UFactory* InFactory, UClass* InClass, UObject* InParent, const FName& Name, const TCHAR* Type)
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::OnAssetPreImportFunc"));
}
void UMyImportSubsystem::OnAssetPostImportFunc(UFactory* InFactory, UObject* InCreatedObject)
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::OnAssetPostImportFunc"));
}
void UMyImportSubsystem::FOnAssetReimportFunc(UObject* InCreatedObject)
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::FOnAssetReimportFunc"));
}
void UMyImportSubsystem::FOnAssetPostLODImportFunc(UObject* InObject, int32 inLODIndex)
{
UE_LOG(LogTemp, Log, TEXT("UMyImportSubsystem::FOnAssetPostLODImportFunc"));
}
としましたがこれはUE4.24 では呼び出されません。
継承元の UImportSubsystemのメソッドが呼び出されるため、これを回避するにはエンジン改造を行うしかないようです。(FSubsystemCollectionBase::GetSubsystemInternal に対して修正を行う)
BPを使う
EditorUtilityWidgetなどのBPから以下の様に処理をバインドし、実行した上でなら意図した処理が走ります。
インポート時の前後にデータ加工をする場合は有用になる方法だと思われます。
C++からImportSubsystemにアクセスする
直接、ImportSubsystemにアクセスして処理をバインドすることも可能だと思われます。(未検証)
#include "Editor/LevelEditor/Public/LevelEditor.h"
auto ImportSubsystem = GEditor->GetEditorSubsystem<UImportSubsystem>();
if(ImportSubsystem){
UE_LOG(LogTemp, Log, TEXT("ImportSubSystem!"));
}
ImportSubsystemはまだ検証が必要の様です。
EditorValidatorSubsystem
エディターオンリーです、なのでC++コードを使う場合は、エディター用のモジュールに処理を書かないとパッケージ化する時にエラーがでます。
アセットをチェックする(BPのみ)
EditorUtilityBlueprintから親クラスEditorValidatorBaseを選択してエディター用BPを作成します。
[Can Validate Asset]をオーバーライドし、チェック対象のクラス型かの処理を書きます。更に[Validate Loaded Asset]をBPオーバーライドし、チェック内容の処理を書き、結果を返します。
以下、スケルタルメッシュのマテリアル数を調べるテストコード例:
Can Validate Asset
Validate Loaded Asset
スケルタルメッシュにキャストをしてマテリアル数を調べます、コード例ではマテリアル数が3以外はエラーとしています。
実行テスト
スケルタルメッシュのアセットを右クリック -> [アセットアクション] -> [Valudate Assets]で個別のアセットに対しチェックを走らせることができます。
アクターをチェックする(C++とBP併用)
Validate Loaded Assetで取得できる In Asset はUBlueprint クラス型なので、アクタークラスを調べる場合、ClassDefaultObject (以下CDO) にアクセスする必要があります。
CDOはBPから取得できないため、C++で取得できるようなコードを用意する必要があります。
4.25以降では GetEditorProperty
ノードが追加されるため、これを使ってプロパティが取得できるようになるようです。
Editorモジュール
にUEditorValidatorBaseを継承したクラスを作成し、CDOを取得するメソッドを作成します。
以下コード例:
C++でUEditorValidatorBaseを継承したクラスを用意
UCLASS()
class APPEDITOR_API UMyEditorValidatorBase : public UEditorValidatorBase
{
GENERATED_BODY()
public:
// アクターにキャストしてCDOを取得する
UFUNCTION(Category = "MyEditorValidatorBase", BlueprintCallable)
AActor* GetActorCDO(UObject* InAsset);
};
#include "MyEditorValidatorBase.h"
// アクターにキャストしてCDOを取得する
AActor* UMyEditorValidatorBase::GetActorCDO(UObject* InAsset)
{
UBlueprint* _Blueprint = Cast<UBlueprint>(InAsset);
check(_Blueprint);
AActor* _CDO = _Blueprint->GeneratedClass->GetDefaultObject<AActor>();
return(_CDO);
}
BPにチェックコードを用意
次にUMyEditorValidatorBaseを継承したEditorUtilityBlueprintを作成し、[ValidateLoadedAsset]をオーバーライドして処理を記述します。
InAssetのキャストにC++で実装した[GetActorCDO]を使って取得します。
以下、アクタータグ[TestTag]を持っているかのチェックコード例:
CDO取得時の注意点
上記のCDOを取得するコードはアクターなBPの場合は問題ないのですが、アクターではないBP(アニメーションBPやUMGなど)でもCDOを取得しにいってストップしてしまいます。
UBlueprint* _Blueprint = Cast<UBlueprint>(InAsset);
check(_Blueprint);
AActor* _CDO = _Blueprint->GeneratedClass->GetDefaultObject<AActor>();
これを回避するために、CanValidateAsset
にファイルパスや命名規則等で意図しないアセットがチェックされないように処理しないとなりません。
以下コード例。
bool UMyEditorValidatorBase::CanValidateAsset_Implementation(UObject* InAsset) const
{
if (InAsset) {
if (!InAsset->IsA(UBlueprint::StaticClass())) return(false);
}
// ファイルパス "Game/Chara/" を含むファイルをチェック対象とする場合
UBlueprint* Blueprint = Cast<UBlueprint>(InAsset);
if (Blueprint->GetPathName().Contains(TEXT("/Game/Chara/"), ESearchCase::CaseSensitive)) {
return(true);
}
// ファイルパスからワイルドカードで調べる場合
// if (Blueprint->GetPathName().MatchesWildcard(TEXT("/Game/Chara/Chara??BP.*"), ESearchCase::CaseSensitive)) {
// return(true);
// }
return(false);
}
アクターが所持のコンポーネントを調べる場合の注意点
アクターが所持しているコンポーネントのプロパティをチェックする場合、CDOを使っている関係上、コンストラクタで作成されたコンポーネント(CreateDefaultSubobject
で作成されたコンポーネント)だけチェックできます。後から追加したコンポーネントは認識できません。
全てC++でチェックコードを用意
[Can Validate Aseet]と[Validate Loaded Asset]をC++で書くこともできます。
ただし、EditorValidatorSubsystemへの登録があるため、BPへの継承は必要なようです。
UCLASS()
class APPEDITOR_API UMyEditorValidatorBase : public UEditorValidatorBase
{
GENERATED_BODY()
public:
protected:
virtual bool CanValidateAsset_Implementation(UObject* InAsset) const override;
virtual EDataValidationResult ValidateLoadedAsset_Implementation(UObject* InAsset, TArray<FText>& ValidationErrors) override;
};
bool UMyEditorValidatorBase::CanValidateAsset_Implementation(UObject* InAsset) const
{
return( InAsset ? InAsset->IsA(UBlueprint::StaticClass()) : false );
}
EDataValidationResult UMyEditorValidatorBase::ValidateLoadedAsset_Implementation(UObject* InAsset, TArray<FText>& ValidationErrors)
{
UBlueprint* Blueprint = Cast<UBlueprint>(InAsset);
check(Blueprint);
AActor* CDO = Blueprint->GeneratedClass->GetDefaultObject<AActor>();
if (!CDO)
{
AssetFails(InAsset, FText::FromString(TEXT("FAILED to get CDO")), ValidationErrors);
}
else
{
// チェックコードを書く
}
// チェック結果
if (GetValidationResult() != EDataValidationResult::Invalid)
{
AssetPasses(InAsset);
}
return( GetValidationResult() );
}
WorldSubsystem
エディタ、ランタイムで使用可能です。
ワールド開始/終了時に紐づくサブシステムです。ゲーム起動時の初期化タイミングは GameInstanceSubsystem
より先に処理されます。
作成例
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "MyWorldSubsystem.generated.h"
UCLASS()
class TEST_API UMyWorldSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
UMyWorldSubsystem();
// 初期化
virtual void Initialize(FSubsystemCollectionBase& Collection);
// 初期化解除
virtual void Deinitialize();
// インスタンスを取得
static UMyWorldSubsystem* GetInstance();
};
#include "MyWorldSubsystem.h"
#include "Subsystems/SubsystemCollection.h"
UMyWorldSubsystem::UMyWorldSubsystem()
{
}
void UMyWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
FString _WorldInfoString;
// ワールド情報のチェック
switch (GetWorld()->WorldType){
case EWorldType::None:{ _WorldInfoString += TEXT("None"); }break;
case EWorldType::Game:{ _WorldInfoString += TEXT("Game"); }break;
case EWorldType::Editor:{ _WorldInfoString += TEXT("Editor"); }break;
case EWorldType::PIE:{ _WorldInfoString += TEXT("PIE"); }break;
case EWorldType::EditorPreview:{ _WorldInfoString += TEXT("EditorPreview"); }break;
case EWorldType::GamePreview:{ _WorldInfoString += TEXT("GamePreview"); }break;
case EWorldType::GameRPC:{ _WorldInfoString += TEXT("GameRPC"); }break;
case EWorldType::Inactive:{ _WorldInfoString += TEXT("Inactive"); }break;
};
_WorldInfoString += TEXT(" | ");
_WorldInfoString += GetWorld()->GetFullName();
UE_LOG(LogTemp, Log, TEXT("UMyWorldSubsystem::Initialize() : %s"), *_WorldInfoString );
}
void UMyWorldSubsystem::Deinitialize()
{
UE_LOG(LogTemp, Log, TEXT("UMyWorldSubsystem::Deinitialize()"));
}
UMyWorldSubsystem*
UMyWorldSubsystem::GetInstance()
{
UObject* _WorldContext = /*ワールドコンテキストの取得*/
UWorld* _World = GEngine->GetWorldFromContextObject(_WorldContext, EGetWorldErrorMode::LogAndReturnNull);
auto _Inst = _World->GetSubsystem<UMyWorldSubsystem>();
return( _Inst );
}
ワールドサブシステムのうまい使いどころが思いつきませんが、実質パーシスタントレベルのサブシステムと考えればいいのですかね?
その他
同系統のサブシステム同士の初期化順序について
通常はシステム側が登録した順序に処理をするため、初期化順が問題にならないようなコードが望ましいようです。
もし初期化順序を制御したい場合は、FSubsystemCollectionBase
の InitializeDependency
を使うことで制御可能なようです。実装内容としては該当のサブシステムの Initialize
内で先に初期化したいサブシステムクラスをInitializeDependency
の引き数として渡すと未初期化ならばその場で初期化処理が走るようです。
以下サンプルコード。
void UMySubsystem1::Initialize(FSubsystemCollectionBase& Collection)
{
// UMySubsystem2が未初期化ならば先に初期化する
Collection.InitializeDependency(UMySubsystem2::StaticClass());
auto _MySubsystem2 = GetGameInstance()->GetSubsystem<UMySubsystem2>();
if(_MySubsystem2){
// UMySubsystem2に対する処理
}
}
この初期化の制御は同系統のサブシステム(GameInstance, World, Engine, Editor)同士でないと使えません。
サブシステムのBP化
サブシステムのBP化は基本不可のようです(エンジン修正で行ける可能性あり)。
公式には拡張用のBPを用意してそちらにプロパティや処理を追加する方法で行うのが良さそうです。
(1)拡張用BPベースクラスを用意する
拡張用BPベースクラスをC++で定義します。
// 拡張用BPベースクラス
UCLASS(Abstract, Blueprintable, MinimalAPI, meta = (ShowWorldContextPin))
class UMySubsystemHelperBase : public UObject
{
GENERATED_BODY()
public:
// テストプロパティ
UPROPERTY(BlueprintReadWrite)
int32 TestInt;
// テストメソッド
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
void TestFunc();
};
(2)サブシステム側に拡張用BPクラスを持つ
オブジェクトを生成してサブシステムに持たせます。
UCLASS()
class SAMPLE_API UMySubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// ..省略..
// 拡張用BP
UPROPERTY(Transient, BlueprintReadOnly)
UMySubsystemHelperBase* SubsystemHelper;
private:
// 拡張用BPクラス
TSubclassOf<class UMySubsystemHelperBase> SubsystemHelperClass;
};
UMySubsystem::UMySubsystem()
{
// 拡張用BPのクラス検索
static ConstructorHelpers::FClassFinder<UMyGameInstanceSubsystemHelperBase> _BluePrintFile(TEXT("Blueprint'/Game/BP_MySubsystemHelper'"));
if(_BluePrintFile.Class){
SubsystemHelperClass = (UClass*)_BluePrintFile.Class;
}
}
void UMySubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
SubsystemHelper = nullptr;
if(SubsystemHelperClass){
// 拡張用BPを生成,設定
SubsystemHelper = NewObject<UMySubsystemHelperBase>(GetTransientPackage(), SubsystemHelperClass);
}
}
void UMySubsystem::Deinitialize()
{
// 拡張用BPオブジェクト始末
SubsystemHelper = nullptr;
}
これでBP側にプロパティやメソッドを持たせることができます。
ただしアクセスには型キャストが要ります。
まとめ
エディター系サブシステムは主にチェック機能に、ランタイム系サブシステムはマネージャ系クラスの作成にそれぞれ利用できそうです。
あと、EngineSubsystem
などの有効な利用方法があったら誰か教えて下さい。