15
Help us understand the problem. What are the problem?

posted at

updated at

UE4 プログラミングサブシステムを試してみる

概要

UnrealEngine のプログラミングサブシステムについてのメモ書きです。
GameInstanceSubsystem ImportSubsystem EditorValidatorSubsystem WorldSubsystemについての記述になります。

更新履歴

日付 内容
2021/02/24 初期化順序について修正、WorldSubsystemについて追記

環境

Windows10
Visual Studio 2017
UnrealEngine 4.25

参考

以下を参考にさせて頂きました、ありがとうございます。

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 を継承したクラスを用意します。

MyGameInstanceSubsystem.h

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!")); }
};

ログ出力をするだけのメソッドです。

MyGameInstanceSubsystem.cpp
#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() が出力されます。
MyGameInstanceSubsystem_output.jpg

実装したメソッドにアクセスするには以下のようになります。

▼BPの場合
GameInstanceSubsystem_bp.jpg

▼C++の場合

.cpp

UGameInstance* _GameInst = GetWorld()->GetGameInstance();
auto _MySub = _GameInst->GetSubsystem<UMyGameInstanceSubsystem>();
_MySub->CallTest();

Tick処理を実装する

SubsystemにTick処理を実装するには、FTickableGameObjectFTickableEditorObject を継承することで実装できます。

▼FTickableGameObject
"Engine\Source\Runtime\Engine\Public\Tickable.h"

▼FTickableEditorObject
"Engine\Source\Editor\UnrealEd\Public\TickableEditorObject.h"

以下コード例。

MyGameInstanceSubsystem.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);

};
MyGameInstanceSubsystem.cpp
#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を用意してみます。

MyImportSubsystem.h
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);

};
MyImportSubsystem.cpp
#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から以下の様に処理をバインドし、実行した上でなら意図した処理が走ります。

ImportSubsystem.jpg

インポート時の前後にデータ加工をする場合は有用になる方法だと思われます。

C++からImportSubsystemにアクセスする

直接、ImportSubsystemにアクセスして処理をバインドすることも可能だと思われます。(未検証)

.cpp
#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を作成します。

EditorUtilityBlueprint.jpg

[Can Validate Asset]をオーバーライドし、チェック対象のクラス型かの処理を書きます。更に[Validate Loaded Asset]をBPオーバーライドし、チェック内容の処理を書き、結果を返します。

以下、スケルタルメッシュのマテリアル数を調べるテストコード例:

Can Validate Asset

チェックするアセットのクラスが一致するかを返します。
TestSkeletalMesh_00.jpg

Validate Loaded Asset

スケルタルメッシュにキャストをしてマテリアル数を調べます、コード例ではマテリアル数が3以外はエラーとしています。
TestSkeletalMesh_01.jpg

実行テスト

スケルタルメッシュのアセットを右クリック -> [アセットアクション] -> [Valudate Assets]で個別のアセットに対しチェックを走らせることができます。

TestSkeletalMesh_02.jpg

アクターをチェックする(C++とBP併用)

Validate Loaded Assetで取得できる In Asset はUBlueprint クラス型なので、アクタークラスを調べる場合、ClassDefaultObject (以下CDO) にアクセスする必要があります。
CDOはBPから取得できないため、C++で取得できるようなコードを用意する必要があります。

4.25以降では GetEditorPropertyノードが追加されるため、これを使ってプロパティが取得できるようになるようです。

EditorモジュールにUEditorValidatorBaseを継承したクラスを作成し、CDOを取得するメソッドを作成します。

以下コード例:

C++でUEditorValidatorBaseを継承したクラスを用意

MyEditorValidatorBase.h
UCLASS()
class APPEDITOR_API UMyEditorValidatorBase : public UEditorValidatorBase
{
    GENERATED_BODY()

public:
    // アクターにキャストしてCDOを取得する
    UFUNCTION(Category = "MyEditorValidatorBase", BlueprintCallable)
    AActor* GetActorCDO(UObject* InAsset);
};

MyEditorValidatorBase.cpp
#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]を持っているかのチェックコード例:
TestActor_00.jpg

CDO取得時の注意点

上記のCDOを取得するコードはアクターなBPの場合は問題ないのですが、アクターではないBP(アニメーションBPやUMGなど)でもCDOを取得しにいってストップしてしまいます。

アクターの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への継承は必要なようです。

MyEditorValidatorBase2.h
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;
};
MyEditorValidatorBase2.cpp

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より先に処理されます。

作成例

MyWorldSubsystem.h
#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();
};
cpp
#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()
{
    UWorld* _World = GEngine->GetWorldFromContextObject(UGeneralUtilityLibrary::GetWorldContext(), EGetWorldErrorMode::LogAndReturnNull);

    auto _Inst = _World->GetSubsystem<UMyWorldSubsystem>();

    return( _Inst );
}

ワールドサブシステムのうまい使いどころが思いつきませんが、実質パーシスタントレベルのサブシステムと考えればいいのですかね?:thinking:

その他

同系統のサブシステム同士の初期化順序について

通常はシステム側が登録した順序に処理をするため、初期化順が問題にならないようなコードが望ましいようです。
もし初期化順序を制御したい場合は、FSubsystemCollectionBaseInitializeDependency を使うことで制御可能なようです。実装内容としては該当のサブシステムの Initialize 内で先に初期化したいサブシステムクラスをInitializeDependencyの引き数として渡すと未初期化ならばその場で初期化処理が走るようです。
以下サンプルコード。

MySubsystem1.cpp
void UMySubsystem1::Initialize(FSubsystemCollectionBase& Collection)
{
    // UMySubsystem2が未初期化ならば先に初期化する
    Collection.InitializeDependency(UMySubsystem2::StaticClass());

    auto _MySubsystem2 = GetGameInstance()->GetSubsystem<UMySubsystem2>();
    if(_MySubsystem2){
        // UMySubsystem2に対する処理
    }

}

この初期化の制御は同系統のサブシステム(GameInstance, World, Engine, Editor)同士でないと使えません。

まとめ

エディター系サブシステムは主にチェック機能に、ランタイム系サブシステムはマネージャ系クラスの作成にそれぞれ利用できそうです。

あと、EngineSubsystemなどの有効な利用方法があったら誰か教えて下さい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
15
Help us understand the problem. What are the problem?