エンジニアっぽくなりたい

UnityやUnrealEngine 4などでなにか役に立ちそうなことを発信していく。

UE4 私がよく使うエディター上での便利な機能(初心者向けだと思う)

この記事の内容は「エディターを使う上での便利な機能」という感じで、最適化やデバッグに便利なものというわけではありません。
長くUEを使っている人にとっては大した内容ではない可能性が高いです。

最近UE4を触り始めた人からふとした質問をされたりして、初めて触る人は意外とこれ知らない人多いかもなぁ~ってのがありました。
なので今回は、その時に聞かれたものや自分がよく使っているちょっと便利?な機能を紹介してみます。





環境

・UE 4.27.2


各BPからノードを検索

ノード名や変数名、文字列などを指定した文字列で検索できます。
使い方は
ウィンドウ->ブループリントを検索->ブループリントを検索1~4を選びます。


「ブループリントを検索」のウィンドウが開きます。


例えば「Tick」で検索した場合こんな感じ


これはめっちゃ便利、使ってください。

ショートカット系

エディタの環境設定の一般->キーボードショートカット でショートカットキー設定が見れて設定もできます。



実行状態をパースペクティブビューで見たい

実行中にアクターを選択して状態を見たり、好きな場所にカメラを移動したりできます。 使い方は
Alt+S
でできます。

操作はパースペクティブビューのときと同じ操作になります。
わりと当たり前の操作ですが、触り始めたばかりの人は知らないこともあるっぽいです。


ブループリントのコンパイル

ブループリントのイベントグラフで「コンパイル」を押すのと同じことをしてくれる感じ。
使い方は
F7キー
押すだけ。

これが地味ーーーーーーに便利なんです。地味にね。やってみてちょうだい。


コンソールコマンド系

実行中に「@」キー押したら画面下部に出てくる入力欄や、アウトプットログの下部にある入力欄に入力することで利用できます。

画面に映ってるウィジェットを検知

今画面に映っているウィジェットや、エディターを構成している部分のクラスを検知することができます。
使い方は
WidgetReflector
入力すると以下のような画面が表示されます。


「ヒットテストできるウィジェットを選択」をクリックすると

以下のような画面になり、カーソルのある場所のウィジェットを検知します。

その状態でESCキーを押すと、検知した対象がハイライトされた状態で固定できます。
クラス名や状態が見れて便利です。



時間経過速度操作

時間の速さを変えられるコマンドです。
使い方は
Slomo [速度倍率の数値]
これによってモーションやエフェクトなどのスローの状態を見たり、逆に速くしてゲームの時間を経過させたりできます。
モーションのこのタイミングで出るエフェクトがうんぬんかんぬん!!とか言われたときに確認するのに便利でした。


フリーカメラ

Alt+Sでもパースペクティブビューで見れますが、コマンドでも似たような機能があります。
使い方は
ToggleDebugCamera

これによってゲーム中にカメラを自由に操作でき、そこにあるメッシュやマテリアルの情報も取得できます。




という感じ。

思ったこと

もっと便利なものはあるでしょうし、紹介したものも実装次第でうまく機能しないものもある、、のかもしれません。
とは言え紹介したものも場合によっては便利に扱えますし、各機能についてさらに情報を集めればより便利な使い方も見つけられるとおもいます。
まずは知らないと始まらないので、とりあえず紹介してみました。なにかもっといいのあれば教えてください!


UE4 エディター処理でプログレスバーを表示する

エディター系処理で結構長い時間がかかる処理をするときに、プログレスバーとか出して、今これくらいだよーって表示したくなることありますよね。
例えばエディターでレベルとか開くときに以下のような表示がされたりします。
f:id:naoxgames:20220411233815p:plain

こういうのを自分で操作するときも使いたい!

ということでやってみました。




目標

エディター処理でプログレスバーを表示してみる。
ついでに「キャンセル」とか、今処理中のファイル名とかも表示してみる。


環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2


調べる

よくわからないので調査する。
とりあえず確定でプログレスバーが表示される操作を探して、その処理を追いかけてみます。

始めに画像を貼ったように「レベルを開く」と絶対表示されます。

いつものように「エディタの環境設定」→「一般」→「未分類」→「UIの拡張点の表示」にチェックを入れて再起動します。
f:id:naoxgames:20220411234748p:plain

ファイルから「レベルを開く」を見ると「OpenLevel」と記述されているので、コードから「OpenLevel」を探します。
f:id:naoxgames:20220411235057p:plain


このあたりの調査は過去に以下の記事で行いました。
naoxgames.hatenablog.jp

この時の調査結果でFileHelpers.cppの

bool FEditorFileUtils::LoadMap(const FString& InFilename, bool LoadAsTemplate, bool bShowProgress)

の処理を通ることがわかっています。

明らかに怪しい「bShowProgress」という引数を追ってると以下の処理に入る様子

GEditor->Exec( NULL, *LoadCommand );


この処理である「UEditorEngine::Exec」の中を、「FEditorFileUtils::LoadMap」の「LoadCommand」を元に追うと、
UEditorEngine::HandleMapCommand
UEditorEngine::Map_Load

int32 bShowProgress = 1;
FParse::Value(Str, TEXT("SHOWPROGRESS="), bShowProgress);
~
FScopedSlowTask SlowTask(100, LocalizedLoadingMap, bShowProgress != 0);
SlowTask.MakeDialog();

