とある事情で作ったゲームのセーブデータのセーブとロードをブループリントで非同期に扱うことが可能なプラグインをIndie-us GamesのGitHub上で公開しました。
非同期なので、セーブデータのセーブとロードの完了を持たずにゲームのバックグラウンドで処理が行われます。このプラグインを利用することで、とあるプラットフォーム上でセーブとロードがブロックされてしまい、ゲームが数秒以上停止してしまい、プレイに支障がでてしまうというケースに有効です。
ちなみにPCではセーブもロードも高速でほぼ一瞬で終わりますので、このプラグインの出番はほぼないと思います。
簡単な使い方を以下で解説します。
使い方
SaveGame継承の独自クラスを作成します。これは非同期でなくても同様です。自作SaveGameクラス上に変数を作り、あとは"Create Save Game Object"で普通通りにSaveGameを作成します。
自作SaveGame上にセーブ用の値を書き込みます。ここでは現在時間を書き込んで、確認できるようにしています。
"Async Save"というノードを呼び出し、保存します。これは"Save Game to Slot"の非同期版となっており、非同期セーブが完了するとCompletedが呼びだされます。セーブが完了していないうちに再度呼ぶとFailedが返ってきます。
"Async Load"というノードで読込を行います。"Load Game from Slot"の非同期版です。こちらは読み込み完了後に、SaveGameインスタンスを受け取ることができるので、これを自作SaveGameクラスへとキャストしてから使用する必要があります。
これであとは通常のセーブデータとして利用できるので、自由にご活用ください!
これより以下はプラグインの実装が気になるという方だけ読んでみてください。
ブループリントでマルチスレッドによる非同期処理
このプラグインではブループリント上でマルチスレッドを扱った非同期ノードを実装しています。少々特殊な実装となっていますので、技術的な部分を解説します。
"UBlueprintAsyncActionBase"を継承したクラスを作成し、非同期処理が完了後に呼び出されるデリゲートの実装などを行います。詳しい実装の方法はヒストリアさんのブログ記事や、英語ですがUnreal Wikiの情報が参考になります。
デリゲートの実装は少々特殊な構文が必要となるので、そこだけ注意をすれば後はそこまで複雑な実装ではありません。以下は今回実装したAsyncSaveの実装です。
#pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintAsyncActionBase.h" #include "AsyncSave.generated.h" class USaveGame; // 非同期ノード用 デリゲートの定義 DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAsyncSaveOutputPin); /** * Async Save */ UCLASS() class ASYNCSAVELOAD_API UAsyncSave : public UBlueprintAsyncActionBase { GENERATED_BODY() public: // 非同期完了デリゲート UPROPERTY(BlueprintAssignable) FAsyncSaveOutputPin Completed; // 処理失敗デリゲート UPROPERTY(BlueprintAssignable) FAsyncSaveOutputPin Failed; // ノード情報定義 UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject"), Category="AsyncSaveLoad") static UAsyncSave* AsyncSave(const UObject* WorldContextObject, USaveGame* SaveGameInstance, const FString& SlotName, const int32 UserIndex); // 非同期処理本体 virtual void Activate() override; private: const UObject* WorldContextObject; USaveGame* SaveGameInstance; FString SlotName; int32 UserIndex; };
"FAsyncSaveOutputPin"が非同期処理完了後のデリゲートを定義しています。static関数のAsyncSaveでオブジェクト初期化を行い、Activate関数で非同期処理の本体を実装します。では次に実装側(cpp)です。
#include "AsyncSave.h" #include "Engine/Classes/GameFramework/SaveGame.h" #include "Engine/Classes/Kismet/GameplayStatics.h" UAsyncSave* UAsyncSave::AsyncSave(const UObject* WorldContextObject, USaveGame* SaveGameInstance, const FString& SlotName, const int32 UserIndex) { UAsyncSave* Node = NewObject<UAsyncSave>(); Node->WorldContextObject = WorldContextObject; Node->SaveGameInstance = SaveGameInstance; Node->SlotName = SlotName; Node->UserIndex = UserIndex; return Node; } void UAsyncSave::Activate() { // 非同期処理中を確認 static bool bAsyncActive = false; if (!bAsyncActive) { // このラムダ式内が別スレッド処理 FFunctionGraphTask::CreateAndDispatchWhenReady([this]() { bAsyncActive = true; UGameplayStatics::SaveGameToSlot(SaveGameInstance, SlotName, UserIndex); Completed.Broadcast(); // 処理完了デリゲート bAsyncActive = false; }, TStatId(), NULL, ENamedThreads::AnyBackgroundThreadNormalTask); } else { Failed.Broadcast(); // 処理失敗デリゲート } }
一番重要なのはActivate関数内の"CreateAndDispatchWhenReady"に渡しているラムダ式です。このラムダ式内のコードは引数で指定された別のスレッド内で実行されるようになります。
どのスレッドで実行するかは"ENamedThreads"の列挙型で指定可能です。この例ではバックグラウンドスレッドのノーマルプライオリティタスクとして指定をしています。
そしてthisポインターをキャプチャーさせて、引数で受け取ったインスタンス情報を別スレッド上で扱えるようにします。当然ながらここはクリティカルセクションとなるので、割り込まれると困る場合はMutexオブジェクトなどを利用する必要があります。
全てが完了したら、デリゲートからBroadcastを呼び出し、ノードの完了通知を行います。これでブループリント上で非同期処理の結果を受け取ることができます。
注意すべきこと
ブループリントで別スレッドの非同期処理を扱った場合、処理は正しく実行されますがブレークポイントなどのハンドリングを行うことができなくなります。これはおそらくブループリントの処理がGame Thread(メインスレッド)上で行われているからです。
気軽にマルチスレッドを扱うことができるのは便利ですが、こういった副作用がありますので注意が必要そうです。