CC2の楽屋裏

ゲーム制作会社サイバーコネクトツー公式ブログ

*

第003回 UE4のショートカットキーの仕組み

      2019/04/01

今回は ツール系テクニカルアーティスト しばはらがお送りします。

突然ですが、PhotoshopやMayaなどでの仕事が速い人ってどんなイメージがありますか?テクニックは色々あると思いますが、私の周りでは「ショートカットキーを使いこなしている」という話をよく聞きます。

というわけで、UE4の仕事を高速化すべく、ショートカットキーの設定について調べてみました。

ショートカットキーの設定

UE4でショートカットキーの設定を確認・変更するときは、UE4の[メニューバー] →[Edit]→[Editor Preferences]を開き

tb003_01

Keyboard Shortcuts の項目を開きます。

tb003_02

ここに並んでいるのがショートカットキーを割り当て可能なコマンドの一覧です。こうして見てみると、実はかなり多くのマンドに割り当てができることが分かります。もし普段使っているエディターの操作が見つかれば、作業が高速化できるかもしれませんね。

ただ、ここでちょっと気をつけておかなければいけないポイントがあります。

ショートカットキーの設定の注意点 その1: 隠されているコマンド

ショートカットキーのアサインは対象エディターを起動した後でないとできないものがあります。

例えば、UE4起動直後の設定画面はこんな感じです。※全グループを閉じて表示しています

tb003_03これが、一度テクスチャーエディターを開いた後で見に行くと

tb003_04

なんか増えたぞ…!

設定画面に新しく表示されているものがあります。隠しコマンドを見つけたような気分ですね。

このような挙動になる理由について、記事の後半でプログラム的な説明をしています。とりあえずはそういうものなのだ、とおぼえておいてください。

ショートカットキーの設定の注意点 その2:アサインの重複

UE4のショートカットキーの実行は、原則フォーカスしているウィンドウに対して行われます。そのため、異なるエディター上のコマンドに対しては、同じキーをそれぞれアサインできるようになっています。

たとえば、ブループリントエディターがアクティブなときにCtrl+S(保存)を押せば開いているブループリントアセットが保存されますが、もしUE4起動時のメインウィンドウ(レベルエディター)がアクティブであれば、このキー入力は開かれているレベルの保存コマンドとして処理されます。

レベルエディターと他のエディターを行き来しながら作業する状況では、フォーカスするウィンドウを間違えてショートカットキーを入力してしまうことも起きてきます。このとき、各エディターに同じキーをアサインしたコマンドがそれぞれあると、意図しない処理が実行されてしまうことになるわけです。

シーケンサーの作業は色々なウィンドウを行き来する代表例ではないかと思います。

tb003_05

同時に使うことがあるエディター間では、キーアサインの重複に注意しましょう。

その他の注意点として、自分好みにカスタマイズしたUE4に慣れていると、いざ他の人の席でいつもの操作をやってみせようとするともたつく、という弊害があります。これはUE4に限らずある話ですね(笑)

 自作のコマンドをショートカットキーに対応させる

ここからはC++で自分のコマンドを作り、それをUE4上からショートカットキーを使って呼び出す方法を説明していきます。

※この項は、UE4 バージョン 4.19.2, Win64 環境下での動作をもとに書かれています。

コマンドの登録は、
・UE4にコマンドを登録する
・登録したコマンドとコマンド実行時の処理を紐付ける
という2つのステップで実現します。

したがって、以下のような順番で説明を進めていきます。

(1) コマンド登録を行うクラスをつくる
(2) コマンドの処理本体と、コマンドと処理の紐付け処理を持つクラスをつくる
(3) UE4の起動フローでつくった処理を呼び出す
(4) 依存モジュールへの参照を追加する
(5) 動作確認

 (1)コマンド登録を行うクラス

このクラスは、UE4が提供している TCommandsというテンプレートクラスを継承して作ります。

[MyEditorCommands.h]
#pragma once

#if WITH_EDITOR

#include "Commands.h"
#include "EditorStyleSet.h"

class FMyEditorCommands : public TCommands<FMyEditorCommands>
{
public:
	FMyEditorCommands()
		: TCommands<FMyEditorCommands>(TEXT("MyEditor"), NSLOCTEXT("Contexts", "MyEditor", "MyEditor Command"), NAME_None, FEditorStyle::GetStyleSetName())
	{			
	}