SlowTask.EnterProgressFrame(10, FText::Format( NSLOCTEXT("UnrealEd", "LoadingMapStatus_CleaningUp", "{0} (Clearing existing world)"), LocalizedLoadingMap ));

というコードにたどり着きます。

「FScopedSlowTask」がプログレスバーの処理をやってくれているようです。

これを調べてみるといろいろ出てくる。
それらを参考に目標の要件を満たしてみる。


やってみる

今回は以下の記事で対応したレベルを開いて一括変換を行う、という処理に対してプログレスバーを付けてみようと思います。
naoxgames.hatenablog.jp

過去の内容と今回対応する内容を混ぜたものが以下の感じです。
内容は、すべてのレベルを開いて、すべてのActorの「ActorHiddenInGame」をtrueにするというもの。

MyBlueprintFunctionLibrary.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"

#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS(BlueprintType)
class PROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
    
public:
    UFUNCTION(BlueprintCallable)
    static void SetAllHiddenForAllLevels();

    UFUNCTION(BlueprintCallable)
    static void OpenLevelForEditor(const FAssetData AssetData);

    UFUNCTION(BlueprintCallable)
    static void SaveCurrentLevel();
};



MyBlueprintFunctionLibrary.cpp

#include "MyBlueprintFunctionLibrary.h"
#include "FileHelpers.h"
#include "AssetRegistryModule.h"
#include "Misc/ScopedSlowTask.h"
#include "Kismet/GameplayStatics.h"
#include "Editor.h"


void UMyBlueprintFunctionLibrary::SetAllHiddenForAllLevels()
{
    FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
    IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

    TArray<FAssetData> AssetDatas;
    AssetRegistry.GetAssetsByClass(UWorld::StaticClass()->GetFName(), AssetDatas);

    FScopedSlowTask LevelTask(AssetDatas.Num());
    //プログレスバー表示
    LevelTask.MakeDialog(true);

    for (int i = 0; i < AssetDatas.Num(); i++)
    {

        //キャンセル処理
        if (LevelTask.ShouldCancel())
        {
            break;
        }

        //プログレスバー更新
        LevelTask.EnterProgressFrame(1, FText::FromString(AssetDatas[i].GetFullName()));

        OpenLevelForEditor(AssetDatas[i]);

        if (!GEditor)
        {
            continue;
        }

        TArray<AActor*> Actors;
        UGameplayStatics::GetAllActorsOfClass(GEditor->GetEditorWorldContext(false).World(),AActor::StaticClass(), Actors);

        for (AActor* Actor : Actors)
        {
            Actor->SetActorHiddenInGame(true);
        }

        SaveCurrentLevel();
    }
}

void UMyBlueprintFunctionLibrary::OpenLevelForEditor(const FAssetData AssetData)
{
    const FString FileToOpen = FPackageName::LongPackageNameToFilename(AssetData.PackageName.ToString(), FPackageName::GetMapPackageExtension());
    const bool bLoadAsTemplate = false;
    const bool bShowProgress = false;
    FEditorFileUtils::LoadMap(FileToOpen, bLoadAsTemplate, bShowProgress);
}

void UMyBlueprintFunctionLibrary::SaveCurrentLevel()
{
    FEditorFileUtils::SaveCurrentLevel();
}



また、プロジェクトの依存関係に以下を追加します。(.Build.cs)

PublicDependencyModuleNames.AddRange(new string[] { "UnrealEd" });



これの「UMyBlueprintFunctionLibrary::SetAllHiddenForAllLevels()」をEditorUtilityWidgetなどで呼び出しましょう。


実行!!!!



という具合で、すべてのレベルを処理する際にプログレスバーを表示して、一つ毎に表示されている文字列を処理中のレベル名にできました。
コードのコメントにも書いてますが、キャンセル用の処理を書くことで、キャンセルボタン押したときの挙動を作成できます。
今回の対応でもキャンセルは有効です。


思ったこと

まぁググればすぐに出るような内容ですが、エンジンから該当の処理見つけt改めてちゃんとやってみましたっていう感じです。
かなり長い処理には、途中でキャンセルしたりできるのは結構有効だと思われます。



正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 シーケンサーを一時停止する

シーケンサーを使っていて一時停止で困ることがあります。
タップ待ちなどでシーケンサーの進行は一時停止したいが、キャラのモーションは継続させたい。
もしくはシーケンサー関係なくゲーム自体を一時停止させることでシーケンサーもキャラも、ゲーム内すべてを停止させたい。
そんな感じで使い分けるならどんな感じでやるのかなとふと思ったので対応してみる。




目標・要件


シーケンサーの一時停止を2種類対応する。
・キャラのモーションは継続させるがシーケンサーの進行は停止させる。
・キャラのモーションもシーケンサーも、ゲームの進行自体を停止させる。


環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2
・サードパーソンのテンプレートを元にします。


やってみる

作戦としては
一時停止系に関しては調べてみると
シーケンサーを一時停止させる
→「UMovieSceneSequencePlayer::Pause」を利用する。

・ゲームの進行自体を停止させる
→「UGameplayStatics::SetGamePaused」を利用する。

とするのがよさそうです。

シーケンサー一時停止中にモーションを継続させる。
→AnimationTrackを使うと一時停止してしまうので、AnimationBlueprintで処理させる。

という対応を検討してみます。


準備

まずはテスト用のシーケンサーを作成します。
私は以下のような感じで作成しました。

