UE4の勉強記録

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

「Unreal Engine 4.xを使用してRPGを作成する」の足りない部分を作成する ターン制戦闘の改良

f:id:kazuhironagai77:20200308232015p:plain

<前文>

先週、先々週と、ゲームデザインにおける主人公について考察しました。ので今週は、別な事について述べたいと思います。ゲームを作成するからには、商品として価値がある物を作りたいと、前々から述べていましたが、いかんせん、ゲームをあまりやった事が無い私は、ゲームを作成してもどのくらいそのゲームに価値があるのかが分からないんです。

そこで、考えているのが、英語の発音が身に付くゲームです。と言っても、発音はアメリカ人とワンツーマンで練習しなければ身に付かないので、英語の発音の聞き取りが出来るようになるだけです。一般的な日本人は、BとVとか、ThとS、そしてLとRの区別がつきません。このゲームで遊んでいるうちに、自然にそういう英語の発音が区別出来るようになるゲームです。これなら商品としての価値を私でも測る事が出来ます。

ただ、はっきり言ってしますと英語って日本語と違って発音にそんなにこだわる言語じゃないんで、音を一個ずつ聞き取れないからと言って相手のしゃべっている事が分からないって事は絶対ありません。むしろ音が聞き取れると、同じ単語でも人によって全然違う音で発音していたり、同じ人でも時と場合で全然違う音で発音していたりする事に気が付くだけです。ので、あくまで娯楽の一環としてそういう能力もおまけで付いてくると考えて下さい。

英語の音の聞き取りが上達するゲームを作成するに当たってどんな事を考えなければならないのかを以下にまとめます。

  1. 大抵そういうゲームは滅茶苦茶つまらなくなる。どうすれば面白く出来るか?
  2. どうすれば発音の区別がつくようになるの?

今回は、2のどうすれば発音の区別がつくようになるのか?について考察したいと思います。

私は理系ですので、全く音声学とか言語学とかを勉強した事はありませんし、興味もありません。しかし自分の英語学習からの経験から、効果がありそうな練習方法とそうでない物は推測出来ます。その中で私が最も効果がありそうと思っているのが、日本語を利用して勉強する方法とminimal pairによる方法を合わせた学習法です。

分かり易く説明するために、以下に私が勝手に作成した英語の母音と日本語のアイウエオの対応表を示します。

f:id:kazuhironagai77:20200308232112p:plain

英語の音声学や言語学を勉強した人からは怒られそうですが、この表だったら、沢山あって覚えきれない英語の母音が一瞬で覚えられます。

この表の大切な所は「あいうえお」を使用している所です。「a, e, i, o, u, (y)」じゃないんです。「a, e, i, o, u, (y)」を使うとeが「え」の音なのか、「い」の音なのかすぐに混乱します。この表だったら、英語の母音って色々言われているけど「あ、い、う、え、お」と「あー、いー、うー、おー」に、プラス日本語に対応しない音のvの逆さまの音と、それを伸ばした音だけだったんだ。と一瞬で理解出来ます。

それから、英語はなまっているので、日本語の「あ、い、う、え、お」と「あー、いー、うー、おー」では通じないんです。ぶっちゃけて言うと、赤い文字の部分は全部「あ」に聞こえます。と続ければいいんです。そうすると「あ」と「あー」の区別がつかない人はいませんから、長い音のɑ:とɜ:の違い、短い音のæとɑとʌ(ə)の違いが分かればいいとなるわけです。

長い音のɑ:はほとんど「あー」と同じですが、ɜ:は日本人には聞いた事が無い音ですが良く聞くと「あー」がかすかに混じっています。これで区別がつきます。

短い音のæ、ɑ、とʌ(ə)は、æは「え」が混じった「あ」、ɑは「お」が混じった「あ」、ʌ(ə)は、大体「あ」です。これで短い音の「あ」も区別がつきます。

これで、英語の母音の種が出来ました。後はこの種を育てるだけです。

この育てる方法にminimal pairによる学習を使います。minimal pairとは、vestとbestみたいな一音だけ違う単語を用いて、vとbの違いを学習する方法です。特に、二つの単語のどちらかをランダムに読んで、正解の単語を当てる練習は、アメリカ人にマンツーマンでテストしてもらうか、プログラミングで組む以外出来ないです。この部分をゲーム化すると結構面白く更に英語の発音も学べるゲームになると考えています。

