<前文>
<本文>
<目的>
3章3節の5、ターン制の戦闘(turn based combat)の5、UMGによる戦闘UI(Combat UI with UMG)を勉強します。やっと簡単になったのかなと思ったらSlateモジュールなどを使用して作成するみたいですね。これは結構ハードルが高いかもしれませんね。
<方法と結果>
<Step.0>
前回まで使用していたCh2をそのまま今回も使用します。
<Step.1>
教科書に、
まず〔プロジェクト名〕.Build.csを開き、コンストラクターの最初の行に"UMG", "Slate", "SlateCore"を追加します。
とありました。しかし私のプロジェクトのCh2.Build.csファイルには、
があります。これをアンコメントした方が良いような気がします。この場合PrivateDependencyModuleNames()関数で実装される事になるので最初の行に追加した場合のPublicDependencyModuleNames()関数で実装した場合と厳密には違いますがどちらが正しいのでしょうか?
まず、サンプルコードを見てみます。
サンプルコードではPrivateDependencyModuleNames()関数をアンコメントせずにPublicDependencyModuleNames()関数に追加していますね。
ウーン。前にPrivateDependencyModuleNames()関数とPublicDependencyModuleNames()関数の違いについて勉強したのですが忘れてしまったのでもう一度復習してから決定します。このサイトに、PrivateDependencyModuleNames()関数とPublicDependencyModuleNames()関数の違いについての解説があります。
この解説によると、
PublicDependencyModuleNames()関数―>Public とPrivateフォルダーの両方が利用出来る。
PrivateDependencyModuleNames()関数―>Privateフォルダーのみが利用出来る。
とあります。ただしこの後の解説で普通のプラグインはPublicのフォルダー内にはインターフェイスしか入っていないのでPrivateのフォルダーのみにアクセス出来るPrivateDependencyModuleNames()関数で良いとあります。
成程。分かりました。モジュールを使用するに当たってそのモジュール内にある関数がもしPublicフォルダーにしか存在しないならそのモジュールはPublicDependencyModuleNames()関数を使用しなければなりませんが、それ以外はPrivateDependencyModuleNames()関数でいいと言う事ですね。
しかしこれは逆に言えば全てにPublicDependencyModuleNames()関数を使用すれば良いとも言えます。大は小を兼ねる。ですね。
色々考えましたが、今回はサンプルコードと同じにします。
"UMG", "Slate", "SlateCore"をPublicDependencyModuleNames()関数に追加しました。
<Step.2>
次にRPG.hファイルに以下のコードを追加します。
教科書のRPG.hファイルは私のCh2.hファイルなので、Ch2.hファイルに追加します。
サンプルコードにはEngine.hファイルもインクルードされていたのでついでに追加しました。
ビルドします。
かなり時間がかかりましたが成功しました。
<Step.3>
次に戦闘UIのための基礎クラスを作成します。名前はCombatUIWidgetです。UserWidgetクラスから作成します。Source/RPG/UIフォルダー内に作成します。
まず、UIのフォルダーから作成します。UIフォルダーはSourceフォルダー内のCh2フォルダー内に作成します。
まずvisual studioのsolution explorer内のsolutions and foldersをクリックします。
実際のファイルが保存されているフォルダーが表示されますので、そこにUIフォルダーを追加します。
次にCombatUIWidgetクラスを作成します。まずC++ウィザードをUE4エディターから起動させて親クラスにUserWidgetを選択します。
Pathには先程作成したUIフォルダーを指定します。
作成しました。
UE4エディターではこんな感じで表示されています。
Visual Studioのsolution explorer内では以下のように表示されています。
それでは教科書通りにコードを追加していきます。
追加しました。
AddPlayerCharacterPanel()関数とAddEnemyCharacterPanel()関数は、キャラクターへのポインターを獲得する事とそのキャラクターのためのウィジェットを作成する事を行うための関数だそうです。
試しにビルドしたところ成功しました。
<Step.4>
CombatUIWidgetを親クラスとするCombatUIと名付けたWidget BlueprintをContent/Blueprints/UIフォルダー内に作成します。
以下に教科書に書かれている手順を示します。
- まずUE4エディターからContent/Blueprints内にUIと名付けた新しいフォルダーを作成します。
- 次にそのフォルダー内にCombatUIと名付けたWidget Blueprintを作成します。
- そしてそのBPを開きFile|Reparent Blueprintに行きCombatUIWidgetを選択します。
後から親クラスを変更するみたいです。この方法は初めてです。教科書に書かれている通りにやって行きましょう。
まずフォルダーUIをContent/BP内に作成しました。これは簡単ですね。
次にWidget BlueprintをUser Interfaceから選択します。
名前をCombatUIと名付けます。
そしてそのBPを開きFile|Reparent Blueprintに行きCombatUIWidgetを選択します。
CombatUIWidgetがありませんね。
うーん。とコードと教科書を見直していたらStep.3でCombatUIWidgetと名付けるところをMyUserWidgetのままにしていました。
こんな凡ミスを犯すなんて!と思いましたが、やってしまったものはしょうがないのでこれを直します。直す方法ですが色々考えたのですが最初からやり直すのが一番速そうです。のでMyUserWidgetを消してCombatUIWidgetを作り直します。
まずMyUserWidgetを消します。ここで思ったのですがUE4C++における正しいファイルの消し方の手順みたいのはないのでしょうか?ちょっと調べて見ます。
ありました。
このサイトによると
となってました。ただしこの回答をした人も俺はこのやり方で問題なく出来ているけど。みたいなあまり自信のない書き方でした。Binariesファイルだけ消去する理由は何故なんでしょうか?どうせなら必要なファイル以外は全て消してやり直した方が安心出来ると思うのですが?それに4. Run GenerateProjectFiles.batするよりもuprojectファイルを実行した方がいい気がします。ので私が勝手にUE4C++におけるC++クラスファイルの消去の仕方を作り直しました。
- Visual StudioとUE4エディターを閉じる。
- Soruceファイル内の削除したいhファイルとcppファイルを削除する。
- .vs, Binaries, Intermediate, Saved, そして.slnフォルダーを消します。(必要なフォルダー以外は全て削除します。)
- .uprojectを実行します。
- .uprojectを右クリックしてGenerate visual studio… を選択して新しいVisual studio のsolutionを作成する。
多分これの方がいいでしょう。やって行きます。
念のためにプロジェクトのコピーをしておきます。
MyUserWidget.hとMyUserWidget.cppファイルを消します。
.vs, Binaries, Intermediate, Saved, そして.slnフォルダーを消します。
.projectをクリックします。
はいを選択します。
ビルドが終わるまで待ちます。ビルドが終わるとUE4エディターが自動的に開かれれるので、MyUserWidgetがない事を確認して閉じます。
.uprojectを右クリックしてGenerate visual studio…を選択します。
.slnが作成されました。
Epic launcherからCh2を開きます。そしてUE4エディターからvisual studio を開きます。
見事にMyUserWidgetが消えています。
当然ですがビルドとしても成功します。
ただGameModeの設定がNoneになっているのですが前からこうだったのか覚えていません。
ちょっと不安が残りますが続きをやって行きましょう。
Step.3をやり直します。Source/RPG/UIフォルダー内にCombatUIWidgetクラスをUserWidgetクラスから作成します。
今度は間違えずに名付けます。
今度こそ間違えなく出来ました。
コードを追加していきます。
ここでビルドしてみると成功しました。
これで要らないMyUserWidgetクラスを消去して必要なCombatUIWidgetクラスを追加出来たはずです。
CombatUIをもう一度開いて見ます
今度はCombatUIWidgetがありました。
CombatUIWidgetを選択します。
<Step.5>
教科書に
Designerインターフェイス内で、二つのHorizontal Box widgetを作成します。
それぞれの名前をenemyPartyStatusとplayerPartyStatusにします。
この二つは敵とプレイヤーのそれぞれの現状の状態を示すための子のwidgetを保持します。
とありました。
作りました。
それぞれのboxのIs Variableがenableになってる事を確認します。
之の事でしょうか?違うような気もしますが、他に見当たらないので良いとします。両方のboxを確認しました。
enemyPartyStatusから位置を調節します。
Anchorから黄色で囲ったヤツを選択します。
教科書に指定された値を入力します。
playerPartyStatusも同様に位置を調節します。
以下に示したように教科書に示されている図とほぼ同じように出来ました。
<Step.6>
次にプレイヤーと敵のキャラクターの状態を表示するwidgetを作成します。最初に基礎になるウィジェットを作成します。
BaseCharacterCombatPanelと言う名前のWidget Blueprintを作成します。
MyBlueprintタブ、CharacterTargetそしてObject ReferenceのカテゴリーからGameCaracter変数を選択する事で新しい変数を追加します。
と教科書に書かれていました。
Widget Blueprintを作成しBaseCharacterCombatPanelと名付けました。
BaseCharacterCombatPanelをクリックしてBPエディターを開きます。
BPエディターが開いたら右上にあるGraphをクリックして表示をグラフに変更します。
BPエディターの左側にあるMyBlueprintからVariablesの+をクリックして新しい変数を追加します。
新しく追加された変数を選択してDetailをみるとvariable TypeがBooleanになっているのでこれをGameCaracterに変更します。
Object typesからGame Characterを選択し、更にObject Referenceを選択しました。
これで合っていると思うんですがCharacterTargetが何なのか分かりませんね。念のためにサンプルコードを起動(UE4.12で起動)してチェックしてみます。
CharacterTargetはこの変数の名前でした。
教科書には以下のように書かれていたのですが、
新しい変数、CharacterTarget、と読めばまあCharacterTargetが新しい変数の名前を指していると読めなくはないですね。
後、GameCharacterの右隣にルービックキューブみたいなのが表示されていますが、サンプルコードの方はarrayになっているの?
これって確か古いUE4のバージョンと新しいバージョンでは表示方法が違っていたはずです。のでarrayになってはいないはずです。
はい。これでここまでは一応出来ました。次に行きます。
<Step.7>
新しいWidget Blueprintを作成します。名前はPlayerCharacterCombatPanelとします。
新しいBlueprintの親にBaseCharacterCombatPanelを指定します。
とありました。
まずWidget Blueprintを作成し名前をPlayerCharacterCombatPanelに変更しました。
更にBaseCharacterCombatPanelを親に指定し直しました。
<Step.8>
次に教科書では以下の作業を追加するように述べています。
デザイナーインターフェイスから、三つのテキストウィジェットを追加します。
一つ目のラベルはキャラクターの名前、
もう一つはキャラクターのHP、
そして三番目はキャラクターのMPです。
スクリーンの左下に配置されるようにします。
そして我々が作成したCombat widget内のplayerPartyStatusboxのサイズである高さ200ピクセル内に収まるようにします。
やって行きます。
出来ました。
それぞれのTextBoxのdetailでSize To Contentをチェックします。
これらのそれぞれのウィジェットのために新しいバインドを作成します。DetailsパネルにあるTextインプットの隣のBindをクリックしてください。
しました。
<Step.9>
次にテキストブロックを生成するためのブループリント関数を作成します。
教科書に一例としてHPを連結する方法が紹介されています。
- このグリッド内のオープンエリア内を右クリックし、Get Character Targetを探します。探したらそれをクリックします。
- このノードのアウトプットピンを引っ張り、Variables|Character Info下にあるGetHPを選択します。
- 新しいFormat textノードを作成します。そのテキストをHP: {HP}と設定します。そしてGetHPのアウトプットをFormat Textノードのインプットに接続します。
- 最後にFormat TextノードのアウトプットをReturnノードのreturnに接続します。
このまま実行します。
1.で紹介された方法でCharacter Targetを収得しました。
同様に2.で紹介された方法でCharacter Target のHPのノードを収得しました。
3.で紹介されているFormat textノードが何なのか不明ですね。サンプルコードを実行して確認してみます。
こうなっていました。となるとFormat textというノードがあるようですね。
こうなりました。サンプルコードと違いFormat Textはintをintとして受け入れています。取りあえずこのままで行きます。
最後に
Format textのresultとReturn Nodeのreturn valueを連結します。
これをPlayerCharacterCombatPanel BPのNameとMPも同様に行い、更にEnemyCharacterCombatPanelBPも同様に作成して下さいとあります。
分かりました。それではやって行きましょう。
<PlayerCharacterCombatPanel BPのName>
こんな感じで作成しました。念のため、サンプルコードの方も見てみます。
まあ大体一緒ですね。
<PlayerCharacterCombatPanel BPのMP>
次はMPを作成します。
こんな感じになりました。
<EnemyCharacterCombatPanelBP>
EnemyCharacterCombatPanelBPをPlayerCharacterCombatPanelBPと同様に作成します。
しました。
<EnemyCharacterCombatPanel BPのName>
こんな感じに出来ました。
<EnemyCharacterCombatPanel BPのHP>
PlayerCharacterCombatPanelBPのHPと同じですね。
<Step.10>
やっと我々はプレイヤーと敵のウィジェットを持つ事になりました。
CombatUIのブループリント内でAddPlayerCharacterPanel関数とAddEnemyCharacterPanel関数を実装しましょう。
と教科書に書かれていますのでやりましょう。
まず最初に、キャラクターステータスのウィジェットを発生するためにヘルパーブループリント関数を作成します。
この新しい関数をSpawnCharacterWidgetと名付けます。そして以下のパラメーターをインプットに追加します。
- Target Character (of type Game Character Reference)
- Target Panel (of type Panel Widget Reference)
- Class (of type Base Character Combat Panel Class)
とありますのでCombatUIブループリントからやっていきます。
を開き、関数を追加します。
そのパラメーターを
教科書通りに使用としたら最後のパラメーターのタイプであるBase Character Combat Panel Classがありませんでした。これってCombatUIの隣にあるWidgetの事ですよね。
うーん。どうやってこれをインクルードするのでしょうか?
取りあえずコンパイルしてみます。
あれ出来たみたいです。
一応サンプルコードで確認してみます。以下にサンプルコードのSpawnCharacterWidget関数のインプットを示します。
うん。合ってそうです。
<Step.11>
Step.10でSpawnCharacterWidget関数のインプットまで作成しました。Step.11ではSpawnCharacterWidget関数を実装します。
- Create Widgetを使用して与えられたクラスの新しいウィジェットを作成します。
- 新しいウィジェットをBaseCharacterCombatPanelタイプにキャストします。
- Character Targetの結果をTargetCharacterのインプットにセットします。
- TargetPanelのインプットとして新しいウィジェットを追加します。
と実装の手順が説明されていますのでその通りに作成します。
出来ました。出来ましたがCast to BaseCharacterCombatPanelノードの所で、オブジェクトは既にBaseCharacterCombatPanelであるのでキャストする必要はありませんと警告が出ています。
念のためサンプルコードをチェックすると
サンプルコードは何の警告も出ていませんでした。
後、手順3はBaseCharacterCombatPanelオブジェクトの変数Character Targetの値をSpawnCharacterWidget関数のインプットであるTarget Panelでセットします。と言う意味でした。
それならSet the Target Character input to the Character Target of the Base Character Combat Panelというべきじゃね。と思ってるのですがどうでしょうか?
<Step.12>
今度はCombatUIのブループリントのイベントグラフ内で作業します。
右クリックしてEventAddPlayerCharacterPanelとEventAddEnemyCharacterPanelイベントを追加します。
これらをそれぞれSpawnCharacterWidgetノードに連結して、そのtargetのアウトプットをTargetCharacterのインプットと連結します。
そして適切なパネル変数をTarget Panelインプットに連結します。
ここまでは出来ました。Target Panelに連結する変数Enemy Party StatusとPlayer Party Statusが見つかりません。勝手に作っていいのでしょうか。サンプルコードで確認してみます。
お。変数にしっかり定義されてます。これは?
Detailをみるとこの変数のタイプはHorizontal Boxになっています。もしかして
デザイナーモードに戻ってenemyPartyStatusを確認してみると変数にするにチェックが付いていました。これですね。
チェックをつけてEnemy Party StatusとPlayer Party Statusを変数化します。
グラフモードに戻るとEnemy Party StatusとPlayer Party Statusが変数として表示されています。
今度こそ出来ました。
<Step.13>
- とうとう戦闘の最初に我々のゲームモードからこのUIフォームを発生しさらに戦闘の最後にそれを破壊する事が出来る様になりました。
- RPGGamModeのヘッダー内で、UCombatUIWidgetとcombatUIのために発生するクラスにポインターを追加します。
- (そうする事で我々はウィジェットブループリントを選択します。そのウィジェットブループリントは我々のCombatUIWidgetクラスから継承しています。)
- これらはPublicであるべきです。
と書かれていました。
教科書の指定通りにコードをRPGGamMode.hに追加しました。追加した二つの変数は教科書の指示通りにPublicに追加しました。
<Step.14>
RPGGamModeのTestCombat()関数の最後に以下に示すように、このウィジェットの新しいインスタンスを発生させます。
とありました。
ので丸コピーで教科書のコードをRPGGamModeのTestCombat()関数の最後に追加しました。
最初のコードの意味は明白ですね。Step.13で宣言した変数、CombatUIInstanceの初期化を実装しています。
2番目は何をやっているのでしょうか?
AddViewport()関数ってどっかで聞いたことがある関数ですね。
うーん。思い出しました。ブループリントでWidgetを表示する時に使用する関数です。
となるとここでウィジェットを表示するわけですね。
となるとこの部分のコードの意味はもう明白ですね。マウスのカーソルを表示すると言う意味ですね。
同様なブループリントのノードがありますね。
このForループが分からない所があるのですがそれはちょっと置いておいてAddPlayerCharacterPanel()関数から見ていきます。
AddPlayerCharacterPanel()関数が呼ばれるとCombatUIWidgetのEvent Graphにある上記のイベントが実行されます。TargetはgameInstance->PartyMembers[i]なのでいつものプレイヤーのデータが送られるのは分かります。
しかしPlayer Party Statusは単なるボックスなのでこれを送る意味があまり分かりません。
この先にあるSpawnCharacterWidget()関数内の実装をみてもPlayer Party Statusを指すTarget PanelはAdd Child()関数に直接パスされていて一寸意味が分からなくなってしまっています。この辺は考察でしっかり考えてみたいと思います。
これはウィジェットを作成し、ビューポートにそれを追加し、マウスカーソルを追加し、そしてAddPlayerCharacterPanel()関数とAddEnemyCharacterPanel()関数を全てのプレイヤーと敵にそれぞれ追加します。
とあります。この「全てのプレイヤーと敵にそれぞれ追加します。」の部分ですがこれはForループの部分を指していると思われるのですが、これを行ってしまうとすべてのplayerのメンバーと敵のメンバーのステイタスのウィジェットが一遍に開いてしまうのではないでしょうか?ここも分からない部分です。
が取りあえず先に行きます。
<Step.15>
- 戦闘が終了した後、ビューポートからウィジェットを取り除きます。
- そしてレファレンスをnullにセットします(のでガーベージコレクションされます)。
- Tick関数は以下の様になります。
と教科書にありました。以下の部分のコードをコピーしましょう。
実際にコピーしたのは上記の部分だけでした。教科書の最初の解説をそのままコードにしただけですね。
<Step.16>
- この時点で、コンパイル出来ます。
- しかしコンバットをテストしたらゲームはクラッシュします。
- なぜからcombatUIをあなたがhで作成したCombatUIClassとして使用するためにDefaultRPGGameModeクラスのdefaultをセットする必要があるからです。
とありました。と言う事はビルドは出来ると言う事ですね。試してみます。
成功しました。
defaultRPGGameModeクラスのCombatUIClassの設定をCombatUIに変更しました。
これで全部出来たはずです。テストしてみます。
速すぎでスクリーンショットが全部は取れないですが教科書の説明と同じ結果が表示されました。
<考察>
今週は量が多くて時間が無くなってしまいました。ので考察は来週します。