main() blog

プログラムやゲーム、旅、愛する家族について綴っていきます。

【UE5】Subsystem(サブシステム)を使ってみよう!

概要

特定のタイミングで管理(生成/破棄)されるインスタンスを作る仕組みです。
Enigne、Editor、GameInstance、World、LocalPlayerと同じライフサイクルで管理が可能です。
〇〇Managerを作りたい!という時に有用な機能です。

基底クラスによってランタイム(ゲーム)、ワールド(エディタ/ランタイム)、エディタなどで使用できるSubsystemを指定できます。

UGameInstanceSubsystem
UWorldSubsystem
ULocalPlayerSubsystem
UEngineSubsystem
UEditorSubsystem

プロジェクト毎にシングルトンを用意して組み込むことでも可能ですが、初期化のタイミングやアクセス方法など実装方法がまちまちになってしまいます。
UEで用意されている機能で、同等の仕組みが実装できるようになります。

※本記事はC++での開発を前提としています。

環境

UnrealEngine 5.4.2

GameInstanceSubSystem

ゲーム起動時に生成され、終了時に破棄されます。
ゲーム中のマネージャー系クラスとして使用できるとおもいます。

UGameInstanceSubsystemを継承します。

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MyGameInstanceSubsystem.generated.h"

/**
 * 
 */
UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);

    virtual void Deinitialize();
};
#include "MyGameInstanceSubsystem.h"

void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Initialize()"));

    Super::Initialize(Collection);
}

void UMyGameInstanceSubsystem::Deinitialize()
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Deinitialize()"));

    Super::Deinitialize();
}

テストレベルなど起動してゲームを起動、終了することでGameInstanceSubsystemの初期化、初期化解除が呼ばれていることが確認できます。

Tick処理

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

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Tickable.h"
#include "MyGameInstanceSubsystem.generated.h"

/**
 * 
 */
UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);

    virtual void Deinitialize();

    virtual bool IsTickable() const override
    {
        return bIsInitialized;
    }

    virtual TStatId GetStatId() const override
    {
        RETURN_QUICK_DECLARE_CYCLE_STAT(UMyGameInstanceSubsystem, STATGROUP_Tickables);
    }

    virtual void Tick(float DeltaTime) override;

private:
    bool bIsInitialized = false;
};
#include "MyGameInstanceSubsystem.h"

void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Initialize()"));

    Super::Initialize(Collection);

    bIsInitialized = true;
}

void UMyGameInstanceSubsystem::Deinitialize()
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Deinitialize()"));

    Super::Deinitialize();

    bIsInitialized = false;
}

void UMyGameInstanceSubsystem::Tick(float DeltaTime)
{
    if (!bIsInitialized)
    {
        return;
    }

    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Tick() : [%f]"), DeltaTime);
}

テストレベルなどを起動してTickが呼ばれていることが確認できます。

Subsystemの初期化(依存)

GameInstanceSubsystemなどの同じSubsytem内で初期化の順序の指定方法です。
SubsytemAより先にSubsytemBを初期化したいケースで使用します。
FSubsystemCollectionBaseのInitializeDependencyで依存をしていることで初期化の順番を制御できるようなります。

void UMyGameInstanceSubsystemA::Initialize(FSubsystemCollectionBase& Collection)
{
    Collection.InitializeDependency(UMyGameInstanceSubsystemB::StaticClass());
}

アクセス方法

C++でGameInstanceSubsystemにアクセスする場合はGameInstanceを取得してGetSubsystemで取得する必要があります。
例えばActorなどから参照する場合はGetWorld()でワールドが取得できるので、そこから辿って指定したクラスのGameInstanceSubsystemを取得することができます。

void AHogeActor::Hoge()
{
    UWorld* world = GetWorld();

    if (world)
    {
        UGameInstance* gameInstance = world->GetGameInstance<UGameInstance>();

        if (gameInstance)
        {
            UMyGameInstanceSubsystem* subsystem = gameInstance->GetSubsystem<UMyGameInstanceSubsystem>();

            subsystem->Test();
        }
    }
}

毎回、この方法でアクセスするのは若干煩雑な気がします。
シングルトンの様に簡潔にアクセスしたいと思います。

void AHogeActor::Hoge()
{
    UMyGameInstanceSubsystem::Get()->Test();
}

Subsystemのクラスにstatic関数でGet()を追加してみたいと思いますが、グローバルのGEngineから辿る方法はあまり推奨されていないようなのでActorなどUObjectからアクセスする前提でのGet()を追加してみたいと思います。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{

public:

    static UMyGameInstanceSubsystem* Get(const UObject* worldContextObject)
    {
        if (worldContextObject)
        {
            UWorld* world = worldContextObject->GetWorld();

            if (world)
            {
                UGameInstance* gameInstance = world->GetGameInstance<UGameInstance>();

                if (gameInstance)
                {
                    return gameInstance->GetSubsystem<UMyGameInstanceSubsystem>();
                }
            }
        }

        return nullptr;
    }
}

これであればActorなどUObjectのクラスからは以下の様にアクセスできるようになります。

void AHogeActor::Hoge()
{
    UMyGameInstanceSubsystem::Get(this)->Test();
}

ちなみにグローバルのGEngineから取得する場合は以下のようになると思いますが、推奨はされていないとのことです。
グローバルからの参照が推奨されない理由についてはもう少し調査/情報が必要です。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{

public:
    static UMyGameInstanceSubsystem* Get()
    {
        if (GEngine)
        {
            FWorldContext* context = GEngine->GetWorldContextFromGameViewport(GEngine->GameViewport);
            if (context)
            {
                UGameInstance* gameInstance = context->OwningGameInstance;

                if (gameInstance)
                {
                    return gameInstance->GetSubsystem<UMyGameInstanceSubsystem>();
                }
            }
        }

        return nullptr;
    }
}

別の方法としてはUBlueprintFunctionLibraryを継承したクラスなどで取得関数など用意する方法も考えられます。
※用意した関数はtemplateを使用しているのでBPに公開はできないのでUBlueprintFunctionLibraryを継承しなくても良いとは思われます。

UCLASS()
class HOGE_API UHogeUtility : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
    
    
public:
    template<class T>
    static T* GetGameInstanceSubsystem(const UObject* worldContextObject)
    {
        if (worldContextObject)
        {
            UWorld* world = worldContextObject->GetWorld();

            if (world)
            {
                auto* gameInstance = world->GetGameInstance<UGameInstance>();

                if (gameInstance)
                {
                    return gameInstance->GetSubsystem<T>();
                }
            }
        }
        
        return nullptr;
    }
};

この様にしてアクセスもできますが、若干冗長な気がします。

void AHogeActor::Hoge()
{
    UHogeUtility::GetGameInstanceSubsystem<UMyGameInstanceSubsystem>(this)->Test();
}

自分の方法が正しいものとは限らないので参考程度にしていただければと思います。
皆さんはどのように実装しているものでしょうか?
なにか他に良い方法などがあれば情報を頂けると幸いです。

参考

docs.unrealengine.com

www.docswell.com

pafuhana1213.hatenablog.com