この方法で、発音の区別がつくようなゲームを制作しようと思っています。

それでは今週の勉強を始めます。

<本文>

1.先週の続き

先週忘れていたんですが、MyThirdPerson_AnimBPクラスに作成した新しい変数、testの値がワープしても保持されるのかどうか、確認していなかったです。

f:id:kazuhironagai77:20200308232210p:plain

テストします。

f:id:kazuhironagai77:20200308232225p:plain

この状態からワープします。

f:id:kazuhironagai77:20200308232250p:plain

やはり、testの値は保持されなかったです。直します。

以下のように直しました。

f:id:kazuhironagai77:20200308232307p:plain

FieldPlayerBPクラスのevent graph内の、Event BeginPlayに、武器か、防具が装備されていたらMyThirdPerson_AnimBPクラスのTest変数の値をtrueに変更するコードを追加します。

テストします。

f:id:kazuhironagai77:20200308232323p:plain

ワープしても、モーションが武器持ちのまま保持されています。直りました。

2.ターン制戦闘の改良

2.1 今まで作成した部分の確認

もう、あんまり昔過ぎて、どんなものを作成したのか忘れてしまいました。のでまず確認します。

まず、Play画面にしてcommand, TestCombatをタイプする事で戦闘を開始します。

f:id:kazuhironagai77:20200308232359p:plain

戦闘を開始すると以下のWidgetが表示されます。

f:id:kazuhironagai77:20200308232417p:plain

今の状態で出来る選択は攻撃ボタンを押すだけなので。攻撃ボタンを押します。

f:id:kazuhironagai77:20200308232450p:plain

すると攻撃対象の敵が表示されます。今回の敵はゴブリン一体なので、ゴブリンと書かれたボタンが一個表示されました。

そのボタンをクリックします。

f:id:kazuhironagai77:20200308232508p:plain

ゴブリンに攻撃を行いました。その結果、ゴブリンのHPが15に減りました。その後、ゴブリンがプレイヤーに攻撃をして、プレイヤーが操っているキャラのHPが4に減りました。

双方の攻撃が終わったので、もう一度プレイヤーの攻撃の番です。

といっても攻撃しか出来ないのでゴブリンボタンを押します。

f:id:kazuhironagai77:20200308232524p:plain

前回と同じ様にゴブリンに攻撃を行いました。その後、ゴブリンがプレイヤーに攻撃をして、プレイヤーのHPが0になりゲームオーバーになりました。

2.2 ゴールの設定

まあ、はっきり言ってこのままでは、とても商業レベルのターン制の戦闘になるとは思えませんね。ゲームをあまりやらない私は、今のゲームのターン制の戦闘がどうなっているのか良く知らないですが。

この教科書で勉強を始めた時、最初に考えたのが、初期のドラゴンクエストぐらいの複雑さの戦闘システムは構築したいでした。ので、ここで、一回ゴールとなる戦闘システムのデザインが、初期のドラゴンクエストぐらいの複雑さになるように作成し直してみます。

f:id:kazuhironagai77:20200308232610p:plain

まず、戦闘画面になった時に、選択出来る項目が攻撃以外ありません。ここは、魔法、道具ぐらいは使用出来る様にしたいです。

攻撃を選択した後、何が起きているのか、良く分かりません。プレイヤーに分かるように、

「雲は攻撃を選択した。」

「雲は攻撃相手にゴブリンを選択した。」

「雲の攻撃!」

「ゴブリンは5のダメ―ジを受けた。」

「ゴブリンは攻撃を選択した。」

「ゴブリンは攻撃相手に雲を選択した。」

「ゴブリンの攻撃!」

「雲は15のダメ―ジを受けた。」

のような解説が欲しいです。

更に、画面いっぱいに戦闘情報を表示したいです。今の戦闘は、画面の左端で細々とやっていて何かとてもしょぼいです。

攻撃事にアニメーションやエフェクトも追加したいです。

これぐらいですか。最終的に完成させたい事は。

以下にまとめ直します。

  1. 戦闘中、選択出来る項目が攻撃だけでなく魔法、道具も追加したい。
  2. 戦闘中の行動を逐一、言葉で報告するようにしたい。
  3. 戦闘中のUIを改良して画面全体で表示するようにしたい。
  4. 戦闘中のアニメーションやエフェクトを追加したい。