	virtual void RegisterCommands() override;

	// コマンド1つを表す
	TSharedPtr<class FUICommandInfo> MyEditorCommand;
};

#endif

ヘッダーのポイントを見ていきましょう。

8行目、クラス定義です。継承元のTCommandsのテンプレート引数には、クラス自身の名前を与えます。

16行目、コマンドの登録を行う関数 RegisterCommands をオーバーライドします。

18行目 用意するコマンド毎に FUICommandInfo のポインタ保持するメンバ変数を用意します。

[MyEditorCommands.cpp]
#if WITH_EDITOR

#include "MyProjectEditor.h"
#include "MyEditorCommands.h"

// UI_COMMAND内部でテキストのローカライズに対応する記述があるので、
// 必要になっている記述
#define LOCTEXT_NAMESPACE "MyEditorCommand"
 
void FMyEditorCommands::RegisterCommands()
{
	// UE4にコマンドを登録するマクロ。この呼出時点で、ショートカットキーのウィンドウに表示されるようになる
	UI_COMMAND(MyEditorCommand
		, "Call MyEditor Command", "Desctription : call my editor command",
		EUserInterfaceActionType::None, FInputChord(EKeys::F12, EModifierKey::Control));
}

#undef LOCTEXT_NAMESPACE

#endif //WITH_EDITOR

 

cpp側は RegisterCommands の実装が書いてあるだけです。
12行目、UI_COMMANDというマクロで、コマンドを登録します。ヘッダーに用意したコマンド変数、コマンド名、コマンドの解説文、コマンドに紐付けたUIの種類、初期のショートカットキーという順で指定しています。

「コマンドに紐付けたUIの種類」という項目は、ボタンを押したときにコマンドを呼び出す、など、UIとコマンドの動作を連動させるときに関係する項目です。今回のコマンドはUIに紐付かないので EUserInterfaceActionType::None を指定します。

最後のショートカットキーの設定は、FInputChord という構造体で渡します。コンストラクタの引数として、メインとなるキーと、オプションキー(Ctrl, Shift, Altなど、同時押しするキー)を指定します。

例:”Ctrl + Shift + F12″ というキー指定
FInputChord(EKeys::F12, EModifierKey::Control | EModifierKey::Shift)

(2) コマンドの処理本体と、コマンドと処理の紐付け処理を持つクラスをつくる

[MyEditorCommandBinder.h]
#pragma once

#if WITH_EDITOR

#include "Commands.h"
#include "EditorStyleSet.h"

class FUICommandList;
class SWindow;

class FMyEditorCommandBinder
{
public:
	FMyEditorCommandBinder() : IsBinded(false)
	{}

	// イベントからコマンドの処理の紐付けを呼び出す
	void OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr);

	void SetBindHandle(FDelegateHandle& InHandle) { LoadedDelegateHandle = InHandle; }

private:
	// コマンドの処理の紐付け
	void BindCommands();

	bool IsBinded;
	// イベントの登録抹消のためハンドラを保持
	FDelegateHandle LoadedDelegateHandle;
	
};

#endif

こちらのクラスには親クラスはありません。

23行目のBindCommandsが、コマンドと処理の紐付けをおこなう関数です。

今回のサンプルでは、
UE4のエディターのロードが終わったタイミングで17行目のOnEditorLoaded を経由して、BindCommandsの呼び出しを行います。

コマンドに紐付ける処理は、関数として定義することもできますが、今回のサンプルではラムダ式で与えていますので、プロトタイプ宣言はありません。

[MyEditorCommandBinder.cpp]
#if WITH_EDITOR

#include "MyProjectEditor.h"

#include "MyEditorCommands.h"
#include "MyEditorCommandBinder.h"

#include "Editor.h"
#include "Kismet2/DebuggerCommands.h"

#define LOCTEXT_NAMESPACE "MyEditorCommand"
 
