🔄

[UE5]GameInstanceSubsystemを導入してみた

2024/10/03に公開

はじめに

Subsystemという名前は聞いていたけど、実際どんな使い方をしてどんな役に立つかまでは詳しく知っていなかったので、少し調べてみたらあらびっくり。
便利そうな匂いがプンプンするので使ってみました。
そして改めて思ったのですが、自分の記事でC++が必要になったのは今回が初めてで、コードを見ると全身から汗が噴き出てしまうアレルギー持ちの方にもできるだけコピペでいけるように & 便利にできるようにしましたので、ブラウザバックせずに最後までお付き合いください。

GameInstanceSubsystemってなんやねん

そもそもSubsystemには何種類かあり、それぞれライフタイム(生成されるタイミングと破棄されるタイミング)が違います。
詳しくはこちらを見てください。

https://www.docswell.com/s/EpicGamesJapan/KY18EZ-UE4_AllStudy04_Subsystem#p34

今回紹介するGameInstanceSubsystemは名の通りGameInstanceにくっついているものなので、ライフタイムはGameInstanceと同じになります。
つまりゲームが開始したときに生成され、ゲームが終了したときに破棄されます。
ゲーム全体で使う処理を作りたいけど、アクターとかで管理するのは違うし、Interfaceでゴリ押し実装するのも美しくないなあ・・・という悩みを解決してくれます!
ただ、BPのみでSubsystemを実装することはできないことが導入ハードルを上げている原因だと思います。

成果物

この記事通りに進めると下記のアセットとノードだけでどこでも関数が呼び出せるようになります。

  • 拡張用BP
  • 拡張用BPの中身
  • 呼び出し側

環境

  • UE5.3.2
  • Windows11
  • VisualStudio 2022

全体コード

記事読むのめんどいから結果だけ教えてくれればええねんという方の期待に応えるために、先にコード全体だけ載せておきます。
この記事の手順6.その他の拡張まで行ったコードになります。
クラス名が同じならコピペで行けます。(プロジェクト名はSubsystemTestです)
詳しい説明は次のセクションから行います。

MyDeveloperSettings.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "MyDeveloperSettings.generated.h"

UCLASS(config = Game, defaultconfig)
class SUBSYSTEMTEST_API UMyDeveloperSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	virtual FName GetContainerName() const override;
	virtual FName GetCategoryName() const override;
	virtual FName GetSectionName() const override;

#if WITH_EDITOR

	virtual FText GetSectionText() const override;
	virtual FText GetSectionDescription() const override;

#endif

	// Subsystemを追加したらここにプロパティを追加する

	// Specify the Blueprint asset for the subsystem extension.
	UPROPERTY(EditAnywhere, Config, Category = "Subsystem Paths")
	FString MyGameInstanceSubsystem;
};
MyDeveloperSettings.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyDeveloperSettings.h"

#define LOCTEXT_NAMESPACE "UMyDeveloperSettings"

FName UMyDeveloperSettings::GetContainerName() const
{
	return Super::GetContainerName();
}
FName UMyDeveloperSettings::GetCategoryName() const
{
	// 新しいDeveloperSettingsクラスを作成し、ここを同じカテゴリ名にすると同じセクションに追加されます
	return "SubsystemTest";
}
FName UMyDeveloperSettings::GetSectionName() const
{
	return Super::GetSectionName();
}

#if WITH_EDITOR
FText UMyDeveloperSettings::GetSectionText() const
{
	// セクション名
	return LOCTEXT("UMyDeveloperSettings::GetSectionText", "Subsystem");
}
FText UMyDeveloperSettings::GetSectionDescription() const
{
	// 説明文
	return LOCTEXT("UMyDeveloperSettings::GetSectionDescription", "This is the configuration for the subsystem.");
}
#endif

#undef LOCTEXT_NAMESPACE
MyGameInstanceSubsystem.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

// 拡張用BPベースクラス
UCLASS(Abstract, Blueprintable, MinimalAPI, meta = (ShowWorldContextPin))
class UMyGameInstanceSubsystemHelperBase : public UObject
{
	GENERATED_BODY()

public:
	// Test Function 1
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction1();

	// Test Function 2
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction2(const int32& MyInt);

	// Test Function 3
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	FString TestFunction3();
};

