NEWS / BLOG
2020.02.18
UE4でシーケンサーをチェックするブルーティリティーを作ろう
#テックネタ

こんにちはー。
プログラマーの惣一郎さんです。

今回は、UE4のシーケンサーを機械的にチェックするブループリントユーティリティーについて紹介したいと思います。
(補足:UE4.23以降、ブループリントユーティリティーは死語となり、エディターユーティリティーブループリントという名前になったようです。てへ。)

先日まで、UE4を使用したプロジェクトに参加していました。
その中で、イベントパートのプログラムを担当していたのですが、イベントの多くがUE4のシーケンサーを利用して制作されました。
UE4を使用する方たちにはシーケンサーの便利さは周知のことだと思われますが、シーケンサーを簡単に説明すると、タイムラインに沿ってオブジェクトの移動やアニメーション、カメラなどを設定して、再生できる機能です。
公式ドキュメントはこちら。
https://docs.unrealengine.com/ja/Engine/Sequencer/Overview/index.html

シーケンサー自体はとても自由度の高い便利な機能なのですが、それを使ってイベントを作るのは人間ですから、どうしてもヒューマンエラーが存在します。
特に、イベントシーンの数が膨大な場合、イベント制作者の人数も増えがちですし、作られたイベントをチェックするのも大変です。
そこでうちのプロジェクトでは、中盤になってからではありますが、シーケンサーのヒューマンエラーを機械的にチェックするブループリントユーティリティーを作成しました。

なお、使用したUE4のバージョンは4.22ですので、それ以外では下記説明のソースコードがそのまま使えるとは限らないこと、ご容赦ください。


■チェックの実行方法
コンテンツブラウザでチェックしたいシーケンサーを選択して、右クリックからブループリントユーティリティーを実行です。
結果は、テキストファイルに出力するようにしました。

img_seq_001.png


■ブループリントユーティリティー
ブループリントユーティリティーが何であるのか、どのように作るのかについては、説明を省略します。
公式サイトはこちらです。
https://docs.unrealengine.com/ja/Engine/Editor/ScriptingAndAutomation/Blueprints/Blutilities/index.html

以下は、ブループリントユーティリティーのスクリーンショットです。
大きな流れとしては、
 1.選択したアセットからシーケンサー(LevelSequence)のみを選んで
 2.C++で自作したチェック関数に通して
 3.結果をテキストファイルに出力。

img_seq_002.png

※カスタムイベントには、「エディタで呼び出す」にチェックを入れないといけない事は忘れがちです。ご注意ください。

■シーケンサーをチェックする関数の大まかな流れ
シーケンサーをチェックする関数は、C++で作成しており、UBlueprintFunctionLibraryを継承した関数ライブラリとして実装しました。

(エディタモジュール内での実装が望ましいです)

UCLASS()
class SAMPLEPROJECTED_API UCommonFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
// システムのチェック用関数 UFUNCTION(BlueprintCallable, Category = "Sequencer") static bool CheckLevelSequence(UMovieSceneSequence* Sequence, FString & resultText); };

// [Debug]シーケンスデータの機械的チェック bool UCommonFunctionLibrary::CheckLevelSequence(UMovieSceneSequence* Sequence, FString &resultText) { UMovieScene* movieScene = nullptr; bool retval = false; int errorCount = 0; // 初期化 resultText = ""; #if WITH_EDITORONLY_DATA // nullチェック if (Sequence != nullptr) { movieScene = Sequence->GetMovieScene(); } if (movieScene == nullptr) { if (Sequence != nullptr) { resultText += "Check NG!! [" + Sequence->GetName() + "]\n"; } else { resultText += "Check NG!! [ --- ]\n"; } resultText += "ERROR type0. Sequence is null.\n"; return false; } if (movieScene->HasAnyFlags(RF_NeedLoad)) { movieScene->GetLinker()->Preload(movieScene); }

// ここ以降に、具体的なチェック処理を記述
//
// errorCount = hogeCheck(movieScene);
//
// ここまで

#endif
return (errorCount==0);
}

それでは以下に、具体的にチェックする内容とそのソースコードを紹介します。
(先日まで参加していたプロジェクト内の固有ルールではない、一般的にニーズがありそうな内容のみのご紹介です)


■イベントトラックについて
・イベントに関数がバインドされていない
 (作業者による純粋な設定漏れと、設定したのに上手く保存されていなかった場合によく発生していた印象です)
・EventPositionが違う
 (EventPositionを変更する必要があまり無いかもしれませんが・・・)

img_seq_003.png

    // マスタートラックのイベントトラックを確認
    {
        const TArray<UMovieSceneTrack*> otherTracks = movieScene->GetMasterTracks();
        for (UMovieSceneTrack* masterTrack : otherTracks)
        {
            if (masterTrack->IsA(UMovieSceneEventTrack::StaticClass()))
            {
                UMovieSceneEventTrack *eventTrack = Cast(masterTrack);
                if (eventTrack == nullptr) {
                    continue;
                }
                checkEventTrack(eventTrack, resultText, errorCount, base, FString(""));
            }
        }
    }

    // バインドされたオブジェクトのイベントトラックを確認
    const TArray bindings = movieScene->GetBindings();
    for (int i = 0; i < bindings.Num(); ++i)
    {
        const FMovieSceneBinding& binding = bindings[i];
        const TArray<UMovieSceneTrack*> bindingTracks = binding.GetTracks();
        for (UMovieSceneTrack* track : bindingTracks)
        {
            const UMovieSceneEventTrack* eventTrack = Cast(track);
            if (eventTrack != nullptr)
            {
                checkEventTrack(eventTrack, resultText, errorCount, base, binding.GetName());
            }
        }
    }

---------------