これらをゴールとして戦闘システムを改善したいと思います。

2.3 戦闘システムのUE4C++コードの復習

戦闘システムを改良する前に、やっておかないといけない事が、戦闘エンジンの見直しです。去年作成しましたが、全くどう作ったのか覚えていません。これを復習します。

f:id:kazuhironagai77:20200308232705p:plain

まず、最初にRPGGameModeクラスのTestCombat()関数についてです。

f:id:kazuhironagai77:20200308232908p:plain

確か、このように設定する事で、プレイ中にコマンドで呼び出せるようになったはずです。

このページに詳しい解説が書かれていました。

f:id:kazuhironagai77:20200308233004p:plain

確かにその通りの説明がされていました。

2.3.1 TestCombat()関数の復習

それではTestCombat()関数の実装部を見ていきます。

f:id:kazuhironagai77:20200308233201p:plain

その最初の列のデータを引き抜いています。

f:id:kazuhironagai77:20200308233433p:plain

これですね。

次に以下のコードでプレイヤーのコントロールを停止してます。

f:id:kazuhironagai77:20200308233647p:plain

こんな細かい事をしていたのですね。確かに戦闘中にプレイヤーのキャラが動いたら大変ですからね。

遊びで上記のコードを外してコンパイルしたら、戦闘中でもプレイヤーのキャラが動きます。

f:id:kazuhironagai77:20200308233945p:plain

この辺はBPで書いても、UE4C++で書いても好みの問題でしょうね。どうもゲームの作成の仕方を教える教材と言うのは胡散臭い感じがしてしまって、前年まではC++で書かれているのを最低条件として選んでいたんです。流石にC++で嘘書いたらすぐにばれますから。この選択そのものは間違ってなかったと今でも思っています。まあ、どこまでUE4C++で書いて、どこからBPで書くかの目安も得る事が出来ましたし。ただ、そのせいで、BPで解説されている具体的なゲーム作成の技術(エフェクトの作成、3Dモデルのアニメーションの作成、レベルデザインなど)にちょっと疎くなってしまいました。その辺はこれから勉強していきます。

f:id:kazuhironagai77:20200308234111p:plain

ここで前述のEmeyInfoデータデーブルから引き出した最初のモンスターのデータからモンスターを作成してます。この方法だと一体のモンスターしか制作出来ません。確か、この戦闘エンジンは複数対複数の戦闘を行うのが売りだったはずです。何、敵1体でテストしているんでしょうか?

後で、この部分のコードを変更するか、戦闘は常に1対1で行うか決めないといけないですね。今見るとこの戦闘エンジン自体、非常に単純な作りなので、自分で最初から設計し直してもいいかもしれません。

f:id:kazuhironagai77:20200308234150p:plain

味方のパーティメンバーの構成がRPGGameInstanceクラスに保持されているので、RPGGameInstanceを呼び出していますね。そしてCombatEngineクラスから作成した変数を初期化しました。

以下にCombatEngineクラスのコンストラクターの一部を示します。

f:id:kazuhironagai77:20200308234220p:plain

うーん。テストで敵、味方共に一体しかいないのに、複数対複数のためのコードが実装されています。これはTestCombat()関数の敵、味方の人数を増やすか、一対一の対決にするためにこの部分のコードを変更する必要がありますね。変更する場合は、CombatEngine2として全部、自分で書き直した方が簡単かつ勉強になるかもしれません。

次のコードですが、tickTargetIndex変数に0を指定しています。

f:id:kazuhironagai77:20200308234247p:plain

このtickTargetIndex変数は確か、戦闘で攻撃する相手のCombatOrderの順序を表していたと思いますが、はっきりとは覚えていません。ちょっとだけ確認します。

以下にSelectNextCharacter()関数を示しますが、tickTargetIndex変数がCombatOrderの順序を表しています。ので記憶は正しかったです。

f:id:kazuhironagai77:20200308234459p:plain

次はSetPhase()関数にCPHASE_Decisionをパスします。

f:id:kazuhironagai77:20200308234530p:plain

SetPhase()関数では、以下に示すようにPhase関数にCPHASE_Decisionをセットし、TickTargetIndex変数を0にセットして、最後にSelectNextCharacter()関数を呼び出します。

f:id:kazuhironagai77:20200308234606p:plain

