UE4の勉強記録

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

「Unreal Engine4.xを使用してRPGを作成する」の3章の3.4を勉強する part 3

<前文>

f:id:kazuhironagai77:20190526204904p:plain

最近ゲームのサーバーサイドでの管理にfirebaseを使ってみたいと思っています。Epic game社は独自のサーバーを無料でUE4のユーザーに貸し出す予定があるみたいですが、このサイトを見ると

f:id:kazuhironagai77:20190616220248p:plain

まだ始まったばかりのようで、正直どうすればいいのか良く分からないです。分かり易いチュートリアルが出来るまで待ったほうが良いみたいと思っています。パラパラとfirebaseの説明を読んだのですがunityからは使用出来るみたいですね。C++からも使用出来るようですが、UE4からはどうなのでしょうか?UE4からfirebaseが使用出来るplugInはあるみたい

f:id:kazuhironagai77:20190616220322p:plain

ですが値段が二万円以上ですね。

<本文>

<目的>

3章3節の4、ターン制の戦闘(turn based combat)、アクションの実行(Performing actions)を勉強します。

<方法と結果>

教科書にこの様に書かれていました。

キャラクターがアクションを実行するためには、我々は全ての戦闘のアクションを一つの共通のインターフェイスまで煮詰めなければなりません。

最初に取り掛かるに良い場所は我々が既に持っているマップのインターフェイスです。

それはキャラクターのBeginExecuteActionExcecuteAction関数です。

この教科書の良い部分の一つにソフトウェアデザインを決定するためのヒントが散りばめられている事が挙げれます。よしUE4の使い方は一通り理解した。RPGを作って見るか!となった時に、単純にオブジェクトオリエントのソフトウェアデザインを使用してソフトウェア開発をして良いのかなと疑問に思う人は多いと思います。もっと効率的なゲーム制作のためのソフトウェアデザインがあるのではないのか?そして疑問に思ってネットや本を探ると沢山のHow to物が出て来ます。UE4FPSゲームTPSゲーム、や2Dゲームの作成方法をステップバイステップで教える教材です。これらの教材のほとんどはどうしてそのようなソフトウェアデザインを採用したのかが全く不明です。AしてBしてCしてを延々と続けて最後に完成しますと言うヤツです。

この教科書はそういう意味ではいい意味で予想を裏切ってくれます。

<Step.0>

前回まで使用していたCh2をそのまま今回も使用します。

<Step.1>

ICombatActionインターフェイスを作成します。教科書にはSource/RPG/Combat/Actions内に作成して下さいとありました。まずこのActionsフォルダーから作成します。

Solutions and foldersボタンを使用してSolution explorer内の表示をフォルダーとファイルに変更します。

f:id:kazuhironagai77:20190616220454p:plain

CombatフォルダーにActionsフォルダーを追加します。

f:id:kazuhironagai77:20190616220515p:plain

ICombatAction.hファイルとICombatAction.cppファイルをActionsフォルダーに追加しました。

f:id:kazuhironagai77:20190616220613p:plain

教科書に載っているサンプルコードをそのままコピーしました。

f:id:kazuhironagai77:20190616220644p:plain

教科書にそれぞれの関数に対しての短い解説がありました。

BeginExcecuteAction()関数はこのアクションを実行するためのキャラクターへのポインターを取ります。

ExcecuteAction()関数はその前に前のフレームからの間を数秒間だけ取ります。

Cppファイルは以下のように作成しました。教科書には以下のように書かれていましたが、

f:id:kazuhironagai77:20190616220724p:plain

PRG.hは私のプロジェクトではCh2.hになるので、

f:id:kazuhironagai77:20190616220748p:plain

Ch2.hを追加しました。

<Step.2>

次にTestCombatAction.hを生のC++から作成します。TestCombatAction.hはICombatActionインターフェイスを継承します。TestCombatAction.hはSource/RPG/Combat/Actions内に作成します。

f:id:kazuhironagai77:20190616220823p:plain

しました。

以下に示すように教科書のサンプルコードからコードをコピーしました。