/// イベントトラックのチェック
bool checkEventTrack(const UMovieSceneEventTrack *eventTrack, FString &resultText, int &errorCount, int frameBase, const FString& objectName)
{
    bool result = true;

    // 再生位置確認
    if (eventTrack->EventPosition != EFireEventsAtPosition::AfterSpawn)
    {
        errorCount++;
        result = false;
        resultText += "ERROR 01. Event Position is not [After Spawn]\n";
    }

    // セクションごとに
    const TArray<UMovieSceneSection*> sections = eventTrack->GetAllSections();
    for (int i = 0; i < sections.Num(); ++i)
    {
        const UMovieSceneSection *section = sections[i];

        const UMovieSceneEventTriggerSection* sectTrg = Cast(section);
        if (sectTrg != nullptr)
        {
            resultText += "(" + FString::FromInt(i) + ") UMovieSceneEventTriggerSection:" + sectTrg->GetName() + " [" + objectName + "]\n";

            TMovieSceneChannelData data = sectTrg->EventChannel.GetData();
            auto values = data.GetValues();
            auto times = data.GetTimes();
            for (int j = 0; j < values.Num(); ++j)
            {
                auto val = values[j];
                if (val.GetFunctionEntry() == nullptr || !val.IsValidFunction(val.GetFunctionEntry()))
                {
                    errorCount++;
                    result = false;
                    resultText += "ERROR 02. Function is not valid. : " + FString::FromInt(times[j].Value / frameBase) + "\n";
                }
                else {
                    resultText += "Function is valid!! : " + FString::FromInt(times[j].Value / frameBase) + " : " + val.FunctionName.ToString() + "\n";
                }
            }
            continue;
        }

        const UMovieSceneEventRepeaterSection* sectRpt = Cast(section);
        if (sectRpt != nullptr)
        {
            resultText += "(" + FString::FromInt(i) + ") UMovieSceneEventRepeaterSection:" + sectRpt->GetName() + "\n";

            // イベントに関数がバインドされているか確認
            if (sectRpt->Event.GetFunctionEntry() == nullptr || !sectRpt->Event.IsValidFunction(sectRpt->Event.GetFunctionEntry()))
            {
                errorCount++;
                result = false;
                resultText += "ERROR type03. Function is not valid. (EventRepeaterSection)\n";
            }
            continue;
        }
    }
    return result;
}

 

■カメラカットについて
・0期間のカメラカットが存在する
 (没になったカメラカットを消さずに横に避けたりした時に、ひっそり残っていることがありました。)
・カメラがバインディングされていない
 (カットの切り貼りしていると、うっかりこの現象が発生していたようです)

img_seq_004.png

    // カメラトラックを確認
    UMovieSceneTrack* camTrack = movieScene->GetCameraCutTrack();
    if (camTrack != nullptr)
    {
        const TArray sections = camTrack->GetAllSections();
        int errorSection = 0;
        for (int i = 0; i < sections.Num(); ++i)
        {
            UMovieSceneCameraCutSection* camSection = Cast(sections[i]);
            if (camSection != nullptr) {
                FFrameNumber startF = camSection->GetInclusiveStartFrame();
                FFrameNumber endF = camSection->GetExclusiveEndFrame();

                // 0期間のカメラカットセクション
                if (endF <= startF)
                {
                    debug += "ERROR type10. Zero duration camera section : " + Sequence->GetName() + "\n";
                    errorCount++;
                }
                // カメラオブジェクトバインディングエラー
                FMovieSceneObjectBindingID id = camSection->GetCameraBindingID();
                if (!id.IsValid() || !guidList.Contains(id.GetGuid()) )
                {
                    debug += "ERROR type11. Camera Cut Section has Invalid camera binding. : " + Sequence->GetName() + "\n";
                    errorCount++;
                }
            }
        }
    }

 

■クロックソースについて
・クロックソースがTickでない
 (GamePauseを掛けて一時停止したにもかかわらず、一時停止を解除後に一気にタイムラインが進行する事がありました。恐ろしい...)

img_seq_005.png

    // Clock Sourceを確認
    if (movieScene->GetClockSource() != EUpdateClockSource::Tick)
    {
        debug += "ERROR type20. Clock source isn't TICK.\n";
        errorCount++;
    }

 

■DisplayRateについて
・DisplayRateが一致していない
 (部分的に速く進行したり遅く進行したりするサブシーケンスが存在しました。恐ろしい...)

img_seq_006.png

    // FPS確認
    FFrameRate fps = movieScene->GetDisplayRate();
    if (fps.Numerator != 30 || fps.Denominator != 1)
    {
        debug += "ERROR type21. Display Rate isn't 30fps : [" + Sequence->GetName() + " | " + FString::FromInt(fps.Denominator) + "/" + FString::FromInt(fps.Numerator) + "]\n";
        errorCount++;
    }

 

■おまけ:テキストファイルを保存する関数
FFileHelper::SaveStringToFileをBPから呼べるようにラッピングしただけです。

    bool UCommonFunctionLibrary::FileSaveString(FString SaveText, FString FileName, bool appendMode)
    {
        if (!appendMode)
        {
            return FFileHelper::SaveStringToFile(SaveText, *(FPaths::ProjectDir() + FileName));
        }
        else {
            return FFileHelper::SaveStringToFile(SaveText, *(FPaths::ProjectDir() + FileName), FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), FILEWRITE_Append);
        }
    }

 

 

いかがだったでしょうか?
プロジェクト固有のルールもあるはずですので、そういったものもチェックできるようにして、ヒューマンエラーを一気にあぶりだせるようにしておくと、チーム全体が幸せになれますよね。
という事で、みなさんも楽しいUE4開発ライフをお送りください。


ではでは~。