UE5Coro の紹介
はじめに
この記事は Unreal Engine (UE) Advent Calendar 2024 シリーズ 3 の 9 日目の記事です。
Unreal Engine のコルーチンライブラリである UE5Coro の紹介です。
Unreal Engine のコルーチン対応についてはすでに詳しい紹介がされている記事があります。是非そちらもご一読ください。
私自身も以前 C++ のコルーチン、 async/await の記事を書いています。
この記事では UE でのコルーチン (というか async/await) を現時点でまともに使おうとした場合の選択肢である UE5Coro について書いていきます。
今回の記事では基本的には下記の環境で試しています。
- Unreal Engine 5.4.4 (部分的に 5.5.0 でも確認)
- Windows 11
- Visual Studio 2022 v17.11.5
- Rider 2024.2.6
UE5Coro とは
Unreal Engine 用のコルーチン、非同期ランタイムのライブラリです。
C++20 ではコルーチン、async/await は仕様として定義されていますが、 UE5Coro は Unreal Engine に適合した形でランタイム実装がされており、ごく自然な形で Unreal C++ 中に async/await を使用することができるのが大きな特徴です。Blueprint と混ぜて使うことも可能です。
Unreal Engine の Core ライブラリでもコルーチン対応が進められているようですが、 5.5.0 時点でも Experimental のようです。
UE5Coro の導入
1.0 系では C++17 や古い UE を対象にしているようですが、 C++20 を前提としたいので 2.0 以降を対象にします。UE も 5.3 以降対応となります。UE 5.3 からは C++20 がデフォルトです。
Releases ページから .zip をダウンロードし、導入するプロジェクトの Plugins フォルダにコピーします。その際、 UE5Coro のルートフォルダ名を "UE5Coro" に変更してください (zip の名前のままにしない) 。
導入後、 Build.cs に UE5Coro のモジュールを追加します。 "UE5Coro" モジュールが必須ですが、使用する機能によっては追加のモジュール定義が必要になります。
Windows での注意
UE5Coro の 2.0 では MSVC v14.41 以降を要求しますが、 UE 5.4.4, 5.5.0 では使用するコンパイラーが標準では v14.38 であるためそのままでは使用できません。
下記の記事を参考に、 *Target.cs にコンパイラーのバージョンを設定します。
public UE5CoroTest54Target(TargetInfo Target) : base(Target)
{
// 略
WindowsPlatform.Compiler = WindowsCompiler.VisualStudio2022;
WindowsPlatform.CompilerVersion = "14.41.34120";
}
コルーチン
UE5Coro には様々な機能がありますが、メインとなるのはコルーチンです。
UE5Coro では UE5Coro::TCoroutine 型を戻り値とするメソッドがコルーチンとなります。 TCoroutine を返すメソッドは実装内で await 可能となっています。
UE5Coro には実行モードとして二つのモードがあります。
- Async mode
- Latent mode
Async mode
Async mode は比較的 C++ 寄りのモードです。
- 実装したメソッドは Blueprint からアクセスすることはできません (C++ からのみ利用できる)
- 結果を戻り値として返すことができます (TCorutine<T> を使用する)
Async mode は条件が緩い、というか Latent mode の条件を満たさない場合 Async mode となります。
UE5Coro::TCoroutine<FTimespan> TestAsync()
{
auto s = FDateTime::Now();
co_await UE5Coro::Latent::Seconds(1);
co_return FDateTime::Now() - s;
}
Latent::Seconds は Delay に相当する UE5Coro の関数です。
Latent mode
Latent mode は Unreal Engine と互換性が高いモードです。 Latent mode として実装した関数は Blueprint の Latent Node になります。よって Blueprint からアクセス可能な関数になります。 Latent Node にすることで TCorutine のインスタンス管理などを考慮する必要がなくなります。
- 実装したメソッドは Blueprint からアクセス可能です
- 結果を戻り値として返すことができません (Latent Node になるため)
Latent Node にするには次の形にする必要があります。
- 戻り値型を FVoidCoroutine にする必要があります
- FVoidCoroutine は TCorutine を継承した構造体ですが、Blueprint から扱えるように調整がされています
- Latent Node にするための UE C++ の UFUNCTION 設定が必要です
- 引数に FLatentActionInfo を持たせ、Latent, LatentInfo の meta 指定が必要です (Latent Node なので)
実装例です。
UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = LatentInfo))
FVoidCoroutine LatentTestCoro(FLatentActionInfo LatentInfo)
{
co_await UE5Coro::Async::MoveToThreadPool(*GBackgroundPriorityThreadPool);
FPlatformProcess::Sleep(1.0f);
co_await UE5Coro::Async::MoveToGameThread();
}
MoveToThreadPool, MoveToGameThread は後程解説しますが、スレッドの移動をする関数です。
このように書くだけで Blueprint 上では Latent Node として見えるようになります。
通常の Latent Node の実装で上と同じ処理を書いた場合は次のようになります。 UE5Coro を使った場合と比べてあまりにも面倒くさいことになっています。
class FLatentTestLatentAction : public FPendingLatentAction
{
private:
FLatentActionInfo LatentInfo;
std::atomic_bool Completed;
public:
FLatentTestLatentAction(const FLatentActionInfo& LatentInfo) :
LatentInfo(LatentInfo),
Completed(false)
{
Async(EAsyncExecution::ThreadPool, [this]
{
FPlatformProcess::Sleep(1.0f);
Completed = true;
});
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
Response.FinishAndTriggerIf(Completed, LatentInfo.ExecutionFunction, LatentInfo.Linkage,
LatentInfo.CallbackTarget);
}
};
UFUNCTION(BlueprintCallable, meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo"))
void LatentTest(UObject* WorldContextObject, FLatentActionInfo LatentInfo)
{
auto world = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (world)
{
world->GetLatentActionManager().AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FLatentTestLatentAction(LatentInfo));
}
}
Async mode と Latent mode の使い分け
C++ で完結する範囲は Async mode で書いた方が制約がないのでベターでしょう。一方、 Async mode では TCorutine のインスタンスを最後まで管理をする必要があると思いますので、 Latent mode のメソッド内で co_await をして処理しきるようにし、 Latent mode のメソッドを Blueprint から呼ぶのが基本的な形になるのではないかと考えています。
スレッドの移動
UE5Coro にはスレッドの移動をする関数がいくつかあります。これを使用して長い時間のかかる処理を別のスレッドに逃がして実行させるコードを簡単に書くことができます。
UE5Coro::TCoroutine<> TestAsync2()
{
co_await UE5Coro::Async::MoveToTask();
// ワーカースレッド
// 何か長い処理
co_await UE5Coro::Async::MoveToGameThread();
// メインスレッド (GameThread)
}
このようにワーカースレッドで処理を行い、完了したら GameThread に戻ってくる、というよくある処理の実装をするのが UE5Coro の主となる使い方になるかなと思います。
- MoveToGameThread → GameThread へ移動
- MoveToThread → 指定のスレッドタイプのスレッドへ移動
- MoveToThreadPool → 指定のスレッドプールのスレッドへ移動
- MoveToTask → UE の Task で用意したスレッドへ移動
便利機能
UE5Coro の主要機能ではありませんが、 UE5Coro を使用するのにあたって便利なユーティリティがいくつか実装されています。ここで挙げていないものもありますので、ドキュメントも参照してください。
Latent awaiters
GameThread 上で使う awaiter のユーティリティです。Delay に相当する関数などがあります。Latent awaiters は GameThread 以外で使用するとエラー中断しますので注意してください。
UE5Coro::TCoroutine<> LatentSample()
{
co_await UE5Coro::Latent::Seconds(2); // 2秒待ちます
co_await UE5Coro::Latent::NextTick(); // 次の Tick まで待ちます
}
Aggregate awaiters
複数のコルーチンをまとめて待機するためのユーティリティです。 C# の Task クラスにある When 系などと同じ用途です。
UE5Coro::TCoroutine<> AggregateSample()
{
auto Awaiter1 = UE5Coro::Latent::Ticks(5);
auto Awaiter2 = UE5Coro::Latent::Ticks(10);
auto Awaiter3 = UE5Coro::Latent::Ticks(15);
co_await UE5Coro::WhenAll(std::move(Awaiter1), std::move(Awaiter2), std::move(Awaiter3));
}
終わりに
最近使っているプログラミング言語ではほぼ async/await が使えてるので Unreal Engine でも同じように async/await を使いたいので真っ先に調べました。
UE5Coro は使用感は悪くないですし Blueprint にも混ぜて使えるのはすごくよいのですが、コンパイラバージョンを UE 標準対応バージョンより上げなくてはいけない (5.5.0 時点) というのが微妙にリスキーな感じで本番で使うにはまだ二の足を踏む感じですね。自分的には BlueprintAsyncAction で非同期コードを書くのはそんなに面倒ではなかったので当面は BluePrintAsyncAction でなんとかするかなという感じです。
Unreal Engine でも async/await は可能なら積極的に使いたいところなので (UE 本体も含めて) 今後の発展に期待しています。
Discussion