f:id:naoxgames:20220211000724p:plain
内容としては
・サードパーソンのプロジェクトにある「ThirdPersonCharacter」をSpawnableで配置
・「ThirdPersonCharacter」の開始と終わりに適当な座標を設定し、移動させる
・「CameraActor」をLevelに配置し、Possessableで設定(Spawnableでも問題なし)
また、名前は「LS_Pause_Test」としています。

このシーケンサーを再生すると、立ちモーションのまま移動をするだけのものになっているはずです。

作成したらレベルに配置して、「自動プレイ」「ループ」を以下のように設定しておきましょう、確認が楽になります。
f:id:naoxgames:20220211001415p:plain
これを元にこの先の対応を行っていきます。


シーケンサーを一時停止させる

特に凝ったことはせず、あっさり対応します。
イベントトラックで以下のように設定します。
f:id:naoxgames:20220210235621p:plain
f:id:naoxgames:20220210235648p:plain
「LevelSequenceDirector」(イベントトラックの中身設定するところ)で「LevelSequencePlayer」である「Player」を呼び出し、「Pause」を呼ぶだけです。
タイミングは任意に設定してください。

これだけでは一時停止して何もできなくなるだけなので、再開させる処理も対応します。

ここではレベルブループリントに書くことにします。
レベルに配置した「LS_Pause_Test」を選択した状態でレベルブループリントを開き、リファレンスを作成でアクセスできるようにします。
f:id:naoxgames:20220211001709p:plain
そして以下のようにします。
f:id:naoxgames:20220211001921p:plain
この例では「1」キーを押したときにPlayをするようになっており、一時停止時は再開をするようになります。

ここまでの対応で実行をすると、
シーケンサーが途中で止まる
・キャラは立ちモーションを継続している
・「1」キーを押すとシーケンサーが再開する
となります。


ゲームの進行自体を停止させる

次にゲームの進行を停止、つまりキャラのモーションすらも止めてなにも動かない状態にします。
f:id:naoxgames:20220211002312p:plain
これを呼べば終わりです。
ですがこれを呼ぶと、復帰することができなくなるので復帰処理を書きたくなります。
例えば以下のようにします。(レベルブループリントに書いてます)
f:id:naoxgames:20220211003341p:plain
このようにすると、
・「2」キーを押すと一時停止
・「3」キーを押すと再開

と思いきや、このままではうまくいきません。(試してみてください。)

調べてみると、Widgetなどでしかイベントを取得できなくなるようです。
ですが入力処理に設定をすることで呼ばれるようにすることが可能です。

インプットイベントの以下の詳細から「Execute when paused」にチェックを入れます
f:id:naoxgames:20220211003843p:plain
これによって呼ばれるようになりました。(試してみてください。)

例として「2」「3」キーに処理を割り当てましたが、このままにしておきましょう。


ここまでの対応で実行をすると、
シーケンサーが途中で止まる
・キャラは立ちモーションを継続している
・「1」キーを押すとシーケンサーが再開する
・「2」キーを押すとゲームが一時停止して、キャラのモーションも停止する
・「3」キーを押すとゲームが再開する
となります。


アレ?できたんじゃね?


シーケンサー一時停止中に任意のモーションを継続させる

とりあえず要件満たしましたが、現状モーションは立ちモーション一択です。
かといってアニメーショントラックを使ってモーションをさせると、シーケンサー一時停止時にそのタイミングのセクションの位置でモーションが止まってしまいます。
なので任意のモーションに変更できる足がかりを考えてみます

そもそもシーケンサー一時停止を使うときは、おそらくタップ待ちなどの処理になると思われます。
となると、基本的にはループモーション的なものが設定されるのでしょう!という前提で考えます。

作戦としては、
・AnimationBlueprintに変数を追加
・その値に応じてモーションを変化させる
・その値はシーケンサーから設定できるようにする
という感じで行こうと思います。

まずはAnimationBlueprintについて、以下のようにしてみます。
f:id:naoxgames:20220211005526p:plain
こちらはサードパーソンのプロジェクトにある「ThirdPerson_AnimBP」を編集しています。
「IsOverride」というboolの変数を追加する必要があります。
「IsOverride」は以下のように「シネマティクスに公開」にチェックを入れましょう、シーケンサーでいじれるようになります。
f:id:naoxgames:20220211005922p:plain
C++では以下のようにします。

UPROPERTY(Interp, BlueprintReadWrite)
    bool IsOverride;


この処理で、「IsOverride」が「True」だったら「ThirdPersonRun」モーションが流れる ということになります。
(「ThirdPersonRun」はサードパーソンのプロジェクトにあるマネキンの走るモーション)


次にシーケンサーでAnimationBlueprintの値を設定します。

以下のように対応します。
・キャラのSkeletalMeshComponentを設定します。(ここでは「CharacterMesh0」) f:id:naoxgames:20220211010313p:plain

・SkeletalMeshComponentからアニメーションインスタンスを設定します。(ここでは「ThirdPersonAnimBP」)
f:id:naoxgames:20220211010501p:plain

・先ほど追加した「ThirdPersonAnimBP」の「IsOverride」の変数を探し、設定します。
f:id:naoxgames:20220211010750p:plain
以下のように「IsOverride」を「False」にしたり「True」にしたりする
f:id:naoxgames:20220211011223p:plain


これによりモーションは
シーケンサーの特定タイミングで走りモーションになったり、立ちモーションになったりする
シーケンサーから指定できる
シーケンサー一時停止の影響を受けない
ということになります。