UCLASS()
class SUBSYSTEMTEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UMyGameInstanceSubsystem();

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	// Subsystem extension blueprint
	UPROPERTY(Transient, BlueprintReadOnly)
	UMyGameInstanceSubsystemHelperBase* SubsystemHelper;

	UFUNCTION(BlueprintCallable, Category = "MyGameInstanceSubsystem")
	void TestFunction2(const int32& MyInt)
	{
		if (SubsystemHelper)
		{
			SubsystemHelper->TestFunction2(MyInt);
		}
	}

	UFUNCTION(BlueprintCallable, Category = "MyGameInstanceSubsystem")
	FString TestFunction3()
	{
		if (SubsystemHelper)
		{
			SubsystemHelper->TestFunction3();
		}

		// SubsystemHelper が null の場合のデフォルトの戻り値
		return FString(TEXT("Default Value"));
	}

private:
	// 拡張用BPクラス
	TSubclassOf<class UMyGameInstanceSubsystemHelperBase> SubsystemHelperClass;
};

MyGameInstanceSubsystem.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstanceSubsystem.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
#include "MyDeveloperSettings.h"

UMyGameInstanceSubsystem::UMyGameInstanceSubsystem()
{
	// プロジェクト設定を取得
	const UMyDeveloperSettings* DeveloperSettings = GetDefault<UMyDeveloperSettings>();

	if (DeveloperSettings)
	{
		if (!DeveloperSettings->MyGameInstanceSubsystem.IsEmpty())
		{
			// MyGameInstanceSubsystemの項目で指定したパスからBPをロードする
			static ConstructorHelpers::FClassFinder<UMyGameInstanceSubsystemHelperBase> SubsystemBlueprint(*DeveloperSettings->MyGameInstanceSubsystem);

			if (SubsystemBlueprint.Class)
			{
				SubsystemHelperClass = (UClass*)SubsystemBlueprint.Class;
			}
			else
			{
				UE_LOG(LogTemp, Warning, TEXT("Failed to find Subsystem Blueprint. Please check the path: %s"), *DeveloperSettings->MyGameInstanceSubsystem);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Subsystem path is empty."));
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("MyDeveloperSettings is invalid."));
	}
}

// 初期化
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	SubsystemHelper = nullptr;
	if (SubsystemHelperClass)
	{
		SubsystemHelper = NewObject<UMyGameInstanceSubsystemHelperBase>(GetTransientPackage(), SubsystemHelperClass);
	}
	UE_LOG(LogTemp, Warning, TEXT("MyGameInstanceSubsystem:Initialize"));
}

// 破棄
void UMyGameInstanceSubsystem::Deinitialize()
{
	SubsystemHelper = nullptr;
	UE_LOG(LogTemp, Warning, TEXT("MyGameInstanceSubsystem:Deinitialize"));
}

手順

  1. プロジェクト設定にオリジナル設定を追加する(コピペ)
  2. UGameInstanceSubsystemを継承してクラスを作成する
  3. 生成と設定のコードを書く(コピペ)
  4. 拡張用BPを作成する
  5. 動作確認
  6. その他の拡張

1. プロジェクト設定にオリジナル設定を追加する(コピペ)

C++で拡張用BPの情報を取得する場合拡張用BPのパスを書くことになりますが、ハードコーディングで記述するのはあまりよろしくはないので、プロジェクト設定から設定できるようにします。
historia様の記事を参考にしました。
https://historia.co.jp/archives/18025/

「Tool」 > 「New C++ Class...」から新規C++クラスを作成します。
All ClassesからDeveloperSettingsを選択します。

クラス名はデフォルトのままで進めますが、適宜変えてください。

VisualStudioが起動したら、下記のコードをそれぞれ.hと.cppにコピペします。

MyDeveloperSettings.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "MyDeveloperSettings.generated.h"

UCLASS(config = Game, defaultconfig)
class SUBSYSTEMTEST_API UMyDeveloperSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	virtual FName GetContainerName() const override;
	virtual FName GetCategoryName() const override;
	virtual FName GetSectionName() const override;

#if WITH_EDITOR

	virtual FText GetSectionText() const override;
	virtual FText GetSectionDescription() const override;

#endif

	// Specify the Blueprint asset for the subsystem extension.
	UPROPERTY(EditAnywhere, Config, Category = "Subsystem Paths")
	FString MyGameInstanceSubsystem;
};
MyDeveloperSettings.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyDeveloperSettings.h"

#define LOCTEXT_NAMESPACE "UMyDeveloperSettings"