void FMyEditorCommandBinder::OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr)
{
	// クラッシュ回避
	if (GEditor == nullptr)
	{
		return;
	}

	// FSlateRendererのイベントへの登録を抹消
	if (IsInGameThread())
	{
		FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer();
		SlateRenderer->OnSlateWindowRendered().Remove(LoadedDelegateHandle);
	}

	// 処理の登録は一回のみ
	if (IsBinded)
	{
		return;
	}
	IsBinded = true;

	BindCommands();
}


// コマンドの処理の紐付け
void FMyEditorCommandBinder::BindCommands()
{
	const FMyEditorCommands& Commands = FMyEditorCommands::Get();
	
	if (FPlayWorldCommands::GlobalPlayWorldActions.IsValid())
	{
		// UE4のあらゆるところで呼び出せるコマンドとして紐付け
		FPlayWorldCommands::GlobalPlayWorldActions->MapAction(
			Commands.MyEditorCommand,
			FExecuteAction::CreateLambda(
				[]()
				{
					const FText Message = FMyEditorCommands::Get().MyEditorCommand->GetDescription();
					const FText Title = FMyEditorCommands::Get().MyEditorCommand->GetLabel();
					FMessageDialog::Open(EAppMsgType::Ok, Message, &Title);
				}
			)
		);
	}
}

#undef LOCTEXT_NAMESPACE

#endif //WITH_EDITOR

13行目からのOnEditorLoadedの役割はBindCommandsの呼び出しです。ただし、BindCommandsが何回も呼び出されることがないようにフラグ制御などを行っています。

39行目からのBindCommandsでは、(1)のFMyEditorCommands で用意したコマンドに処理を紐付けていきます。処理の紐付けは、FUICommandList::MapActionという関数を呼び出して行います。呼び出しに使用するFUICommandListは、コマンドの処理を実際に実行するときにショートカットキーの入力を受け付けるエディターのものを使います。

今回は自分でエディターを新規につくるわけではないので、47行目にあるように、FPlayWorldCommands::GlobalPlayWorldActionsという、UE4エディターの様々な場面でキー入力を受け付けてくれるものを使っています。MapActionの第一引数ではコマンド変数を渡し、第二引数では処理の内容としてラムダ式や関数を渡します。サンプルでは不要のため省略していますが、第三引数以降で「指定した関数がtrueのときのみコマンドを実行可能にする」といった振る舞いも設定可能です。

50行目からのラムダ式で記述してある内容は、UE4にコマンドを登録する時に指定した、コマンド名とコマンドの解説文をメッセージボックスで表示するというものです。

(3) UE4の起動フローでつくった処理を呼び出す

これらコマンドの登録処理を、エディターモジュールの読み込み時に呼び出したいと思います。

[MyProjectEditorModule.cpp]
#pragma once

#include "MyProjectEditor.h"
#include "MyProjectEditorModule.h"

#include "MyEditorCommands.h"
#include "MyEditorCommandBinder.h"

class CMyProjectditorModule : public FDefaultGameModuleImpl
{
public:
private:
	virtual void StartupModule() override;
	virtual void ShutdownModule() override {};

private:
	FMyEditorCommandBinder cmdBinder;
};

IMPLEMENT_GAME_MODULE(CMyProjectditorModule, MyProjectEditor);

void CMyProjectditorModule::StartupModule()
{
	// UE4にコマンドの存在を登録する
	FMyEditorCommands::Register();

	// コマンドのバインドをエディターロード後に予約する
	FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer();
	FDelegateHandle LoadedDelegateHandle = SlateRenderer->OnSlateWindowRendered().AddRaw(&cmdBinder, &FMyEditorCommandBinder::OnEditorLoaded);

	cmdBinder.SetBindHandle(LoadedDelegateHandle);
}

このソースは、モジュールの起動・終了処理を書くファイルです。

モジュールについての詳細は、技術ブログ001回でも引用させていただいたこちらを参考にされてください。
[UE4] モジュールについて|株式会社ヒストリア

サンプルのStartupModule内、24行目で FMyEditorCommands::Register を呼び出しています。
この関数は、基底であるTCommandsの関数です。その内部で FMyEditorCommands::RegisterCommands を呼び出してくれます。

残りは、コマンドと処理の紐付け、FMyEditorCommandBinder::BindCommands の呼び出しですが、StartupModuleの実行時点では、UE4の起動処理は完了しておらず、コマンドへの処理の登録時に必要になる FPlayWorldCommands::GlobalPlayWorldActions へのアクセスがうまくいきません。