※ここではboolでやりましたが、使い勝手悪すぎるので、実際はEnumとかにしてC++側で判定したモーションをAnimBPに渡して流すとか、そういう感じにするのが良いと思います。


思ったこと

物自体はわりと簡単でしたね。
一時停止したーーいって言われて、一時停止だけならすぐだけと、モーションとかAnimationBlueprintとかも絡んでくるからさほどあっさりではないよねっていう。
今回調べるまで「Execute when paused」知らなかったし、わりと普通に一時停止系やると困ることがちょこちょこありそうだなと思いました。




正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 各Level内のBP設定値を一括で修正

例えばとあるアクターのメンバ変数名を変更したくなったとします。
ですがC++で変更するとその変数に設定されていた値はリセットされてデフォルトの値になります。
思いつく対応策は一時的に変更前と変更後の変数を共存させて、変更前の値を変更後の変数に代入する、そして変更前の変数を削除 みたいなことをすればできそうです。
ですが、レベルに直接配置され、各レベルで違った設定をしていた場合、どうやってすべてを適切に変更後の変数に設定できるのでしょう。
すべてのレベルを開いて設定しなおす!が通ればそれがいいかもしれませんが、実際はそうもいきません。
今回はそんな時にどうしたらいいのか考えてみました。




目標・要件

各レベルに配置されたBPの設定値を変更する。
この変更処理はエディターから行う。


環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2


やってみる

作戦としては、
・何かしらの手段で、エディターから任意のレベルを開く
・エディターからレベルに配置されているアクターを取得して変更を加える
・レベルを保存する
・レベルの数分繰り返す
という感じです。

ですがすべてはエディター上という縛りがあり、通常思いつく手段が使えない可能性があります。
頑張ります。

今回の検証は以下の内容を行います。
・複数あるレベルに配置された「AActor」の「ActorHiddenInGame」を「True」にする。
これができたら要件を満たしたとします。
また、対応は「エディターユーティリティウィジェット」で行おうと思います。


エディターユーティリティウィジェットを作成

今回はエディターからの処理を簡単に行いたいのでエディターユーティリティウィジェットを使います。
コンテンツブラウザを右クリックし、「エディターユーティリティウィジェット」を選択します。
f:id:naoxgames:20220214032858p:plain
こんな感じ
f:id:naoxgames:20220214033025p:plain

エディターユーティリティウィジェットを開き、ボタンを配置します。
f:id:naoxgames:20220214033106p:plain

ボタンを選択し、詳細から「イベント」の「onClicked」のところにある「+」をクリックし、クリックイベントを作成します。
f:id:naoxgames:20220214033228p:plain

イベントグラフに以下のようなイベントが作成されます。
f:id:naoxgames:20220214033315p:plain

コンテンツブラウザから作成したエディターユーティリティウィジェットを右クリックし、「エディターユーティリティウィジェット」をクリックします。
f:id:naoxgames:20220214033359p:plain
すると以下のように先ほど設定したボタンが表示されます。
f:id:naoxgames:20220214033445p:plain

このボタンを押すことで、先ほどのボタンのイベントが呼ばれるようになります。


何かしらの手段で、エディターから任意のレベルを開く

とりあえず、パッと思いつくレベル開く系の処理は使えませんでした!!!!
OpenLevel的なやつです。

他の道がないか探したところ、エディターのメニューの「ファイル」に「レベルを開く」があることに気づく。
f:id:naoxgames:20220214033637p:plain
この処理をパクろう!!!!

ということで、エンジンからこの処理を探します。

まずはUIの拡張点を表示します。
「編集」→「エディタの環境設定」→「拡張点」で検索→「UIの拡張点の表示」にチェックを入れます。
f:id:naoxgames:20220214033745p:plain
エンジンの再起動をしてください。

再度「レベルを開く」を確認すると、緑色の文字で「OpenLevel」と表示されているのがわかります。
f:id:naoxgames:20220214033955p:plain
※私は始めから設定していたので前のスクショと見た目変わってません。

このキーワードをエンジンコードから探します。
「"Open Level"」と検索したところ以下がヒットしました。
FileHelpers.cpp

OpenAssetDialogConfig.DialogTitleOverride = LOCTEXT("OpenLevelDialogTitle", "Open Level");


この処理付近を調査すると、この後のダイアログを開く処理の引数に、レベル選択時のコールバックを設定しています。

ContentBrowserModule.Get().CreateOpenAssetDialog(OpenAssetDialogConfig,
                                                     FOnAssetsChosenForOpen::CreateStatic(&FLocal::OnLevelsSelected, OnLevelsChosen),
                                                     FOnAssetDialogCancelled::CreateStatic(&FLocal::OnDialogCancelled, OnLevelPickingCancelled));

この「OnLevelsChosen」はこの処理が書かれている関数の引数に入ってきたものなので、この処理あたりにブレイクを張って実際に「レベルを開く」を実行してみます。

呼び出し履歴から追うと、
FEditorFileUtils::LoadMap()で行っていることがわかります。

さらに追っていくと
FEditorFileUtils::LoadMap(引数あり版)で処理している様子。

この処理を同じように使えば自分でもエディター上でレベル開けるのでは?ということで試してみます。

以下のようなクラスを作成します。(今回はBlueprintFunctionLibraryで行います。)

MyBlueprintFunctionLibrary.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"

