2020/11/23エンジニア

    [Unreal Engine] UE4非同期処理とウィジェットについて(2)

    執筆者 / K.T

    はじめに

    時間のかかる処理を実行すると、画面更新が止まるなどの弊害が起きることがあります。

    このような問題を避けるために、非同期処理の仕組みが用意されています。

    サンプルとして、画面上のテキストを毎フレーム更新しながら、並行して時間のかかる処理を行う仕組みを作成します。

    この記事の内容

    長いので、2回に分けてお送りします。

    前回(1/2)は「ウィジェット」というユーザーインタフェースの表示部分を作成しました。

    今回(2/2)は「ウィジェット」の動作部分を作成し、時間のかかる処理が非同期で実行されるようにします。

    用意するもの

    ・Unreal Engine 4.23.1

    ・Visual Studio 2017

    ブループリントライブラリの作成

    まず、時間のかかる処理を、C++のブループリントライブラリとして作成しましょう。

    C++クラスを新規追加してください。

    親クラスはBlueprint Function Libraryを選択します。

    VisualStudioが開くので、追加したMyFuncBPのヘッダファイルとソースファイルを、以下のように編集します。

    編集が終わったら、ソリューションをビルドして、保存後に閉じてください。

    MyFuncBP.h

    #pragma once

    #include "CoreMinimal.h"
    #include "Kismet/BlueprintFunctionLibrary.h"
    #include "Async/AsyncWork.h"
    #include "MyFuncBP.generated.h"

    // 終了通知デリゲートの宣言
    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FNoticeEnd);

    // BP関数クラス
    UCLASS(Blueprintable)
    class ASYNCTASKSAMPLE_API UMyFuncBP : public UBlueprintFunctionLibrary
    {
         GENERATED_BODY()

    public:
        
    // 終了通知デリゲート
         UPROPERTY(BlueprintAssignable, Category=
    "MyFuncBP")
         FNoticeEnd     NoticeEnd;

        
    // 開始
         UFUNCTION(BlueprintCallable, Category=
    "MyFuncBP")
        
    void      Start();

    private:
        
    // タスク
        
    static void    MyTask(FNoticeEnd notice_end);
    };
         UFUNCTION(BlueprintCallable, Category=
    "ControlActor")
        
    static void    BindEvent(UObject* object, FName func_name);
    };


    MyFuncBP.cpp

    #include "MyFuncBP.h"

    // 開始
    void UMyFuncBP::Start(void)
    {
         UMyFuncBP::MyTask(NoticeEnd);     
    // タスクを同期実行
    }

    // タスク
    void UMyFuncBP::MyTask(FNoticeEnd notice_end)
    {
         FPlatformProcess::Sleep(
    10.0f);   // 時間のかかる処理
         notice_end.Broadcast();          
    // 終了通知
    }

    ポイントとなる部分について、簡単に解説します。

    ・MyFuncBP.h(9行目)

    FNoticeEndという動的マルチキャストデリゲートを宣言しています。

    時間のかかる処理が終わった後に、ブループリント側へ、イベントという形で終了通知するためのものです。

    ・MyFuncBP.cpp(6行目)

    時間のかかる処理を含むタスクを実行します。(今はまだ非同期ではなく、同期実行です)

    ・MyFuncBP.cpp(12~13行目)

    時間のかかる処理の例として、10秒間のスリープを行います。

    処理が終わったら、終了通知デリゲートを呼び出します。

    ウィジェットの動作部を作成

    再びUE4プロジェクトに戻り、WidgetBPを開きます。

    配置済みのButtonを選択し、詳細からイベントOn Clickedの+ボタンをクリックしてください。

    グラフ編集モードに切り替わります。

    イベントグラフには、ボタンが押された時に呼び出されるOn Clicked (Button)ノードが追加されています。

    現在のフレーム番号を保存するための値を作成します。

    フレーム番号のテキストとバインドする(紐付ける)文字列を作成します。

    フレーム番号の文字列を複製して、他の文字列を作成します。

    名前以外のパラメータはそのままで構いません。

    先ほど作成したブループリントライブラリの参照を作成します。

    ここまでで、動作に必要な素材が揃いました。

    ここからは、動作の内容をブループリントで記述していきましょう。

    まず、プレイ開始時に呼び出される初期化関数です。

    以下のような処理を行っています。

     (1) ボタン上の文字列を設定

     (2) ブループリントライブラリを生成し、参照を保存

     (3) 終了通知デリゲートに、カスタムイベントをバインド

     (4) カスタムイベントで文字列を設定(ボタン上・終了フレーム番号)

    次は、毎フレーム呼び出されるティック関数です。

    以下のような処理を行っています。

     (1) 現在のフレーム番号を+1

     (2) 現在のフレーム番号を、フレーム番号の文字列に設定

    次は、ボタンが押された時に呼び出されるイベントです。

    以下のような処理を行っています。

     (1) ボタンを無効化(2回以上押されることを防ぐため)

     (2) ボタン上の文字列を設定

     (3) 開始フレーム番号の文字列を設定

     (4) ブループリントライブラリのStart関数を呼び出し

    以上で、動作の内容をブループリントで記述できました。

    続いて、各テキストと文字列をバインドしていきましょう。

    ブループリントデザイナーモードに切り替えてください。

    フレーム番号のテキストを選択した状態で、詳細の[Content]-[Text]右端のリストボックスから、対応する文字列FrameNoStringを選択します。

    これによって、文字列FrameNoStringを変更すると、それがフレーム番号のテキストへ反映されるようになりました。

    同様にして、他のテキストも、対応する文字列とバインドしてください。

    動作を確認(同期実行)

    ウィジェットの動作部分が完成したので、再び動作を確認してみましょう。

    ツールバーの[プレイ]-[選択ビューポート]を選択します。

    前回と異なり、フレーム番号がカウントアップされるようになりました。

    次に、ボタンを押してみてください。

    フレーム番号のカウントアップが止まり、しばらくして再び動き出します。

    この時、開始フレーム番号と終了フレーム番号は、同一になっています。

    ボタンを押した後、内部では以下のような順番で処理が行われています。

     (1) ウィジェットブループリントのOn Clicked (Button)ノードが呼び出される

     (2) 開始フレーム番号として現在のフレーム番号を保存

     (3) UMyFuncBPクラスのStart関数を呼び出し

     (4) UMyFuncBPクラスのMyTask関数を呼び出し

     (5) 時間のかかる処理(例:10秒間のスリープ)を実行

     (6) 終了通知デリゲートを呼び出し

     (7) ウィジェットブループリントのカスタムイベントが呼び出される

     (8) 終了フレーム番号として現在のフレーム番号を保存

    問題は、(1)~(8)がメインスレッドで「順番に」処理される点です。

    (5)で時間のかかる処理が終了するまでは、(6)以降の処理は行われずに待たされることになります。

    そのため、本来は数十FPSで切り替わるべきフレーム番号が、長時間変化せずに止まってしまいます。

    これはいわゆる「処理落ち」の極端な症状とも言えます。

    問題を解消するには

    「順番に」処理することで問題が起きるのですから、前後の処理を「並行して」行えば、問題は解消されるはずです。

    具体的には、(5)~(6)を非同期タスクとして切り離し、メインスレッドと並行して処理します。

    こうすれば、(5)~(6)が処理されている間にも、メインスレッドでは通常どおり数十FPSでフレーム番号を切り替えていくことができます。

    それでは、ブループリントライブラリを修正しましょう。

    VisualStudioを開いて、MyFuncBPのヘッダファイルとソースファイルを、以下のように編集します。

    編集が終わったら、ソリューションをビルドして、保存後に閉じてください。

    MyFuncBP.h

    #pragma once

    #include "CoreMinimal.h"
    #include "Kismet/BlueprintFunctionLibrary.h"
    #include "Async/AsyncWork.h"
    #include "MyFuncBP.generated.h"

    // 終了通知デリゲートの宣言
    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FNoticeEnd);

    // BP関数クラス
    UCLASS(Blueprintable)
    class ASYNCTASKSAMPLE_API UMyFuncBP : public UBlueprintFunctionLibrary
    {
         GENERATED_BODY()

    public:
        
    // 終了通知デリゲート
         UPROPERTY(BlueprintAssignable, Category=
    "MyFuncBP")
         FNoticeEnd     NoticeEnd;

        
    // 開始
         UFUNCTION(BlueprintCallable, Category=
    "MyFuncBP")
        
    void      Start();

    private:
        
    // タスク
        
    static void    MyTask(FNoticeEnd notice_end);
    };

    // 非同期タスククラス
    class FMyAsyncTask : public FNonAbandonableTask
    {
        
    friend class FAutoDeleteAsyncTask<FMyAsyncTask>;

    public:
         FORCEINLINE TStatId     GetStatId()
    const
         {
              RETURN_QUICK_DECLARE_CYCLE_STAT(FMyAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
         }

        
    // コンストラクタ
         FMyAsyncTask(TFunction<
    void(FNoticeEnd)> my_task, FNoticeEnd notice_end)
              : m_MyTask(my_task)
              , m_NoticeEnd(notice_end){}

        
    // タスクを実行
        
    void      DoWork(){ m_MyTask(m_NoticeEnd); }

    private:
         TFunction<
    void(FNoticeEnd)>  m_MyTask;      // タスク
         FNoticeEnd     m_NoticeEnd;                
    // 終了通知デリゲート
    };


    MyFuncBP.cpp

    #include "MyFuncBP.h"

    // 開始
    void UMyFuncBP::Start(void)
    {
        
    auto      async_task = new FAutoDeleteAsyncTask<FMyAsyncTask>(UMyFuncBP::MyTask, NoticeEnd);
         async_task->StartBackgroundTask();    
    // タスクを非同期実行
    }

    // タスク
    void UMyFuncBP::MyTask(FNoticeEnd notice_end)
    {
         FPlatformProcess::Sleep(
    10.0f);   // 時間のかかる処理
         notice_end.Broadcast();          
    // 終了通知
    }

    ポイントとなる部分について、簡単に解説します。

    ・MyFuncBP.cpp(6~7行目)

    FAutoDeleteAsyncTaskクラスは、指定のタスクを非同期実行した後で、自動的に削除処理を行う仕組みを持っています。

    StartBackgroundTask関数は、呼び出し後にブロックせず、すぐ戻ります。

    このため、非同期タスクと並行して、メインスレッドの処理を続行することができます。

    動作を確認(非同期実行)

    再びUE4プロジェクトに戻り、プレイを開始してみましょう。

    ボタンを押した後も、フレーム番号は止まらずにカウントアップを続けています。

    また、開始フレーム番号が設定され、ボタン上のテキストも更新されています。

    しばらく待つと、終了フレーム番号が設定され、ボタン上のテキストが更新されます。

    これで、非同期実行されていた時間のかかる処理が終了したことがわかります。

    無事に問題を解消することができました。

    まとめ

    前回(1/2)はウィジェットの表示部分を作成し、ビューポートに追加して画面に表示されるようにしました。

    今回(2/2)はウィジェットの動作部分を作成し、時間のかかる処理が非同期に実行されるようにしました。

    処理落ちが問題になる場合、非同期処理という回避策があることを思い出していただければ幸いです。

    参考リンク

    ブログ一覧へ戻る