f:id:kazuhironagai77:20190616220858p:plain

f:id:kazuhironagai77:20190616220908p:plain

以下に教科書のBeginExcecuteAction()関数とExcecuteAction()関数の説明をもう一度示します。

f:id:kazuhironagai77:20190616220933p:plain

上記のコードをみるとExecuteAction関数がその前のフレームから数秒だけ間を取りますの意味が分かりますね。それ以外は具体的にこれらの関数が何をやりたいのかまだ不明です。

<Step.3>

次に、アクションを保持し実行出来る様にこのキャラクターを変更します。教科書には以下のように書かれていました。

最初にTest delay timercombat action pointerに代えましょう。

後でGameCharacter.h 内で意思決定のシステム(decision making system)を作成した時にそれをpublicにします。

まず、教科書通りに以下の部分のコードをGameCharacter.h 内に追加します。

f:id:kazuhironagai77:20190616221013p:plain

f:id:kazuhironagai77:20190616221030p:plain

更に教科書には以下のように書かれていました。

次に、decision()関数をCombat actionに割り当てるためと、action関数がGameCharater.cpp内でこのアクションを実行するためにdecision()関数を変更する必要があります。

教科書通りにGameCharacter.cpp内のDecision()関数内を変更しました。

f:id:kazuhironagai77:20190616221106p:plain

教科書の解説が非常に分かり易かったのでそのまま載せます。

  • BeginMakeDecision()関数:TestCombatAction()関数の新しいインスタンスを割り当てます。
  • MakeDecision()関数:単にreturnを返します。
  • BeginExecuteAction()関数:保持されているcombat action上にある同じ名前の関数を呼びます。
  • ExcecuteAction()関数:保持されているcombat action上にある同じ名前の関数を呼びます。そしてもし結果がTrueだったらポインターを消してTrueを返します。それ以外ではfalseを返します。

なるほどやっと意味が分かりました。今度はTestCombatAction()関数をテストする訳ですね。

早速テストしてみましょう。

f:id:kazuhironagai77:20190616221150p:plain

教科書によれば今度はdoes nothingが変わりに表示されると書かれています。その通りの結果になりましたね。

短いですが切れが良いので今回はここで終了して考察を行います。

<考察>

TestCombatを使用した時のコードの全体の流れがちょっと見えないので今回はそれを追跡します。実は一回最初からコードを追跡したのですが、途中でおかしくなり断念しました。原因はCombatEngineクラスのコンストラクターをよく読まなかったからでした。のでこれが最初の追跡ではありません。

まずRPGGameModeクラスのTestCombat()関数はゲームプレイ中のCommandからTestComatをタイプする事で開始します。

f:id:kazuhironagai77:20190616221234p:plain

f:id:kazuhironagai77:20190616221245p:plain

色々TestCombat()関数でやっていますが、結局はCombatEngineクラスを初期化するためのコードです。CombatEngineを初期化した場合、CombatEngineクラスのコンストラクターで以下の事が行われます。

f:id:kazuhironagai77:20190616221314p:plain

ここで行われたのは

  1. playerPartyenemyPartyを初期化。
  2. combatOrderをそのplayerPartyenemyPartyから作成。
  3. tickTargetIndex0に指定。
  4. SetPhase()関数を使用してCombatPhaseCPHASE_Decisionに設定。

ここで大切なのはSetPhase()関数では、

f:id:kazuhironagai77:20190616221359p:plain

SelectNextCharacter()関数が呼ばれると言う事です。一番最初にコードの流れを追う時ここを見逃してしまい完全に流れを間違えてしまいました。

そのSelectNextCharacter()関数では、

f:id:kazuhironagai77:20190616221434p:plain

waitingForCharacter変数がfalseにセットされtickTargetIndex変数は1に変更されます。ただしcurrentTickTargetではcombatantOrderの最初のキャラクター、KUMOにセットされます。

これだけでは何も起きませんが、この後ですぐにCombatEngineクラス内のTick関数が呼ばれます。

f:id:kazuhironagai77:20190616221501p:plain