思い出してきました。たしか、このSetPhase()関数に来る前に既にtickTargetIndexを0にセットしているのに何でここでもう一度0にセットするんだ。といって調べたら、他の箇所でもSetPhase()関数を呼び出していたと言う記憶を。

調べたらTick()関数内にありました。

f:id:kazuhironagai77:20200308234704p:plain

それはともかくとして、このCombatEngineクラスのコンストラクターで何をやっているのかが分かりましたので以下にまとめます。

1. CombatOrder配列に敵、味方全員のメンバーを加えます。
2. tickTargetIndex変数に0 をセットします。
3. SetPhase()関数を実行します。CPHASE_Decisionargumentととしてパスされています。

     3.1 Phase変数にCPHASE_Decisionをセットします。

     3.2 SelectNextCharacter()関数を呼び出します。

          3.2.1 waitingForCharacter変数をfalseにセットします。

          3.2.2 tickTargetIndex変数を1にセットします。

          3.2.3 currentTickTarget変数にCombatOrder配列の一番最初のキャラをセットします。

4. CombatOrder配列にセットされている全てのキャラのcombatInstance変数にthis、つまりこのクラスのinstanceをセットします。

5. 2と3を繰り返します。

5は要らない気がしますね。後、このエンジンを作成した時に、味方全員の攻撃が終わってから生き残った敵が攻撃するのはオカシイと言っていたのを思い出しました。それについても後で直しましょう。

やっとCombatEngineクラスのコンストラクターが何を行っているのかが分かりました。

ではまた、GameModeクラスに戻ってTestCombat()関数のコードを追っていきます。

f:id:kazuhironagai77:20200308234932p:plain

UCombatUIWidgetを作成して表示してますね。ちなみに、UCombatUIWidgetは以下に示すようにUUserWidgetとIDecisionMakerを継承しています。

f:id:kazuhironagai77:20200308234954p:plain

うーん。どうやってこれを作成したか全く覚えていませんね。

後、このクラスがWidgetとして作成されるとしても、実際はこのクラスのBP派生クラスの

f:id:kazuhironagai77:20200308235014p:plain

が呼ばれるはずです。その辺の関係をどのように指定しているのかが分かりません。というかもう、UE4C++でUUserWidgetの派生クラスを作成する一般的なやり方すら覚えていません。その辺から復習します。

このページに簡単な説明がありました。

f:id:kazuhironagai77:20200308235253p:plain

あんまり、分かり易くないです。こっちの古いバージョンの方がわかりやすかったです。

f:id:kazuhironagai77:20200308235323p:plain

ここに書かれている事を簡単にまとめると、

1.プロジェクト名のヘッダーファイルに以下のコードを追加します。

f:id:kazuhironagai77:20200308235414p:plain

2.プルジェクト名.Build.csに以下のコードを追加します。

f:id:kazuhironagai77:20200308235717p:plain

3.コンパイルします。

4.C++ウィザードからUserWidgetを親クラスとして選択して、新しいクラスを作成します。

f:id:kazuhironagai77:20200308235917p:plain

だいたいこんな感じでした。

そう言えば大体こんな感じで作成した気がします。

BP派生クラスの指定は、GameModeクラスのBPでしていました。

f:id:kazuhironagai77:20200309000021p:plain

取りあえずの疑問は解決しました。

f:id:kazuhironagai77:20200309000120p:plain

この水色でハイライトされた部分です。

f:id:kazuhironagai77:20200309000145p:plain

次にマウスのカーソルを表示しています。UE4C++で書かれているので、難しく見えますが、BPでいつも行っているwidgetの表示方法と同じです。

次に、CombatUIInstance、つまり先程、UUserWidgetから作成したCombatUIWidgetクラスの関数、AddPlayerCharacterPannel()を呼び出しています。

f:id:kazuhironagai77:20200309000219p:plain

AddPlayerCharacterPannel()関数を見てみると、BlueprintImplementableEvent が使用されていて、BPで実装されているようです。

f:id:kazuhironagai77:20200309000254p:plain

CombatUIブループリントのAddPlayerCharacterPannelは以下のようになっていました。

f:id:kazuhironagai77:20200309000329p:plain

Player Party StatusはHorizontal Boxで以下に示すデザインの緑の部分です。

f:id:kazuhironagai77:20200309000355p:plain

Spawn Character Widget関数内は以下のようになっていました。

f:id:kazuhironagai77:20200309000422p:plain