#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS(BlueprintType)
class PROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
    
public:
    UFUNCTION(BlueprintCallable)
    static void OpenLevelForEditor(const FAssetData AssetData);
};



MyBlueprintFunctionLibrary.cpp

#include "MyBlueprintFunctionLibrary.h"
#include "FileHelpers.h"

void UMyBlueprintFunctionLibrary::OpenLevelForEditor(const FAssetData AssetData)
{
    const FString FileToOpen = FPackageName::LongPackageNameToFilename(AssetData.PackageName.ToString(), FPackageName::GetMapPackageExtension());
    const bool bLoadAsTemplate = false;
    const bool bShowProgress = true;
    FEditorFileUtils::LoadMap(FileToOpen, bLoadAsTemplate, bShowProgress);
}



また、プロジェクトの依存関係に以下を追加します。(.Build.cs)

PublicDependencyModuleNames.AddRange(new string[] { "UnrealEd" });


これでエディター上でレベルを開く準備ができました。
エディターユーティリティウィジェットのイベントで以下のように処理を作成します。
f:id:naoxgames:20220214035325p:plain
内容としては、AssetRegistryを利用してレベルのアセット(Worldというクラスらしい)を取得、それらを上記で作成した処理を使いレベルを開きます。
PrintStringは画面に文字は表示されませんが、アウトプットログで確認できるので使用しています。

これによってエディターでレベルを開けるようになりました。


エディターからレベルに配置されているアクターを取得して変更を加える

GetAllActorsOfClassを使いたい!!!
そうするとエディター上でワールドを取得する処理が必要になります。
いろいろ調べてみると、取得する方法がありそうです。
「編集」→「プラグイン」から以下のプラグインを有効にします。
f:id:naoxgames:20220214040050p:plain
f:id:naoxgames:20220214035951p:plain

すると「GetEditorWorld」が使えるようになります。
f:id:naoxgames:20220214040113p:plain

先ほどエディターユーティリティウィジェットで作成したレベルを開く処理の続きで以下のように処理を追加します。
f:id:naoxgames:20220214040511p:plain

これにより、各レベルを開き、Actorを取得し、変更を加えることができました。


レベルを保存する

先ほどのレベルを開く方法があったFileHelpers.cppで「Save」とかで検索すると「SaveCurrentLevel」というそれっぽい関数を見つけました。
実際にメニューの「ファイル」→「現在のレベルを保存」を選択するとこの処理を通ることもわかります。

なので、これを使おう!
ということで、先ほどの処理に追加します。
f:id:naoxgames:20220214041133p:plain
そのままノードでいけるので、これでOKです。


実行!!!!

実行!!!!
すると、各レベルを開き、アクターを取得して編集し、すべてのアクター編集後にレベルを保存、という流れができました。
動画やスクショで示すものがありませんが、お試しいただけるとわかります。

今回は「ActorHiddenInGame」というパラメーターを対象にしましたが、これによってとある値を判定して、それに応じたパラメーターを別の変数に与える(変換する)ようなことにも使えます。
また、今回はすべてのレベルを開いてますが、名前やフォルダとかである程度判別できるならそうしたほうがよいかと思います。なんだかんだ全レベル開くのはプロジェクトによっては大変かと。。。
あとは保存処理、今回のは変更対象のアクターがいなくても保存する感じになってるので、不要ならスキップしましょう。
という感じかなと。


思ったこと

これがあればレベルの中を特定の条件で一括変更できると思われるので、非常に有用かと。
今回は「一括変更」をメインにしましたが、チェック機能としても利用できるかと思われます。
とはいえ対象のレベルをすべて開くので、場合によってはめちゃくちゃ重いと思われるので、実際実用的なのかはその場合次第・・・・?



正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 AnimBPを使いまわす方法(2)~Animation Layerを使う~

以前に書いた「AnimBPを使いまわす方法」を見た友人から「Animation Layerを使うことで各子AnimBP毎のアニムグラフが設定できるよ!」という話をいただいたので、試してみようと思います。

前回の記事は以下
naoxgames.hatenablog.jp
これの続きで今回は対応していきます。




目標・要件


AnimBP継承先毎に固有のアニムグラフを設定できるようにする。

環境

・UE 4.26.1 (エンジンビルドはしていません)
・VisualStudio 2019 16.11.2


調べる

よくわからないので調べます。
Animation Blueprint Linking を使用する | Unreal Engine ドキュメント
これかしら?
「実験的機能」と表示されていてドキドキが隠せない。


やってみる

作戦としては、
継承元のAnimBPの変数にリンクする「AnimInstance」のクラスを指定できるようにして、継承先で必要な「AnimInstance」クラスを設定できるようにする。
あとはおおよそドキュメントに沿った対応をしてみる。
今回は簡単に、それぞれの固有のAnimBPの中でそれぞれのモーションをさせて、それをメインのAnimBPに反映させるようにしてみます。
なので、
・AnimationLayerには各キャラのモーションをさせる処理をする
・メインのAnimBPにはAnimationLayer+視線制御をさせる
となり、
AnimationLayerで設定したモーションをしつつ視線制御ができていたら成功!ということで。


Animation Layer Interfaceを作る

ドキュメントに倣って作ってみます。
コンテンツブラウザから右クリックをし、「アニメーション」→「アニメーションレイヤーインターフェーズ」を選択します。
f:id:naoxgames:20220211025337p:plain
ここでは「MyAnimLayerInterface」とします。