まずCombatPhaseはCPHASE_Decisionにセットされているはずです。となると最初のcaseに該当するのでそのまま読めばいい事になります。

するとbool変数であるwaitingForCharacterがTrueを返すかどうかif節で聞かれます。

f:id:kazuhironagai77:20190616221539p:plain

waitingForCharacter変数はFalseの値を保持しています。のでそのままif節内のコードを実行します。

UGameCharacter クラスであるcurrentTickTargetのBeginMakeDecision()関数を呼び出します。

f:id:kazuhironagai77:20190616221620p:plain

ここで"Character %s making decision"をプリントアウトします。テストの結果をみると

f:id:kazuhironagai77:20190616221645p:plain

しています。更にその後TestCombatAction()を初期化します。次は

f:id:kazuhironagai77:20190616221740p:plain

が実行されます。のでUGameCharacter クラスであるcurrentTickTargetのMakeDecision()関数が呼ばれます。

currentTickTargetのMakeDecision()関数は以下に示すように単にtrueを返すだけです。

f:id:kazuhironagai77:20190616221818p:plain

次に以下のコードが実行されます。勿論decisionMadeはtrueですのでif節内のコードが実行されます。

f:id:kazuhironagai77:20190616221849p:plain

まずSelectNextCharacter()関数が呼ばれます。この関数思ってたより複雑な関数ですがやっている事は次のキャラクターを選ぶ事だけです。SelectNextCharacter()関数は先週勉強したばかりの関数ですので詳細はスキップしひたすらコードを追っていきます。

f:id:kazuhironagai77:20190616221940p:plain

まず、waitingForCharacter変数がfalseに設定されます。これは次のキャラクターが選ばれるのですから当たり前ですね。その次に今のキャラクターの次のキャラクターから順番に死んでいないかチェックします。もし死んでいなければそのキャラクターに変更です。CombatantOrder内の全部のキャラクターが終了したらtickTargetIndexを-1にセットしcurrentTargetをpullptrにセットします。

今回はTickTargetIndex変数は1にセットされていてかつキャラクターは全て生きているはずですので、次のキャラクターであるゴブリンが選ばれるはずです。更にTickTargetIndex変数は2にセットされるはずです。

最初の戦闘の最初のブロックのアクションの決定の部分しか見ていませんが、かなり全体像が見えて来ました。

またtick関数が呼ばれます。今度はゴブリンがキャラクターです。

全く同じ事を繰り返しますが、今度は最後にCombatPhaseがCPHASE_Actionに変更されるはずです。

キャラクターは最初に戻り今度はCPHASE_Actionに移ります。

f:id:kazuhironagai77:20190616222051p:plain

勿論waitingForCharacterはfalseですからUGameCharacterクラスのcurrentTickTargetのBegineExecuteAction()関数が呼ばれます。

f:id:kazuhironagai77:20190616222126p:plain

ここからTestCombatActionクラスの変数であるcombatActionからBeginExecuteAction関数が呼ばれます。

f:id:kazuhironagai77:20190616222225p:plain

ここでやっと"%s does nothing"が呼ばれるわけですね。

次はUGameCharacterクラスのcurrentTickTargetのExecuteAction()関数が呼ばれます。

 

f:id:kazuhironagai77:20190616222248p:plain

TestCombatActionクラスの変数であるcombatActionからExecuteAction関数が呼ばれます。

f:id:kazuhironagai77:20190616222355p:plain

これはDelayTimerが0以下になるまで繰り返されます。その結果DelayTimerが0以下になると

以下に示すコードが実行され次のキャラクターが活動を開始します。今回の場合はゴブリンです。

f:id:kazuhironagai77:20190616222416p:plain

ゴブリンは完全に同じ事を繰り返して、活動を停止し、戦闘のターンは最初に戻ります。

テストの結果のように以下の行動をゲームを停止するまで繰り返す事になります。

f:id:kazuhironagai77:20190616222436p:plain

今回は短いですがこれで終にします。短いなりにターン制の戦闘の流れとそのためのコードの書き方が理解出来ました。