FName UMyDeveloperSettings::GetContainerName() const
{
	return Super::GetContainerName();
}
FName UMyDeveloperSettings::GetCategoryName() const
{
	// 新しいDeveloperSettingsクラスを作成し、ここを同じカテゴリ名にすると同じセクションに追加されます
	return "SubsystemTest";
}
FName UMyDeveloperSettings::GetSectionName() const
{
	return Super::GetSectionName();
}

#if WITH_EDITOR
FText UMyDeveloperSettings::GetSectionText() const
{
	// セクション名
	return LOCTEXT("UMyDeveloperSettings::GetSectionText", "Subsystem");
}
FText UMyDeveloperSettings::GetSectionDescription() const
{
	// 説明文
	return LOCTEXT("UMyDeveloperSettings::GetSectionDescription", "This is the configuration for the subsystem.");
}
#endif

#undef LOCTEXT_NAMESPACE

ビルドしてプロジェクトを起動し、プロジェクト設定を開くと一番上に項目が追加されているはずです。

2. UGameInstanceSubsystemを継承してクラスを作成する

「Tool」 > 「New C++ Class...」から新規C++クラスを作成します。
All ClassesからGameInstanceSubsystemを選択します。

クラス名はデフォルトのままで進めますが、適宜変えてください。

3. 生成と設定のコードを書く(コピペ)

今回の記事では、よりシンプルな使い方ができるように拡張用BPの生成と設定をC++側で行います。
BP側でもできるようですが、少しめんどうに感じたのでC++側で行うことをおすすめします。

拡張用BPのパスは先ほどのMyDeveloperSettingsクラスから取得する前提で記述しています。

MyGameInstanceSubsystem.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

// 拡張用BPベースクラス
UCLASS(Abstract, Blueprintable, MinimalAPI, meta = (ShowWorldContextPin))
class UMyGameInstanceSubsystemHelperBase : public UObject
{
	GENERATED_BODY()

public:
	// Test Function 1
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction1();
};

UCLASS()
class SUBSYSTEMTEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UMyGameInstanceSubsystem();

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	// Subsystem extension blueprint
	UPROPERTY(Transient, BlueprintReadOnly)
	UMyGameInstanceSubsystemHelperBase* SubsystemHelper;

private:
	// 拡張用BPクラス
	TSubclassOf<class UMyGameInstanceSubsystemHelperBase> SubsystemHelperClass;
};
MyGameInstanceSubsystem.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstanceSubsystem.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
#include "MyDeveloperSettings.h"

UMyGameInstanceSubsystem::UMyGameInstanceSubsystem()
{
	// プロジェクト設定を取得
	const UMyDeveloperSettings* DeveloperSettings = GetDefault<UMyDeveloperSettings>();

	if (DeveloperSettings)
	{
		if (!DeveloperSettings->MyGameInstanceSubsystem.IsEmpty())
		{
			// MyGameInstanceSubsystemの項目で指定したパスからBPをロードする
			static ConstructorHelpers::FClassFinder<UMyGameInstanceSubsystemHelperBase> SubsystemBlueprint(*DeveloperSettings->MyGameInstanceSubsystem);

			if (SubsystemBlueprint.Class)
			{
				SubsystemHelperClass = (UClass*)SubsystemBlueprint.Class;
			}
			else
			{
				UE_LOG(LogTemp, Warning, TEXT("Failed to find Subsystem Blueprint. Please check the path: %s"), *DeveloperSettings->MyGameInstanceSubsystem);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Subsystem path is empty."));
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("MyDeveloperSettings is invalid."));
	}
}

// 初期化
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	SubsystemHelper = nullptr;
	if (SubsystemHelperClass)
	{
		SubsystemHelper = NewObject<UMyGameInstanceSubsystemHelperBase>(GetTransientPackage(), SubsystemHelperClass);
	}
}

// 破棄
void UMyGameInstanceSubsystem::Deinitialize()
{
	SubsystemHelper = nullptr;
}

4. 拡張用BPを作成する

Editorに戻ります。
新規Blueprintクラスを作成して、親クラスに先ほど作ったMyGameInstanceSubsystemHelperBaseを指定します。

作成したBPを開いて関数のオーバーライドを選択すると、C++で設定したTestFunction1があるはずです。

この関数に適当にPrintStringを仕込んでおきます。

最後にプロジェクト設定に拡張用BPのパスを指定します。

これで設定は完了です。

5. 動作確認

BP_ThirdPersonCharacterで実行してみます。
右クリックの検索から、MyGameInstanceSubsystemSubsystemHelperを配置し、SubsytemHelperからTestFunction1を配置します。