作成された「MyAnimLayerInterface」を開き、アニメーションレイヤー名を変更します。
ここでは「UniqueMotion」とします。
また、「入力」の「+」をクリックしてポーズの入力を追加します。
f:id:naoxgames:20220211025711p:plain


Animation Layer用AnimBPを作る

各キャラ用にAnimLayerのアニムグラフを書くAnimBPを作成します。
コンテンツブラウザから右クリックで「アニメーション」→「アニメーションBP」を選択します。
f:id:naoxgames:20220211030426p:plain
以下のような画面になるので、今回作ろうとしてるキャラのスケルトンを選択してください。
f:id:naoxgames:20220211030620p:plain
ここでは「ABP_UniqueMannequin」とします。

作成したAnimBPを開き、「クラス設定」を選択して詳細から「実装インターフェース」に「MyAnimLayerInterface」を追加します。
f:id:naoxgames:20220211030944p:plain

すると「アニメーションレイヤー」の項目に「UniqueMotion」が増えています。
f:id:naoxgames:20220211031022p:plain

UniqueMotionを開き、アニムグラフが開かれるのでそこでモーションをアウトプットへ繋げましょう。
f:id:naoxgames:20220211031154p:plain
私は適当にジャンプモーション設定しました。


このAnimBPを作成する手順をキャラ数分繰り返します。
今回作成したAnimBPは
「ABP_UniqueAnimal01」(鹿用)
「ABP_UniqueAnimal02」(豚用)
とします。


メインのAnimBPに設定する

メインのAnimBPで使えるようにします。
まずキャラのAnimBPの基底となっているAnimBPを開きます。ここでは「AnimBP_Base」と名付けています。
「クラス設定」から詳細で「実装インターフェース」に「MyAnimLayerInterface」を追加します。
f:id:naoxgames:20220211033524p:plain

先ほどと同じくアニメーションレイヤーに項目が追加されます。
f:id:naoxgames:20220211033722p:plain

アニムグラフで右クリック→「リンクされたアニメーションレイヤー」を選択
f:id:naoxgames:20220211033825p:plain

以下のようなノードができるので、詳細から「Layer」を「UniqueMotion」にします。
f:id:naoxgames:20220211033935p:plain

これによって、リンクされたAnimBPの「UniqueMotion」レイヤーを使えるようになります。

次にリンクするAnimInstanceのクラス指定をメインのAnimBPに追加しておきます。
以下のような変数をAnimBPに追加してください。
型はAnimInstanceのクラス参照型です。
f:id:naoxgames:20220211034239p:plain
ひとまずデフォルト値は「ABP_UniqueMannequin」にしておきます。


次に各キャラのメインのAnimBPを開き、「クラスのデフォルト」を選択して詳細から上の手順で追加した「LinkAnimInstClass」を設定します。
f:id:naoxgames:20220211034536p:plain
各キャラ用のAnimLayer用AnimBPを指定しましょう。


最後に各キャラのBPで以下のような処理を行います。
f:id:naoxgames:20220211035507p:plain
主に「AnimLayer用」の部分の処理になります。
SkeletalMeshComponentに先ほどのAnimBPで設定した「AnimLayer用AnimBP」のクラス参照を渡してリンクさせます。


実行!!!!!

以下のようになりました!


基底AnimBPで行っている共通処理を全キャラに反映しつつ、各キャラ毎の処理をAnimLayerのアニムグラフを使って行った感じです。
便利!!!


思ったこと

Mさん!これであってますか!?
実験的機能って書いてあったけど大丈夫なんですか!?
ひとまず要件はクリアして、動作も確認できてるのでよさそうだとは思います。




正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4シーケンサーのメニューに項目を追加したい(4)~Level Sequence Asset の Context Menu~

シーケンサーとはちょっと離れるかもしれませんが、今回はLevel Sequence のAssetの右クリックメニューに項目を追加しようとおもいます。

目標


Level Sequence のAssetの右クリックメニューに項目を追加する。
具体的には
f:id:naoxgames:20211231214015p:plain
このあたりに追加する
※エンジン改造はしません。

前回の続きで対応するため、以下もご一読ください。
naoxgames.hatenablog.jp
naoxgames.hatenablog.jp
naoxgames.hatenablog.jp
一連の対応を上げているGitリポジトリです。
この記事のコードはすべてこのリポジトリに追加で進めています。
github.com

環境

・UE 4.26.1 (エンジンビルドはしていません)
・VisualStudio 2019 16.11.2

やってみる

いつも通りエディターの拡張点などからエンジンの類似処理を見つけて真似てみる作戦で行きます。

エンジンから類似処理を探す

LevelSequenceのアセットをコンテンツブラウザから右クリックしてみると以下のようになります。
f:id:naoxgames:20211231221040p:plain
「GetAssetActions」がなにかできそうなので、これを調べてみます。
その結果「FTakeRecorderModule::RegisterMenus()」と「FSequencerModule::RegisterMenus()」を見ると参考になりそうな処理が書いてありました。
これを元に対応していきます。

拡張メニュー追加

拡張メニュー追加の処理を実装していきます。
以下はアセットを右クリック時に「LevelSequenceContextMenuExtension」という項目を表示し、選択するとダイアログが出るようにしています。

SequencerExtensionEd.Build.cs

~
        PublicDependencyModuleNames.AddRange(
            new string[]
            {
                ~
                "ContentBrowser",
                ~
            }
            );
