UE4の勉強記録

UE4の勉強の記録です。個人用です。

新しいツールバーボタンを作る(リバースエンジニア編)

<前文>

f:id:kazuhironagai77:20180211085827p:plain

で勉強しています。

今回は、先週、作成したツールバーボタンのコードを分析していきます。前回ツールバーボタンを作成する事に成功しましたが、コードが全くのブラックボックスと化していますので、リバースエンジニアする事で、中身を理解したいと思います。

 

<本文>

<目的>

前回作成したツールバーボタンを作成するのコードを分析する事で、「どのようにUE4エディターで、ツールバーボタンを作成するかを理解する。」事が目的です。理解の目安としては、

  1. 全体の流れを大まかに説明出来る。
  2. 一つ一つのコードが何を行っているのかをコメントできる。
  3. 部分的にコードを変えて、結果をある程度変える事が出来る。
  4. アイコンを変える。

自分が作ったアイコンをツールバーに表示出来たら、すごく楽しいです。

<方法>

リバースエンジニアリングとは言いましたが、特にリバースエンジニアを勉強した事はないので、以下の事をやって見ます。

  • 教科書のhow it works…をよく読む。
  • 使用されたすべてのメンバー関数、クラスをUE4APIで調べる。
  • コードを少しだけ変えて結果の変化を観察する。

<仮説>

前回書いたコードの意味が全く分からない状態ですがヒントはあります。例えばUE4CookbookEditorクラスにある二つのメンバー関数であるStartupModule() 、ShutdownModule()です。C++の常識から考えれば、StartupModule()でオブジェクトを動的に作成して、ShutdownModule()で作成したそれらのオブジェクトを破壊していると考えられます。実際、StartupModule()関数を見てみると、二行目に、

f:id:kazuhironagai77:20180211090046p:plain

と書かれており、何らかの、オブジェクトを作成しています。さらに、その先のコードには、

f:id:kazuhironagai77:20180211092234p:plain

とあり、UE4CookbookEditor.hで宣言したオブジェクト変数である、ToolbarExtenderと、Extensionを初期化しています。また、ShutdownModule()関数を見てみると、

f:id:kazuhironagai77:20180211090139p:plain

Reset(), Remove()などの関数をStartupModule()で作成もしくは初期化したオブジェクト変数に使用しています。Reset(), Remove()の目的はその名称よりオブジェクトを破壊していると考えられます。

教科書には「UE4エディターはSlateモジュールが持つコマンドによりそのUIを構成している。このコマンドの集合をC++側から使用出来るようにしないとツールボタンの作成は出来ない。」と書かれています。この事より、Slateモジュールの持つコマンドをC++側から使用出来るようにする部分と、実際に使用可能になったコマンドを用いてUE4エディターのツールボタンを作成する部分が存在すると考えられます。

ここで、前回作成した2つのクラス、CookbookCommandsクラスとUE4CookbookEditorクラスが、それぞれを担当していると仮定してみます。つまりCookbookCommandsクラスがSlateモジュールの持つコマンドをC++側から使用出来るようにする事、UE4CookbookEditorクラスが、実際に使用可能になったコマンドを用いてUE4エディターのツールボタンを作成していると考えてみます。これは、大胆な仮定ですが、根拠が全くないわけではないです。CookbookCommandsクラスは、TCommandsクラスを継承しています。TCommandsクラスのAPIによると、TCommandsクラスを継承したクラスは、set of command を作る事が出来ると書かれています(… Inherit from it to make a set of commands.)。つまり、UIを管理するslateモジュールのコマンドをC++側から使用したければ、このTCommandsクラスを継承したクラスをお作り下さいと言っているとも考えられるのです。なので、CookbookCommandsクラスがSlateモジュールの持つコマンドをC++側から使用出来るようにする事を担当していると考えるのは、十分可能なのです。

更に、UE4CookbookEditorクラスで使用可能になったコマンドを用い、UE4エディターのツールボタンを作成していると考えると、上記のStartupModule() 関数の意味がはっきりします。このStartupModule() 関数内で、UE4エディターのツールボタンを作成しているのです。ツールボタンを作成するためにはSlateモジュールのコマンドが使用出来ないといけません。ですから最初にCookbookCommandクラスのオブジェクトを作成しているはずです。

f:id:kazuhironagai77:20180211090235p:plain

StartupModule() 関数内を見ると、CookbookCommandクラスのオブジェクトは生成していません。が、何か惜しいのがあります。FCookBookCommands::Register()です。CookbookCommandsクラスの正式なクラス名は、FCookbookCommandsなので、まさしくCookbookCommandクラスです。C++の教科書でクラス名に直接、メンバーを呼んで使用する方法を学習した記憶はあるのですが、覚えていません。シングルトンでしたっけ? まあしかし、ここで、Slateモジュールのコマンドを用いてUE4エディターのツールボタンを作成している可能性はかなり高いでしょう。