見にくいのでちょっとだけ整理しました。

f:id:kazuhironagai77:20200309000439p:plain

それでは、コードを見ていきますが、まずCreateBaseCharacterCombatPanelWidgetノードでBaseCharacterCombatPanelWidgetを作成しています。実際はその派生クラスであるPlayerCharacterCombatPanelWidgetを作成しているはずです。このクラスにもう一度、BaseCharacterCombatPanelWidgetでキャストして、これ意味ないですと、UE4エディターに指摘されています。ここはPlayerCharacterCombatPanelWidgetでキャストするのが正しいですね。教科書で確認して見ます。

教科書でもBaseCharacterCombatPanelWidgetになっていました。ちなみに、3章のTurn-based combatのCombat UI with UMG内の図で説明されています。

うーん。取りあえずはこのままにしておきましょう。

次のノードで、BaseCharacterCombatPanelWidgetのCharacterTarget変数にGameCharacterをセットします。

f:id:kazuhironagai77:20200309000514p:plain

そして、最後のノードで、作成したPlayerCharacterCombatPanelWidget をPlayer Party Statusに追加します。

PlayerCharacterCombatPanelWidgetは、

f:id:kazuhironagai77:20200309000746p:plain

Canvas Panelを使用していて

f:id:kazuhironagai77:20200309000814p:plain

こんなデザインです。

これを何個も追加したら、PlayerCharacterCombatPanelWidgetが何枚も同じ個所に重なるような気がします。正直、良くは分かりませんが、この方法間違っている気がします。後で確認しましょう。

GameModeクラスのTestCombat()関数に戻ります。

f:id:kazuhironagai77:20200309000849p:plain

今度は、GameCharacterクラスの変数、decisionMakerにこのクラスのCombatUIInstanceをセットしています。

f:id:kazuhironagai77:20200309000915p:plain

CombatUIInstanceはCombatUIWidgetクラスですが、

f:id:kazuhironagai77:20200309000938p:plain

decisionMakerはDecisionMakerクラスです。なんでこれエラーにならないんでしょうか?

DecisionMaker.hを以下に示しますが、コードの最初のclass UGameCharacter;があります。

f:id:kazuhironagai77:20200309001118p:plain

うーん。こんな書き方知らない。兎に角、これがエラーにならない理由ですね。

思い出しました。単なるForward Declaration です。あれ、そうすると、何でGameCharacter.hをここでincludeしているでしょうか。

それはともかく、良く見たらUCombatUIWidgetクラスはDecisionMakerクラスの子クラスでもありました。

f:id:kazuhironagai77:20200309001204p:plain

ので

f:id:kazuhironagai77:20200309001256p:plain

はエラーになるわけなかったです。

最後に敵のモンスターにも同じ事をしています。

f:id:kazuhironagai77:20200309001319p:plain

同じなのでこれはスキップします。

これでTestCombat()関数は終わりです。

つまり以下の画面が表示された状態まで来たと言う事ですね。

f:id:kazuhironagai77:20200309001349p:plain

2.3.2 攻撃ボタンをクリックした後

今度は、攻撃ボタンを押したらどうなるか見てみましょう。

f:id:kazuhironagai77:20200309001440p:plain

まずTargetsのChildrenを全てクリアしています。

Targetsとは何を指しているのかと探して見たら、下の図の緑の部分のボックスでした。

f:id:kazuhironagai77:20200309001503p:plain

この中に攻撃対象のモンスターを表示する訳ですから、最初に空にするのは当然ですね。

次にこのグループノードに来ています。コメントから察するにモンスターが死んでいないか確認している様です。

f:id:kazuhironagai77:20200309001547p:plain

最初のGetCharacterTargetsノードですが、CombatUIWidgetクラスの関数でした。

f:id:kazuhironagai77:20200309001620p:plain

現在、CurretTargetのisPlayerはTrueなはずなのでenemyPartyを返すはずです。

所で、いつisPlayerをセットしましたっけ。調べます。

分かりました。キャラクターをGameCharacterクラスから作成する時に、味方のキャラの場合は以下に示す様にtrueにセット、敵のモンスターならfalseにセットします。

f:id:kazuhironagai77:20200309001709p:plain

しかし、そうすると、CombatUIWidgetクラスのcurrentTarget変数にいつ値をセットしたのでしょうか?

調べると、BeginMakeDecision()関数内でcurrentTarget変数に値をセットしています。