~


SequenceExtensionEd.cpp

~
//AssetContextMenu
#include "ContentBrowserMenuContexts.h"
~
void FSequencerExtensionEdModule::RegisterMenus()
{
~
    {
        //右クリックメニュー
        FToolMenuOwnerScoped ContextMenuOwner("LevelSequenceContextMenuExtension");
        UToolMenus* ToolMenus = UToolMenus::Get();
        UToolMenu* AssetContextMenu = ToolMenus->ExtendMenu("ContentBrowser.AssetContextMenu.LevelSequence");
        if (!AssetContextMenu)
        {
            return;
        }

        FToolMenuSection& Section = AssetContextMenu->FindOrAddSection("GetAssetActions");
        Section.AddDynamicEntry("LevelSequenceContextMenuExtension", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
            {
                UContentBrowserAssetContextMenuContext* Context = InSection.FindContext<UContentBrowserAssetContextMenuContext>();
                if (!Context)
                {
                    return;
                }
                //一つのアセットを選択中のみ有効
                ULevelSequence* LevelSequence = Context->SelectedObjects.Num() == 1 ? Cast<ULevelSequence>(Context->SelectedObjects[0]) : nullptr;
                if (LevelSequence)
                {
                    InSection.AddMenuEntry(
                        "LevelSequenceContextMenuExtension",
                        FText::FromString(TEXT("LevelSequenceContextMenuExtension")),
                        FText::FromString(TEXT("LevelSequenceContextMenuExtension")),
                        FSlateIcon(),
                        FExecuteAction::CreateLambda(
                            [this, LevelSequence]
                            {
                                FText DialogText = FText::FromString("LevelSequenceContextMenuExtension");
                                FMessageDialog::Open(EAppMsgType::Ok, DialogText);
                            }));
                }
            }));
    }
~
}
~


これで準備は完了です。

実行!!!!

f:id:naoxgames:20211231222810p:plain
f:id:naoxgames:20211231222826p:plain
という感じでLevelSequenceのアセットの右クリックメニューに項目を追加することができました。

FAssetTypeActions_Baseを使った実装について

アセットのメニューについて調べてみると、どうやら「FAssetTypeActions_Base」というクラスを継承して、「GetSupportedClass」で指定したクラスのアセットのメニューが設定できるようです。
ですが今回はこの方法を使いませんでした。
理由としては、過去に「FAssetTypeActions_Base」を使った実装を「Skeleton」のアセットに対して行ったことがあるのですが、その際「Skeleton」に対するAssetTypeActionsクラスがすでにエンジン側に存在しており、こちらが作ったものが優先されてしまう状況になりました。
つまり元の機能が使えなくなってしまったのです。
何とかする方法があるのかもしれませんが、結局解決できておりません。

その時に今回の方法を使った場合、問題なく対応ができます。
※Skeletonの場合は「ExtendMenu("ContentBrowser.AssetContextMenu.Skeleton")」と指定する。
そのため、今回は「FAssetTypeActions_Base」を使わない実装を行いました。


思ったこと

この機能を使えば、「LevelSequence」に対する特定の処理を選択したアセットにのみ行うことができて、多分便利!
基本的になにか処理を加えたいときはすべての「LevelSequence」一括で行う気がするので微妙かもしれませんが。。
シーケンサーを何かの形式でエクスポートする処理とか、逆にインポートするときとか、そんなことをするときにかつて仕事で使ったことがあります。


ここまでの内容は以下のリポジトリに上げてあります。 github.com

正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4シーケンサーのメニューに項目を追加したい(3)~Object Binding Context Menu~

シーケンサー上の動的置き換えに行く前にObjectBindingのメニューに項目を追加していじれる対応をします。

目標


シーケンサーのエディター拡張を行い、ObjectBindingのメニューに項目を追加したい。
今回はSpawnable設定のObjectBindingに対して、「FMovieSceneSpawnable」構造体の詳細を表示するのを目標とします。
また、対応は引き続きPluginで行います。
メニューを追加するのはこの辺
f:id:naoxgames:20211130224247p:plain
※エンジン改造はしません。

前回の続きで対応するため、以下もご一読ください。
naoxgames.hatenablog.jp
naoxgames.hatenablog.jp
一連の対応を上げているGitリポジトリです。
この記事のコードはすべてこのリポジトリに追加で進めています。
github.com

環境

・UE 4.26.1 (エンジンビルドはしていません)
・VisualStudio 2019 16.11.2

やってみる

作戦としては基本的にはいつも通り、拡張点からエンジンソースを検索して似たような処理を行います。

UIの拡張点の表示

まずはエディター拡張する部分の拡張点を知る必要があります。
UE4エディターの[編集]→[エディタの環境設定]をクリックし、以下の画像のように[UIの拡張点の表示]にチェックを入れます。

f:id:naoxgames:20210214184933p:plain
UIの拡張点の表示


その後エディターを再起動します。
すると、エディターの各所に緑色の文字が表示されるようになっていると思います。
今回対応したいObjectBindingのメニューでは以下のように表示されています。
f:id:naoxgames:20211130225242p:plain

「Spawnable」で探してみます。


エンジンから類似処理を探す

「Spawnable」でエンジンのコードを検索すると、「SequencerObjectBindingNode.cpp」の「FSequencerObjectBindingNode::BuildContextMenu」で使われていることがわかります。
ここの処理にある「SequencerModule.GetObjectBindingContextMenuExtensibilityManager()」から「Extenders」を取得して「MenuBuilder」に登録しています。
ということで、ここに登録すればよさそうな雰囲気です。