仮説1をまとめます。

f:id:kazuhironagai77:20180211090304p:plain

この仮説を基に、検証していきたいと思います。

<検証>

では、コードを一行ずつ読んでいきます。上記の仮説に基づくならば、CookbookCommandクラスから、読むべきでしょう。CookbookCommandクラスは、SlateモジュールのコマンドをC++から使用出来るようにするクラスのはずです。私はどのようなC++のコードを書けばそんな事が可能なのか、全く分からないので、形式めいたものは、そういうものとして、そのまま受け入れます。まず、ヘッダーファイルから読んでいきます。

f:id:kazuhironagai77:20180211090345p:plain

2行目の#pragma onceは、ヘッダーファイルならば、必ず必要なので、CookbookCommandヘッダーファイルも同じなのでしょう。三行目では、#include “Commands.h”をしています。TCommandsクラスのAPIによると、

f:id:kazuhironagai77:20180211090411p:plain

TCommandクラスのヘッダーは、Commands.hとなっており、TCommandクラスを使用するためだと分かります。また、UE4の公式サイトのCoding Standardによると、

f:id:kazuhironagai77:20180211090436p:plain

となっているため、Commandクラスはテンプレートクラスなので、prefixにTを用いていると分かります。

四行目のEditorStyleSet.hファイルは良く分かりません。FEditorstyleSetクラスAPIを見てみると、

f:id:kazuhironagai77:20180211090504p:plain

このメンバー関数を使うために、このクラスは必要みたいです。ただし、このメンバー関数が何をしているかは、APIにも説明していなかったです。このクラスそのものの説明はありました。

f:id:kazuhironagai77:20180211090544p:plain

Slateモジュールの外見を指定するのに、使うクラスのようです。また、横道にそれますがFEditorStyle::GetStyleSetName()はクラス名::メンバー関数です。またこの形です。もうこれは、調べなければなりません。このブログの最後に調べて載せます。

たった二行ですが、かなり分かってきました。Commands.h とEditorStyleSet.h はSlateモジュールのコマンドをC++側から使用するのに、最低限インクルードしなければならないクラスで、Commands.hが機能を担当するコマンドを、EditorStyleSet.hが形状を担当するコマンドを管理しています。

次に、8行目のclass FCookbookCommands : public TCommands<FCookbookCommands>について調べます。CookbookCommandクラスは、UE4の定めるprefixのどのタイプにも当てはまらないので、Fが選ばれています。TCommandsクラスを継承するのですが、継承の描かれ方が少し通常と違います。普通ならclass FCookbookCommands : public TCommandsではないでしょうか。教科書のhow it works…にこの部分の解説が非常に詳しく書かれていて、この書き方は、Curiously Recurring Template Pattern (CRTP)と言う継承方法だそうです。CRTPを調べて見ると、ウキィペディアのCuriously recurring template patternに簡単な説明がありました。

f:id:kazuhironagai77:20180211090617p:plain

TCommands クラスをbaseクラス、FCookbookCommandクラスをDeriverivedクラスとすると、CRTPのフォームのままにクラス名が書かれている事が分かります。つまり、FCookbookCommandsクラスは、TCommandsクラスを、CRTPの形式で継承している事が分かります。

 ここまでで、以下の部分が解明されました。

f:id:kazuhironagai77:20180211090644p:plain

TCommnadクラスを継承するのに、CRTPを使用する以外は、すごく普通のC++のヘッダーファイルです。これで、Slateモジュールのコマンドが使えるようになるのでしょうか?正直まだよく分かりません。

勿論、この次は、このクラスの中身についてです。常識的考えれば、コンストラクタ、メンバー関数、オブジェクト変数などがあると思います。

f:id:kazuhironagai77:20180211090735p:plain

どう見てもFCookbookCommandsクラスのコンストラクタですよね。TCommands クラスをCRTPの形式で継承していますね。その後に4つのパラメータがあります。教科書のhow it works…に、説明があります。

  1. 最初のパラメータは、このコマンド一式を表す名前。
  2. 次のパラメータは、ツールチップに表示される文字。
  3. 3番目のパラメータは、コマンドの親グループが存在する場合に、ここで指定するため(ない場合はNAME_Noneとする。)。
  4. 最後のパラメータは、これから使われるすべてのコマンドアイコン一式を所持しているSlate Style setのコンストラクタのために使用する。

まず、最初のパラメータは名前だそうです。このクラスでslateモジュールからツールボタンを作るためのコマンドらを、ひとまとめで使えるようにするため、それを表す名前が必要と言う事でしょうか?