f:id:kazuhironagai77:20200309001735p:plain

しかしこの関数、今の所、呼んだ記憶がありません。BeginMakeDecision()関数がどこで呼ばれているか調べます。

何と、GameCharacterクラスのBeginMakeDecision()関数から呼ばれていました。

f:id:kazuhironagai77:20200309001804p:plain

ちょっと複雑なので整理します。

GameCharacterクラスの変数、decisionMakerは

f:id:kazuhironagai77:20200309001850p:plain

DecisionMakerクラスから作成しています。

しかし、前節のTestCombat() 関数の復習でみたように、TestCombat() 関数の最後で、decisionMakerにCombatUIInstanceをセットしています。

f:id:kazuhironagai77:20200309001929p:plain

そしてCombatUIInstance変数は、CombatUIWidgetクラスから作成されます。

f:id:kazuhironagai77:20200309002032p:plain

ので、GameCharacterクラスのBeginMakeDecision()関数がCombatUIWidgetクラスのcurrentTarget変数に値をセットしています。

このGameCharacterクラスのBeginMakeDecision()関数は、CombatEngineのTick関数から呼ばれていました。

f:id:kazuhironagai77:20200309002101p:plain

CombatEngineがGameModeクラスのTestCombat()関数内で初期化された瞬間からTick()関数は呼ばれるはずです。

これで一応、isPlayerをTrueでセットするまでの流れが遡れたはずです。

確認のためにTick()関数から戻って見ましょう。

f:id:kazuhironagai77:20200309002144p:plain

Phase変数は、CombatEngineクラスクラスから作成されたばかりなので、CPHASE_Decisionのはずです。同様の理由でwaitingForCharacterもfalseなはずです。

そうすると、CurrentTickTarget変数のBeginMakeDecision()関数が呼ばれます。

f:id:kazuhironagai77:20200309002218p:plain

f:id:kazuhironagai77:20200309002231p:plain

CurrnetTickTarget変数はGameCharacterクラスなので、GameCharacterクラスのBeginMakeDecision()関数を見ると、

f:id:kazuhironagai77:20200309002304p:plain

GameCharacterクラスのdecisionMaker変数、つまりDecisionMakerクラスの

f:id:kazuhironagai77:20200309002342p:plain

BeginMakeDecision()関数が呼ばれます。ここでcurrentTarget変数にGameCharacterがセットされました。

UE4C++ではそれぞれのクラスからインスタンスが作成された時点で、そのインスタンスのTick()関数が開始されていたんですね。この事は忘れていました。

攻撃ボタンを押した続きを見ていきましょう。

f:id:kazuhironagai77:20200309002449p:plain

まだ、ループの中なので、全ての死んでいない敵のモンスターに対応したAttackTargetOptionクラスが作成されています。と言っても敵のモンスターは一体しかいませんが。

以下にAttackTargetOptionのウィジェットのデザインを示します。

f:id:kazuhironagai77:20200309002515p:plain

f:id:kazuhironagai77:20200309002522p:plain

Canvas Panelを使用していても、配置はTargets内になるみたいですね。

ボタンに表示されるモンスターの名前は以下の方法で得ているみたいです。

f:id:kazuhironagai77:20200309002548p:plain

Target変数に既にGameCharacterクラスのインスタンスがセットされていますが、いつセットしたんでしょうか?

ぱっと調べた限りでは、いつ、Target変数にGameCharacterクラスのインスタンスをセットしたのか分かりませんでした。まあ、いいです。先に進みます。

f:id:kazuhironagai77:20200309002613p:plain

ここでTarget変数にGameCharacterクラスのインスタンスをセットしていました。流石に次のコードは調べませんでした。

でも、この方法で、ボタンの名前がセット出来るとなると、以下の部分ってTick()関数のように、常にチェックしていると言う事になりますよね。

f:id:kazuhironagai77:20200309002917p:plain

ボタンの名前なんか作成時に一回チェックすれば、十分な気もしますが、ボタンの名前の作成にも、結構なコストを払っているんですね。

f:id:kazuhironagai77:20200309002942p:plain

最後に画面に表示します。

実際の戦闘画面では、以下に示すように、ゴブリンと書かれたボタンが表示された所です。

f:id:kazuhironagai77:20200309003013p:plain

2.3.3 ゴブリンボタンをクリックした後