1キーを連打するとしっかり実行されていますね。

6. その他の拡張

6.1. 生成と破棄の確認

MyGameInstanceSubsystem.cppの初期化と破棄のところに、LOGを仕込みます。
分かりやすいようにWarningにしています。

MyGameInstanceSubsystem.cpp
// 初期化
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	SubsystemHelper = nullptr;
	if (SubsystemHelperClass)
	{
		SubsystemHelper = NewObject<UMyGameInstanceSubsystemHelperBase>(GetTransientPackage(), SubsystemHelperClass);
	}
	UE_LOG(LogTemp, Warning, TEXT("MyGameInstanceSubsystem:Initialize"));
}

// 破棄
void UMyGameInstanceSubsystem::Deinitialize()
{
	SubsystemHelper = nullptr;
	UE_LOG(LogTemp, Warning, TEXT("MyGameInstanceSubsystem:Deinitialize"));
}

PIEしてみると、OutputLogにInitializeが出て、PIEを終了するとDeinitializeが出ていると思います。

6.2. 関数に引数を持たせる方法

記事内で説明したTestFunction1のようにシンプルに実行だけするのではなく、引数を持たせたい場合のほうが多いと思います。
その場合は次のようにします。
TestFunction2を追加しました。

MyGameInstanceSubsystem.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

// 拡張用BPベースクラス
UCLASS(Abstract, Blueprintable, MinimalAPI, meta = (ShowWorldContextPin))
class UMyGameInstanceSubsystemHelperBase : public UObject
{
	GENERATED_BODY()

public:
	// Test Function 1
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction1();

	// Test Function 2
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction2(const int32& MyInt);
};

UCLASS()
class SUBSYSTEMTEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UMyGameInstanceSubsystem();

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	// Subsystem extension blueprint
	UPROPERTY(Transient, BlueprintReadOnly)
	UMyGameInstanceSubsystemHelperBase* SubsystemHelper;

	UFUNCTION(BlueprintCallable, Category = "MyGameInstanceSubsystem")
	void TestFunction2(const int32& MyInt)
	{
		if (SubsystemHelper)
		{
			SubsystemHelper->TestFunction2(MyInt);
		}
	}

private:
	// 拡張用BPクラス
	TSubclassOf<class UMyGameInstanceSubsystemHelperBase> SubsystemHelperClass;
};

同様に拡張用BPとBP_ThirdPersonCharacterで処理を作ります。


6.3. 関数に戻り値を持たせる方法

引数ありを説明したなら戻り値も・・・ということで。
TestFunction3を追加しました。

MyGameInstanceSubsystem.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

// 拡張用BPベースクラス
UCLASS(Abstract, Blueprintable, MinimalAPI, meta = (ShowWorldContextPin))
class UMyGameInstanceSubsystemHelperBase : public UObject
{
	GENERATED_BODY()

public:
	// Test Function 1
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction1();

	// Test Function 2
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	void TestFunction2(const int32& MyInt);

	// Test Function 3
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
	FString TestFunction3();
};

UCLASS()
class SUBSYSTEMTEST_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UMyGameInstanceSubsystem();

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	// Subsystem extension blueprint
	UPROPERTY(Transient, BlueprintReadOnly)
	UMyGameInstanceSubsystemHelperBase* SubsystemHelper;

	UFUNCTION(BlueprintCallable, Category = "MyGameInstanceSubsystem")
	void TestFunction2(const int32& MyInt)
	{
		if (SubsystemHelper)
		{
			SubsystemHelper->TestFunction2(MyInt);
		}
	}

	UFUNCTION(BlueprintCallable, Category = "MyGameInstanceSubsystem")
	FString TestFunction3()
	{
		if (SubsystemHelper)
		{
			SubsystemHelper->TestFunction3();
		}

		// SubsystemHelper が null の場合のデフォルトの戻り値
		return FString(TEXT("Default Value"));
	}

private:
	// 拡張用BPクラス
	TSubclassOf<class UMyGameInstanceSubsystemHelperBase> SubsystemHelperClass;
};



参考文献

https://historia.co.jp/archives/18025/

https://www.docswell.com/s/historia_Inc/5WVYJK-ue4-dataasset-subsystem-gameplayability#p1

https://www.main-function.com/entry/2024/06/29/085923

https://qiita.com/tukigaselio/items/7641ac38aabb9996963d

Discussion