名前なら何でもいいんでしょうか?試して見ます。

f:id:kazuhironagai77:20180211090810p:plain

名前をUE4Cookbookaaaaaaにしました。

f:id:kazuhironagai77:20180211090831p:plain

問題なくビルド、実行出来ました。

ツールチップとは、マウスを上に置いたときに表示される一口メモみたいなやつです。FCookbookCommandsクラスの上にマウスを置いたら、Cookbook Commandと表示されるのでしょうか?

f:id:kazuhironagai77:20180211090904p:plain

表示されませんでした。私は、この2番目のパラメータの意味を勘違いしているのでしょうか?

 サンプルコードの3番目のパラメータは、NAME_Noneになっているので、コマンドの親グループが存在しないのだろうと思うくらいです。

最後のパラメータは、EditorStyleSet.hが形状を担当するコマンドを管理しているはずなので、ここでアイコンのコマンドを読み込む事で、このクラスでそれらのコマンドが使用出来るようにしていると考えられます。

結論から言えば、この四つのパラメータは何のために存在するのかの情報を得ることは出来ましたが、実際にそれが正しいがどうか、確認する方法は見つかりませんでした。今の時点では、こういう風に書くものと考えるしかありません。

コンストラクタのつぎには、TCommandクラスから継承したメンバー関数がオーバーライドされていました。

f:id:kazuhironagai77:20180211091005p:plain

教科書のhow it works…の説明によると、「このメンバー関数は、TCommandsクラスを継承したクラスが要求する如何なるコマンドオブジェクトを作ることを許可する。」とあります。分かったような分からないような説明ですが、私の今の解釈では、ここで、コマンドオブジェクトを作成すると解釈しています。そしてそのコマンドオブジェクトがツールバーボタンを作成するためのコマンドを保持していると考えています。

UE4のAPIには、

f:id:kazuhironagai77:20180211091039p:plain

と書かれています。「ここでコマンドの初期化と記述をUIコマンドマクロで行う。」と読むべきでしょうか。こっちの説明は分かり易いです。この中身がこのクラスの肝みたいです。ソースファイルでは、このメンバー関数の解釈が最も重要になるかもしれません。

最後は、

f:id:kazuhironagai77:20180211091105p:plain

と書いてあり、最初に予測した通り、オブジェクト変数です。教科書のhow it works…の説明によると、RegisterCommands()関数で指定したコマンドを使用するためには、RegisterCommands()関数の返り値を保持する変数が必要で、それがこの変数だそうです。

 次にソースファイルを見ていきます。

f:id:kazuhironagai77:20180211091146p:plain

まず、インクルードしているクラスから見ましょう。UE4CookbookEditor.hは、このクラスを使用して、ツールボタンを作成するクラスで、UE4CookbookEditorクラスは、CookbookCommandクラスをインクルードする必要はありますが、CookbookCommandクラスがUE4CookbookEditorクラスをインクルードする必要はないはずです。なくていいはずです。更に、その次のCommands.hクラスは、すでに、ヘッダーファイルでインクルードしているので、ここでもう一度インクルードする必要はないはずです。この二つのヘッダーは外してビルト、実行してみます。

f:id:kazuhironagai77:20180211091211p:plain

f:id:kazuhironagai77:20180211091222p:plain

やはり、問題なくサンプルコードは動きました。

次に、RegisterCommands() 関数内のコードを見て見ましょう。ヘッダーファイルの説明に、「ここでコマンドの初期化と記述をUIコマンドマクロで行う。」とありました。どのように行うのでしょうか?

まず、教科書のhow it works…の説明によると、UI_Commandを使用するためには、#define LOCTEXT_NAMESPACE “” #undef LOCTEXT_NAMESPACEで囲む必要があるそうです。

f:id:kazuhironagai77:20180211091254p:plain

囲んでいますね。そして、UI_COMMANDを使用しています。このUI_COMMANDマクロについて調べたのですが、教科書のhow it works…の説明より詳しいサイトは見つかりませんでした。しかも、教科書のhow it works…はそれぞれのパラメータの説明しかしていません。その説明によると、

  1. 最初のパラメータは、FUICommandInfoを蓄えるための変数用です。
  2. 次のパラメータは、人が読むための、このコマンドの名前です。
  3. 三番目のパラメータは、このコマンドの概要です。
  4. 四番目のパラメータは、EUserInterfaceActionTypeです。このenumは、どんなタイプのボタンを作るかを指定します。ボタンのタイプは、Button, ToggleButton, RadioButtonそしてCheckがあります。
  5. 最後のパラメータは、input chordもしくは、コマンドを使用するのに必要なカギの組み合わせです。(ボタンには使用しませんので、今回は空のFInputGestrue()を使用しています。)