今度は、ゴブリンと書かれたボタンをクリックした後を、追います。

AttackTargetOptionのボタンをクリックした際には、以下のコードが実行されます。

f:id:kazuhironagai77:20200309003051p:plain

まず、最初のノードのAttackTargetはCombatUIWidgetクラスの関数です。以下にその関数の実装を示します。

f:id:kazuhironagai77:20200309003113p:plain

TestCombatActionクラスのインスタンス、actionをnewを使用して初期化しています。

f:id:kazuhironagai77:20200309003146p:plain

ずっとコードを追っていったらここで開放されていました。GameCharacterクラスは、コンストラクターもデストラクターもないクラスなので、メモリーの管理についても注意が必要です。

それはともかくとして、finishedDecision変数がtrueに変更されてこのAttackTarget()関数は終了します。

このFinishedDecision変数がどこで呼ばれているのか調べます。

どうやらFinishedDecision変数はMakeDecision()関数から呼ばれるみたいです。

f:id:kazuhironagai77:20200309003230p:plain

ありました。予想通りにCombatEngineクラスのTick()関数内にありました。

f:id:kazuhironagai77:20200309003254p:plain

DecisionMade変数がtrueにセットされました。

ので、Tick()関数内の以下のコードか実行されます。

f:id:kazuhironagai77:20200309003318p:plain

まず、SelectNextCharacter()関数呼ばれます。前にもSelectNextCharacter()関数のコードは確認しましたが、

f:id:kazuhironagai77:20200309003401p:plain

簡単に言えば次のキャラを選ぶ関数です。

全てのキャラの選択が終了したらtickTargetIndexが-1になりCombatEngineクラスのPhase変数がCPHASE_Actionに変更します。

しかし、実際には選択が必要なキャラは一人しかいないので、一回選択したら終わりです。この辺のコードが、実際に動くのを確認するためには、味方、敵が複数いるテストコードを書く必要があります。

以下にSetPhase()関数のコードを示します。

f:id:kazuhironagai77:20200309003438p:plain

Phase変数がCPHASE_Actionに変更される場合は、新しい関数が呼ばれる事はないみたいです。

それでは、Tick()関数内のCPHASE_Actionを見てみます。

f:id:kazuhironagai77:20200309003507p:plain

WaitingForCharacterはSelectNextCharacter()関数内でFalseにセットされていますので、まずCurrentTickTarget変数のBeginExecuteAction()関数が実行されるはずです。

CurrentTickTargetは、GameCharacterクラスから作成したので、以下のコードが実行されます。

f:id:kazuhironagai77:20200309003532p:plain

以下に示すようにcombatAction変数はCombatActionクラスから作成されています。

f:id:kazuhironagai77:20200309003636p:plain

この変数、上の方でNewで初期化されていて、ずっと追っていたやつじゃないですか。以下にその時のcombatAction変数かnewで初期化されている部分をもう一度示します。

f:id:kazuhironagai77:20200309003707p:plain

TestCombatActionクラスで初期化されているので、TestCombatActionクラスのBeginExecuteAction()関数を調べます。

f:id:kazuhironagai77:20200309003751p:plain

ようするに、敵が死んでいなかったら、敵のHPからダメージを引く事を実行している関数ですね。

Tick()関数内のCPHASE_Actionのコードに戻ります。

f:id:kazuhironagai77:20200309003818p:plain

GameCharacterクラスのExecuteAction()関数をみると、

f:id:kazuhironagai77:20200309003846p:plain

また、CombatActionが呼ばれています。

f:id:kazuhironagai77:20200309003909p:plain

パスした値が0より大きい場合はTrueが返ると言う事でしょうか。

Trueが返った場合、

f:id:kazuhironagai77:20200309003955p:plain

を実行して終わりです。

これで終わりかと思いましたら、Attack Targetノードが終了した後に、Hide Action Panelノードが呼ばれているのを忘れていました。

f:id:kazuhironagai77:20200309004038p:plain

この関数はどうも、CombatUIWidgetクラスのBPで作成された関数のようで、UE4C++のCombatUIWidgetクラスには存在しない関数です。

以下に示すようにやはりBP上で作られていました。

f:id:kazuhironagai77:20200309004105p:plain

実際のコードは以下の様になっていました。

f:id:kazuhironagai77:20200309004354p:plain

CharacterActions変数はCanvas Panelから作成されていて、実際には以下に示す図の緑の部分の事です。