これを避けるため、27行目からの処理で、UE4の起動が完了し次第、FMyEditorCommandBinder::OnEditorLoadedが呼び出されるようにイベントへの登録を行っているのです。

(4) 依存モジュールへの参照を追加する

最後に、今回のソースを追加したモジュールのBuild.csに、以下のモジュールへの参照を追加してください。

“EditorStyle”,
“UnrealEd”,
“SlateCore”,
“Slate”

(5) 動作確認

プロジェクトをビルドしてUE4Editorを起動しましょう。

いきなり設定したショートカットキー(Ctrl + F12)を押してしまって大丈夫です。

tb003_06

紐付けた処理が実行されました。

続いて、設定画面も見てみましょう。追加したコマンドのキーアサインがこの画面で変更できるようになっています。

tb003_07

 

エディターのコマンドが登録されるタイミング

最後に、関連するトピックとして、エディターのコマンドが登録されるタイミングの話をします。

UE4がショートカットキーを認識するタイミング(=エディター設定画面に表示されるタイミング)は、今回サンプルでも書いた TCommands::RegisterCommands が呼び出された時点です。

一方、UE4の各種エディターは、UE4上ではじめて起動されるときに、RegisterCommandsを呼び出しています。そして、その前後のタイミングでエディターの持つ FUICommandList に対してMapAciton関数でコマンドと処理の関連付けを行っています。
※FUICommandList として、エディターの基底クラスのメンバ変数 TSharedRef<FUICommandList> FBaseToolkit::ToolkitCommands を使用します。

こういう事情で、エディターのコマンドが設定画面に反映されるのは、最初にエディターが起動されてから、という振る舞いになっているのです。

UE4に認識されるのが遅延するとはいえ、設定画面の更新はリアルタイムで行われますし、過去に設定したキーアサインが消えてしまうということもありません。このような挙動になった背景が気になるところですが、推測半分の内容で記事が長くなるのは避けられなさそうなので、いったんこういう挙動を見つけました、というところでこの話は締めたいと思います。

参考

今回のソースはRenderDocプラグインを参考に作っています。
Engine\Plugins\Developer\RenderDocPlugin\Source\RenderDocPlugin\Private\SRenderDocPluginEditorExtension.cpp

描画負荷の確認に便利なプラグインですね。
ゲーム起動中に負荷に着目したいフレームを狙って素早くキャプチャのコマンドを実行する必要があるので、ショートカットキー対応をしてくれているのは嬉しいですね。

おわりに

さて、技術ブログ 第三回、最後まで読んでいただきありがとうございました。

引き続き、ご意見、ご感想等などなどお待ちしています。下記 CC2技術ブログのメッセージフォームにて、お気軽にお問い合わせください。

「CC2技術ブログ」へのご意見・ご要望はこちらから

 

 - CC2技術ブログ , ,

  関連記事

tb004_15
第005回 Tick関数がどう処理されているかUnrealC++を追ってみよう!

皆さんグーテンターク(ドイツのあいさつ)! CC2TechBlog今回の担当は新 …

tb004_09
第004回 UE4のアセットを一括修正する

ツール系テクニカルアーティストのしばはらです。 プロジェクトが進んで、ある程度ア …

スクリーンショット (47)
第007回UE4のアセットの参照方法について、そのロードの違い

皆さんボンジュール(フランスのあいさつ)! ランダムのスペルが覚えられないプログ …

第001回 UE4のコンテンツブラウザフィルターを自作する

はじめまして サイバーコネクトツーでツール系テクニカルアーティストをしております …

CC2技術ブログ
第006回 コマンドラインからConfig設定を上書きする時の注意点

皆さん、こんにちは。サイバーコネクトツーでテクニカルサポートのマネージャーをして …

第008回:ゲームクリエイターは破壊表現が好き。

皆さんボンジョルノ(イタリアのあいさつ)! 乱読派プログラマ親泊です。 現代ゲー …

tb002_04
第002回 アクタのトランスフォームの実態

皆さんストラーズドヴィーチェ(ロシアのあいさつ)! 今回の担当は、新人プログラマ …