シーケンサーモジュールに拡張メニュー追加

以下のように対応します。
まずは登録するメニューの中身の処理を作ります。

SequenceExtensionEd.h

~
private:
~
    void AddObjectBindingContextMenuExtention(FMenuBuilder& MenuBuilder);
    void AddSpawnablePropertyMenu(FMenuBuilder& MenuBuilder, UMovieScene* MovieScene, ISequencer* Sequencer);
~
private:
~
    FGuid SelectedObjectBinding;
~

SequenceExtensionEd.cpp

~

//構造体の詳細表示
#include "IStructureDetailsView.h"

~

void FSequencerExtensionEdModule::AddObjectBindingContextMenuExtention(FMenuBuilder& MenuBuilder)
{
    ISequencer* OpenSequencer = GetCurrentSequencer();
    if (OpenSequencer == nullptr)
    {
        return;
    }
    SelectedObjectBinding = FGuid();
    TArray<FGuid> guids;
    OpenSequencer->GetSelectedObjects(guids);
    if (guids.Num() == 1)
    {
        SelectedObjectBinding = guids[0];
    }

    if (!SelectedObjectBinding.IsValid())
    {
        return;
    }

    UMovieScene* MovieScene = OpenSequencer->GetFocusedMovieSceneSequence()->GetMovieScene();
    FMovieSceneSpawnable* Spawnable = MovieScene->FindSpawnable(SelectedObjectBinding);
    if (!Spawnable)
    {
        return;
    }
    MenuBuilder.BeginSection("OverrideObjectBinding", FText::FromString("OverrideObjectBinding"));

    AddSpawnablePropertyMenu(MenuBuilder, MovieScene, OpenSequencer);

    MenuBuilder.EndSection();
}


void FSequencerExtensionEdModule::AddSpawnablePropertyMenu(FMenuBuilder& MenuBuilder, UMovieScene* MovieScene, ISequencer* Sequencer)
{

    FMovieSceneSpawnable* Spawnable = MovieScene->FindSpawnable(SelectedObjectBinding);

    auto PopulateSubMenu = [this, Spawnable](FMenuBuilder& SubMenuBuilder)
    {
        FPropertyEditorModule& PropertyEditor = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

        FDetailsViewArgs DetailsViewArgs(false, false, false, FDetailsViewArgs::HideNameArea, true);
        DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Automatic;
        DetailsViewArgs.bShowOptions = false;
        DetailsViewArgs.ColumnWidth = 0.55f;

        FStructOnScope* StructOnScope = new FStructOnScope(FMovieSceneSpawnable::StaticStruct(), (uint8*)Spawnable);
        TSharedRef<IStructureDetailsView> StructureDetailsView = PropertyEditor.CreateStructureDetailView(
            DetailsViewArgs,
            FStructureDetailsViewArgs(),
            MakeShareable(StructOnScope)
        );
        
        TSharedRef< SWidget > DetailsViewWidget = StructureDetailsView->GetWidget().ToSharedRef();

        SubMenuBuilder.AddWidget(DetailsViewWidget, FText(), true, false);
    };

    MenuBuilder.AddSubMenu(FText::FromString("SpawnableProperty"), FText(), FNewMenuDelegate::CreateLambda(PopulateSubMenu));
}

~


「IStructureDetailsView」を使って構造体の詳細表示をしています。
エンジンソースを検索すると使っている箇所がいくつかあるので参考にしてみました。(わりと丸コピな部分があります。)
※セクション名が「OverrideObjectBinding」になっているのは次にやろうとしてることに合わせた名前にしてしまいました、気にしないでください。

上記で選択したObjectBindingの「FMovieSceneSpawnable」の詳細を表示する処理ができました。
次は登録を行います。

SequenceExtensionEd.h

~
private:
~
    TSharedPtr<FExtender> ObjectBindingContextMenuExtender;
~

SequenceExtensionEd.cpp

~
void FSequencerExtensionEdModule::StartupModule()
{
~
    //ObjectBindingContext
    ObjectBindingContextMenuExtender = MakeShareable(new FExtender);
    ObjectBindingContextMenuExtender->AddMenuExtension(
        "Spawnable",
        EExtensionHook::After,
        PluginCommands,
        FNewMenuDelegate::CreateRaw(this, &FSequencerExtensionEdModule::AddObjectBindingContextMenuExtention)
    );
    SequencerModule.GetObjectBindingContextMenuExtensibilityManager()->AddExtender(ObjectBindingContextMenuExtender);
}

void FSequencerExtensionEdModule::ShutdownModule()
{
~
    SequencerModule.GetObjectBindingContextMenuExtensibilityManager()->RemoveExtender(ObjectBindingContextMenuExtender);
~
}


これで準備は完了です。

実行!!!!


f:id:naoxgames:20211130231751p:plain
ObjectBindingのメニューに追加されました。
ちゃんと「FMovieSceneSpawnable」の内容も表示されていて編集もできます。


思ったこと

これがあればエンジン改造で「FMovieSceneSpawnable」になにか追加してそれをいじったりすることも、その内容をランタイムの何かの手がかりに使うこともできて便利な妄想しかできない。


ここまでの内容は以下のリポジトリに上げてあります。 github.com

正しい対応方法やより良い方法があればご指摘お願いいたします!!!