f:id:kazuhironagai77:20200309004423p:plain

この部分が見えなくなるようです。

2.3.4 味方が全滅した場合

 Tick()関数内のSwtichの部分のコードが終了すると、以下のコードが実行されます。

f:id:kazuhironagai77:20200309004454p:plain

この部分は味方が全滅しているかを調べています。もし全滅した場合は、Phase変数にCPHASE_GameOverをセットします。

f:id:kazuhironagai77:20200309004522p:plain

今度は、Phase変数にCPHASE_GameOverがセットされた場合をみてみましょう。

まず、Tick()関数でTrueが返されます。

f:id:kazuhironagai77:20200309004608p:plain

Tick()関数でtrueが返るとどうなるんでしょう。Tick()関数が終了するでしたっけ?

gamemodeクラスのTick()関数を見たら全て分かりました。

以下にgamemodeクラスのTick()関数の最初の部分を示してそれを使用しながら説明します。

f:id:kazuhironagai77:20200309004750p:plain

CurrentCombatInstanceはCombatEngineクラスから作成されたインスタンスです。それのTick()関数は勝手に動く訳ではなかったです。このGameModeクラスのTick()関数によって動かされていました。

今、CombatEngineクラスからTrueが返されたのでif節内が実行されます。

If節内は更に2つに分けられています。以下にphaseがCPHASE_GameOver、つまり今回の場合を示します。

f:id:kazuhironagai77:20200309004847p:plain

RPGGameInstanceクラスのPrepareReset()関数が呼ばれます。

f:id:kazuhironagai77:20200309004918p:plain

ゲームオーバーなので、ゲームを終了するための準備をしていると考えられますが、何でPartyMembersからEmpty()関数を呼ぶ必要があるか分かりません。

f:id:kazuhironagai77:20200309005041p:plain

PartyMembersはUPROPERTY()で修飾されているので、時期が来たら勝手に開放してくれると思っていました。それともTArrayの場合は、Empty()関数を使用してそれぞれの要素を開放しないと、メモリーリークになってしまうんでしょうか?

ここにTArrayクラスのEmpty()関数についての解説がありました。

f:id:kazuhironagai77:20200309005218p:plain

まあ、分かり切った事しか解説していませんね。

今回は、この疑問は置いておきますが、後で必ず調べます。

後はWidgetを表示するためのコードですね。表示するwidgetはRPGGameModeの以下の部分で指定されています。

f:id:kazuhironagai77:20200309005253p:plain

実際のゲームオーバーのwidget魔改造してしまった後なので、ここではスキップします。

f:id:kazuhironagai77:20200309005312p:plain

2.3.5敵が全滅した場合

もう、大体の流れは分かりましたが、最後に敵が全滅した場合も見ておきましょう。

CombatEngineクラスのTick()関数内で敵が全滅した場合かどうかをチェックしている部分を示しました。

f:id:kazuhironagai77:20200309005345p:plain

何か最初の部分のGoldと経験値の合計の計算は全員死んでから行えばいいような気もしますが、もし敵が全滅していた場合に数え直すよりは速いんでしょうか。

この部分のコードでPhase変数にCPHASE_Victoryがセットされます。

Phase変数にCPHASE_Victoryがセットされたことにより、RPGGameModeクラスのtick()関数内の以下の部分のコードが実行されます。

f:id:kazuhironagai77:20200309005416p:plain

ああ、このレベルが上がった時に、キャラのそれぞれのパラメーターに1をここで足したんで、武器を外したら元に戻ってしまう、バグを生成している箇所じゃないですか。

まあ、でも全体の流れは分かりました。

3.まとめと感想

戦闘システムをどのように作成したのか、全く忘れてしまっていたので、良い復習になりました。

復習して思ったのですが、独自の戦闘エンジンを自分で作成し直した方がいい気がします。

来週はもう少しこの戦闘エンジンについて勉強して独自の戦闘エンジンの設計にかかりたいと思います。

今週は、突然、Third partyのクッキーが使えない。とか表示が出て来て、ブログの編集が出来なくなってしまいました。適当にいじっていたら突然、編集出来るようになったので、急遽、このブログを上げる事にしました。

まったくはてなブログについて勉強していないので、ちょっとでも変わった事が起きると対応出来ないです。来週は、ちょっとだけはてなブログについても勉強します。