となっています。

まず、ヘッダーファイルで、作成したオブジェクト関数であるMyButtonを最初のパラメータに指定しています。ここで、MyButtonがこのRegisterCommands() 関数の戻り値を蓄えると考えられます。UE4CookbookEditorクラスでは、このMyButtonを通してslateモジュールのコマンドを使用するはずです。2番目と三番目のパラメータは、人が後で見るためのものです。四番目のパラメータは、EUserInterfaceActionTypeで、どんなタイプのボタンを作るかを指定します。ボタン関連以外を作る場合は、違うパラメータになるのでしょうか?最後のパラメータは、今回は必要としないので、空のFInputGestrue()を使用しています。

本来は、次のUE4CookbookEditorクラスの検証もやる予定でしたが、時間がなくなってしまったので、ここで一端停止し、残りは来週行います。

<結論>

UE4エディターのツールバーボタンを作成するためには、Slateモジュールのコマンドを使用する必要があります。C++から、Slateモジュールのコマンドを使用するためには、Slateモジュールの持つコマンドをC++側から使用出来るようにする部分と、実際に使用可能になったコマンドを用いてUE4エディターのツールボタンを作成する部分があると考えられます。CookbookCommandsクラスがSlateモジュールの持つコマンドをC++側から使用出来るようにする事、UE4CookbookEditorクラスが、実際に使用可能になったコマンドを用いてUE4エディターのツールボタンを作成していると考えられます。

今回は、全てのコードを検証する時間がなかったので、CookbookCommandsクラスのみ検証しました。

CookbookCommandsクラスの目的は、SlateモジュールのコマンドをC++から使用する出来るようにする事です。このためには、UI_COMMANDマクロを使用する必要があります。ただしUI_COMMANDは、TCommandsクラスのRegisterCommands()関数からしか使用できません。通常は、TCommandクラスを継承したクラスからRegisterCommands()関数をオーバーライドして、そこから、UI_COMMANDマクロを使用します。ただし、あるクラスが、TCommandsクラスを継承するには、Curiously Recurring Template Pattern (CRTP)と言う方法で継承する必要があります。さらに、そのクラスのコンストラクタでは、継承したTCommandsクラスの4つのパラメータを指定する必要があります。これによって、初めて、RegisterCommands()関数をオーバーライドし、UI_COMMANDマクロを使用出来るようになります。また、RegisterCommands()関数の戻り値を蓄えるオブジェクト変数、TSharedPtr<FUICommandInfo>、もこのクラスには、必要です。更に、UI_Commandを使用するためには、RegisterCommands()関数内で、#define LOCTEXT_NAMESPACE “” #undef LOCTEXT_NAMESPACEで、UI_Commandを囲む必要もあります。UI_COMMANDマクロは、RegisterCommands()関数の戻り値を蓄えるオブジェクト変数を含む、5つのパラメータを指定する必要もあります。

<まとめ>

  1. CookbookCommandsクラスがSlateモジュールの持つコマンドをC++側から使用出来るようにする事、UE4CookbookEditorクラスが、実際に使用可能になったコマンドを用いてUE4エディターのツールボタンを作成していると考えられる。
  2. 今回は、時間が足りず、CookbookCommandsクラスのみ検証した。残りは来週します。

     <UE4CookbookEditorクラスの検証>

  1. SlateモジュールのコマンドをC++から使用するためには、UI_COMMANDマクロを使用する必要がある。
  2. UI_COMMANDマクロは、TCommandsクラスのRegisterCommands()関数からしか使用出来ない。
  3. TCommandsクラスを継承したクラスのRegisterCommands()関数をオーバーライドしてUI_COMMANDマクロを使用する方法が一般的。
  4. TCommandsクラスの継承は、Curiously Recurring Template Pattern (CRTP)と言う方法で行う。
  5. TCommandsクラスは、4つのパラメータをパスする必要があり、TCommandsクラスを継承したクラスは、そのクラスのコンストラクタでその4つのパラメータをパスする。
  6. UI_COMMANDマクロをオーバーライドしたRegisterCommands()関数から使用する場合は、#define LOCTEXT_NAMESPACE “” #undef LOCTEXT_NAMESPACEで、UI_Commandを囲む必要がある。
  7. UI_COMMANDマクロは、RegisterCommands()関数の戻り値を蓄えるオブジェクト変数を含む、5つのパラメータをパスする必要がある。
  8. よって、RegisterCommands()関数の戻り値を蓄えるオブジェクト変数をTCommandsクラスを継承したクラスに作る必要がある。

<おまけ>

<クラス名::メンバー関数> C++におけるこのやり方を忘れてしまったので、勉強し直す予定でしたが、もう時間がないので、これも来週します。