UE4の勉強記録

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

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

f:id:kazuhironagai77:20200323002658p:plain

<前文>

ここには、ゲームデザインについてしか書かないと決めたのに、全く守っていません。先週までは、それでも少しは関係する話題を書いていたのですが、とうとう今週は、全く関係ないAIについて書きます。もう私はアカデミックな分野との関係がなくなってしまったので直接、AIの専門家から話を聞いたりする事は出来ないので、あくまでも第三者的な感想でしかありませんが、AIが人間並みの運転が出来る日が来る可能性があるかもと思うようになるビデオを見たので書く事にしました。

何回もこのブログには書いていますが、私はAIに対して非常に懐疑的で基本的にAIによって世界の仕組みの何かが変わるとは思っていません。良く「AIで通訳が出来る様に成るから英語の勉強は無駄だ。」と言う意見がありますが、全く信じていません。のでAIによる自動運転が可能であるとも思っていませんでした。だから学生の時もAI関係の勉強は全くしませんでした。ゲームの作成で敵のキャラの動きを設定するためにAIを勉強しなければならず、artificial intelligence a modern approach とAI for gamesを買って所々読んだだけです。Machine learningやDeep learningについては、何で注目されているのかぐらいしか知りません。

その私がAIで自動運転が出来る様にならないと思っていたのかと言うと、人間は頭の中で3次元空間を作り出して、目で見た情報をその頭の中の3次元に配置する事で空間を認識しているからです。例えば自転車を見たら、頭の中で今までの経験から自転車を3D化して、その自転車の大きさから、自分からの距離を想像して頭の中の3D空間にその自転車を配置してます。その3次元空間を元に判断しているからです。AIが現在どのような方法で運転するための情報を判断しているのかは知りませんが、この基本的な部分は、同じ様にしなければ人間並みの運転は出来る様にはならないでしょう。と言うのが私のAIによる自動運転に対する確信です。

その程度の知識しかない人の個人的な見解なんですがThis Neural Network Creates 3D Objects From Your Photosを見てびっくりしました。AIで写真に写っている物体を3Dに変換する事が可能になったようなんです。AIによる自動運転が現在どうなっているのか全く知りませんが、素人目には、この技術を使ってAIの自動運転を作成したら、凄い良い結果になりそうに思えます。

それだけではなくAIの自動運転が3D graphicsの専門家が必要な新しい分野になってくれるかもしれません。

今、産業界で絶対に3Dが必要な分野は多くありません。圧倒的にゲーム産業と映像産業、後は医療と科学シミュレーションの分野くらいです。最近3Dで地図を作成する話もききましたが微々たるものでしょう。UE4では建築関係も少しはあるようですがこれも微々たるものだと思います。

少し昔に成りますが、WebGLが主なブラウザーに標準装備されるようになった時、私は全てのウェブサイトが部分的にでも3D graphicsが使用される未来を予測しました。3D graphicsの専門家の需要が、ウェブデザイナー並みになる世界を夢見てました。現実は全く採用されないまま今に至っています。ので私の予測はあまり当たりませんが、AIの分野にも3D graphicsの専門家が必要になる日が来るといいなと思いました。

<本文>

先々週、先週とCombatEngineの実際のコードを見て仕組みを大体理解したので、今週は教科書のCombatEngineの箇所を読み直して著者の説明をもう一度、噛み砕いて理解しようと思います。

1.ターン制の戦闘システム

教科書の3章4節がまるまるComgatEngineの作成なので、ここから読んでいきます。1. CombatEngineクラス

戦闘システムは、行動の決定と行動の二つに分けられると最初から説明されています。

なるほど。と次を読み進めますが、いきなりcombatEngineクラスの作成を初めてcombatEngine.hのコードが紹介されています。

その後で、combatEngineクラスの説明です。

この説明がCombatEngineの実際のコードを理解した後だと、かなり読み応えがありました。

  • CombatEngineクラスは敵と出会った時に作成され、戦闘が終わった時deleteされるクラスである。

当たり前の設計ですが、この基本的な考えに基づいてCombatEngineクラスは作成されたから、勝利した後や全滅した後の処理はGameModeクラスのTick()関数内で行っているのですね。

以下に示すように。GamModeクラスのTick()関数内で、戦闘が終わった後で、CombatEngineクラスが使用されているのは、戦闘に勝利した後で、獲得したゴールドと経験値をGameInstanceの変数に追加する所だけです。

f:id:kazuhironagai77:20200323003336p:plain

そしてそれが終了するとすぐにdeleteされています。

f:id:kazuhironagai77:20200323003411p:plain

次にcombatEngineクラスの変数についての解説がありました。何でTArrayにUPROPERTYを使用しないのかなどの説明があるとうれしかったのですが、それはなかったです。

後、combatEngineクラスのTick()関数はGameModeクラスのTick()関数から毎回呼び出すとしっかり書かれていました。普通のクラスのTick()関数が勝手に動く事はやっぱりないですね。

その後で、currentTickTarget変数とtickTargetIndex変数の役割について説明しています。CombatOrder変数には、戦闘に参加する総てのキャラが要素として入っています。Decision時にそのCombatOrder変数内のすべてのキャラがどのような行動を取るのかを決定する必要があります。その時に、現在、CombatOrder変数内のどのキャラの行動を決定しているのかを示す変数が必要で、それをcurrentTickTarget変数とtickTargetIndex変数が担当しています。

そしてTick()関数内の実装部の説明が始まります。

以下に示すように教科書の3章4節の最初の時点では、コードはまだ実装してなく、ここでキャラクターの行動を決定するように要求する。とだけ書かれています。

f:id:kazuhironagai77:20200323003528p:plain

この部分は確かに、キャラクターの行動を決定するように要求する個所です。

しかしこの部分の実装は以下のようになっているのですが、何回、コードを読んでもどこでキャラの行動をプレイヤーに聞いているのかが分からないです。

f:id:kazuhironagai77:20200323003558p:plain

次にキャラの行動が決定したら次のキャラを選択します。と説明されています。

f:id:kazuhironagai77:20200323003624p:plain

実際のコードではSelectNextCharacter()関数が使用されています。この関数、GameCharacterクラスから呼ばれていると思っていましたけど、実際はCombatEngineクラスのメンバー関数だったです。

このSelectNextCharacter()関数、やっている事は極めて単純で、次のキャラをcombatantOrder変数からtickTargetIndex変数の値を元にして選びcurrentTickTarget変数にセットするだけなんですが、forループ内でreturnを使用しているので、一見、combatantOrder変数の全部の要素をチェックしているかのような錯覚を覚え、コードの意味を勘違いして読んでしまったりします。更に、色々な所で呼ばれるので、実質、CombatEngineクラスの最も重要なメンバー関数です。

後は、大体当たり前の事を説明しているか、まだコードそのものは実装していないので、TODOリストが書かれているかのどちらかでしたが、一つ気になる事はcombatantOrder変数の全ての要素をチェックした時に、tickTargetIndex変数に-1をセットする事をこの時点て既に決定してます。

f:id:kazuhironagai77:20200323003724p:plain

この事に対しての説明がなくて、まるで後出しジャンケンみたいです。(最後まで読んだら説明がありました。でも少しだけで後出しジャンケン感はぬぐえません。)

SelectNextCharacter()関数の実装についての解説もありました。この時点では、

f:id:kazuhironagai77:20200323003753p:plain

はまだ実装されていません。この変数が何時実装されてその目的について教科書でどのように解説されているのかも興味がある所です。

ここで、教科書は突然、キャラが活動のための意思決定の機能も活動するための関数もないのでこれから作成しましょう。と言い出します。これらの機能をGameCharacterクラスに持たせるのは、このCombatEngineの設計の最大の謎なんですが、この教科書は全くそれについて解説していませんね。当たり前と言う感じで説明しています。

2. GameCharacterクラス

GameCharacterクラスは、キャラの特性、例えば、HP、MPなどを管理するクラスです。何故このクラスが戦闘システムに関係するのか、残念ながらその説明は教科書には全くなかったです。

しかし教科書では、行動を決定するためと行動のためにそれぞれ2つ、計4つの関数が必要であると解説されていて、その話は重要なので以下にまとめます。

f:id:kazuhironagai77:20200323003905p:plain

行動を決定するために必要とされる行為は2つあり、それぞれBeginMakeDecision()関数と、MakeDecision()関数が担当します。BeginMakeDecision()関数は、キャラが行動を決定するための思考を開始した時の関数で、2番目の関数はキャラが行動を決定した時に返し値がTrueになる関数です。

行動を示す2つの関数を以下に示します。

f:id:kazuhironagai77:20200323004103p:plain

最初の関数、BeginExecuteAction()関数は活動(攻撃)の開始を示す関数で、2番目の関数、ExecuteAction()関数は活動(攻撃)が終了した時にTrueを返す関数です。

この時点における実装も確認します。

f:id:kazuhironagai77:20200323004149p:plain

TestDelayTimer変数を1にセットしました。これがキャラが行動を決定するための思考を開始した印になるみたいです。

ちなみに、以下に示すように、最終段階の実装では、TestDelayTimer変数は外されています。

f:id:kazuhironagai77:20200323004231p:plain

この辺の改良がどのような経過で行われたのかの復習も兼ねて教科書を読んでいきます。

次はMakeDecision()関数の実装部です。

f:id:kazuhironagai77:20200323004301p:plain

パスされたフロート値が0以上の値だったら、trueが返される仕組みになっています。

以下に示したように、MakeDecision()関数も最終段階における実装はかなり違います。

f:id:kazuhironagai77:20200323004342p:plain

今度は、活動(攻撃)を管理する2つの関数、BeginExecuteAction()関数とExecuteAction()関数の実装を見てみます。

まず、BeginExecuteAction()関数ですが、こちらもTestDelayTimerに1をセットするだけです。

f:id:kazuhironagai77:20200323004412p:plain

最終段階では、以下に示すようにBeginMakeDecision()関数と似たような形になっています。

f:id:kazuhironagai77:20200323004447p:plain

これらの関数の実装が最終段階に至るまでに、新たなインターフェイスであるIDecisionMakerとICombatActionクラス、更にそれらの派生クラスであるTestDecisionMakerとTestCombatActionが作成されています。

これらのクラスの目的と何故、新しいクラスを作成する必要があったのかは、戦闘システムの開発の根幹にかかわってくる大切な部分になるはずです。そこに注目して先に行きましょう。

4つの関数の最後の関数であるExecuteAction()関数についてです。

f:id:kazuhironagai77:20200323004543p:plain

実装内容は全くMakeDecision()関数と同じです。実装の最終段階では、

f:id:kazuhironagai77:20200323004604p:plain

となっています。パッと見ると大分実装が変わっていますが、CombatActionをdeleteしている以外は、CombatActionクラスの関数を呼び出していたりして予想の通りです。この関数がどのようにこのような最終段階に発展したのかも注目して教科書を読んでいきましょう。

f:id:kazuhironagai77:20200323004632p:plain

その後、教科書では、Class ComatEnigneをGameCharacter.hに追加します。とあります。Forward Declaration でしょう。と対して気にも留めずに読んでいたら、以下に示すようにGameCharacter.hを既にCombatEngineのヘッダーファイルでincludeしているので、

f:id:kazuhironagai77:20200323004712p:plain

GameCharacter.hでComatEnigne.hをincludeするとcircular dependencyになってエラーになってしまうからと説明されていました。

あー。

circular dependencyについてすっかり忘れていました。

大学の研究室に配属されて、渡された最初の課題を完成させるのにこのcircular dependencyが必要だったのですが、なんせその時の私、ほとんどC++でコードを書いた事がなくてcircular dependencyを知らなかったんです。もう大変な思いをして教授との打ち合わせの日の朝に、circular dependencyのエラーの避け方を知って、やっと解けたと言う非常に因縁のある物なんですが、すっかり忘れていました。

3. CombatEngineクラス、もう一度

今度はCombatEngineクラスからGameCharacterクラスのDecisionMaker()関数とActionCombat()関数を呼び出します。

そのために、まずbool変数であるwaitingForCharacterを作成します。

f:id:kazuhironagai77:20200323004824p:plain

教科書によると、この変数の目的はBeginMakeDecision()関数とMakeDecision()関数をswitchするためなどに使用するそうです。

そう言われれば、確かにGameCharacterクラスで、行動を決定するために必要とされる関数を二つに分けたのですが、その二つのどちらかを選ぶためのboolean変数は必要ですね。

f:id:kazuhironagai77:20200323005013p:plain

それでコードがこんな風に追加されました。前の状態はどうだったのかと思って確認したら、

f:id:kazuhironagai77:20200323005229p:plain

単なるTODOでした。

最終段階の実装も

f:id:kazuhironagai77:20200323005254p:plain

同じですね。

この部分のコードは理解出来るのですが次が今でも混乱しています。

f:id:kazuhironagai77:20200323005353p:plain

これって、必ずTrueになりますよね。

f:id:kazuhironagai77:20200323005424p:plain

あ。

これはかなり恥ずかしい間違いをしていました。TestDelayTimer = -DeltaSecondだとずっと思っていました。何でこんな勘違いしていたんでしょうか?

testDelayTimer = testDelayTimer – DeltaSecond

が正しい解釈ですね。

うーん。これはかなり恥ずかしい。

となると1秒たったらdecisionMade変数がtrueになると言う事ですね。分かりました。

教科書ではその後もコードの解説が続きますが次のキャラを選ぶ時に、waitingForChracter変数をfalseに戻していますね。

f:id:kazuhironagai77:20200323005518p:plain

そして全てのキャラの行動が決定したらPhase変数をActionに変更します。

f:id:kazuhironagai77:20200323005541p:plain

因みに、if節に一行しかない場合も{}は必ずつける。つけないとハックされる。と言う話を私は聞いた事があるのですが。そういうわけで私は上記のような書き方を普段もしています。

PhaseがActionの時の実装は、特に書き記す内容はないのでスキップします。

次にSelectNextCharacter()関数の実装を改良してます。

と言っても前の実装を覚えていないので、

f:id:kazuhironagai77:20200323005624p:plain

意外の部分の変更は正直分かりません。

最後に、全てのキャラはCombatEngineに対するポインターを持っているべきなので、GameCharacterクラスに、CombatEngineクラスの変数、combatInstanceを作成します。

そしてCombatEngineクラスのコンストラクターから戦闘に参加する総てのキャラのcombatInstanceにこのCombatEngineをセットします。

f:id:kazuhironagai77:20200323005715p:plain

この辺のやり方は完全に理解していますが、何でこんなやり方にしたのかは全然分かりません。その説明はあるんでしょうか?

後、デストラクターで味方のパーティは開放しないのですが、その理由についての説明があるとちょっとうれしいです。

続きを読むとなんの説明もなく次の話題に進みました。

次はGameModeクラスにCombatEngineクラスの変数とemeyPartyの変数を追加するそうです。

4. RPGGameModeクラス

こんな感じで追加してます。

f:id:kazuhironagai77:20200323005855p:plain

解説に、

f:id:kazuhironagai77:20200323005913p:plain

と書かれているのですが、実際のRPGGameModeのヘッダーファイルでは、UPROPERTYはついていません。

後、Override the Tick functionの部分はGameModeクラスからのオーバーライドの事だと思います。

f:id:kazuhironagai77:20200323005938p:plain

更に読み進めても。以下の部分で、

f:id:kazuhironagai77:20200323010013p:plain

UPROPERTYが付いているから、ポインターをクリアしてenemy party listをclearすればGCに綺麗してもらえます。何故ならUPROPERTYで宣言されていますので。とありますね。ここではっきりとlistと書かれているので、enemyPartyの事ですね。

今回はしませんが、後でここにUPROPERTYが必要かどうかの検討をします。

これで後は、敵のモンスターをGameCharacterクラスから作成出来る様にすれば、CombatEngineは大体完成です。

5. GameCharacterクラス、もう一度

ここから、EnemyInfoデータから敵のモンスターを作成するためのコンストラクターをGameCharacterクラスに追加します。

特に、記載する内容はないのでスキップします。

6. TestCombat()関数の作成

RPGGameModeクラス内にTestCombat()関数を作成します。

ここも特に、記載する内容はないのでスキップします。

7. ここまでのまとめ

これで3章4節の1が終了します。のでここまでのまとめとします。教科書に特別、有効な情報はなかったですね。Circular dependencyぐらいでしょうか。呼んではっとしたのは。このまま続きを読んでいきますが、CombatEngineを自作するために必要な情報は、教科書からは得られなさそうです。

2.ターン制の戦闘システム パート2

ここから教科書は4章の3節の1,2,3となっているのでパート2に分けました。

1.アクションのパフォーマンス

ここからCombatActionインターフェイスの作成になります。

CombatEngineにおける設計の中で私が最も気になる事の一つである、戦闘時の活動をGameCharacterのBeginExecuteAction()関数とExecuteAction()関数内で実装せずに、敢えてCombatActionインターフェイスを作成し、その派生クラスであるTesCombatActionクラスで実装している理由についての説明があるのでしょうか?

最初に書かれていました。

f:id:kazuhironagai77:20200323010157p:plain

  • (全ての)キャラ達が(それぞれの)活動をする事が可能になるためには、全ての戦闘時の活動を一個の共通のインターフェイスで表せるようになるまで煮詰めなければなりません。
  • このインターフェイス作成のための優れた開始場所は、我々が既に作成しているーそれは、キャラのBeginExecuteAction()関数とExecuteAction()関数です。

分かりました。以下のように解釈しました。

もともとこのゲームは、沢山のキャラ、味方なら色々な職業のキャラ、敵なら色々な種族のモンスターが、混じって戦う設定でした。ので戦闘時に、戦闘を行う職業のキャラやモンスターだけでなく、魔法を使うキャラや戦闘のサポートをするキャラも将来的にはサポートする予定でcombat Engineは設計されていたんです。

そのためにはすべてのキャラの全ての戦闘時における活動を、一括して管理できる親クラスが必要なんです。これはオブジェクト指向におけるソフトウェアデザインでは当然です。それは本来ならば、親クラスを作成するのですが、今回はGameCharacterクラスにあるBeginExecuteAction()関数とExecuteAction()関数の実装を担当するための関数集が必要なので、インターフェイスにしたんです。

そして実際の実装には、そのインターフェイスからの派生クラスが担当する事になる訳です。これの一例がTestCombatActionクラスです。このクラスが一個しかないためにこの派生クラスにオブジェクト指向的に、どんな役割が与えられているのかが良く分からなかったんです。もし、この派生クラスの名前がWarriorCombatActionとか、WizardCombatActionとかの名前なら、ああこのクラスは職業が戦士のキャラの時に呼ばれるんだ、とかこのクラスは種族が魔導士の時に呼ばれるんだ。と作成した時に理解出来たはずです。

多分、この解釈であっているでしょう。

その後、教科書では、CombatActionインターフェイスの作成を行います。

生のC++におけるインターフェイスの作成方法がちょっとうろ覚えなのですが、大体の流れとしては生のC++におけるインターフェイスとしてCombatActionインターフェイスは作成されているように思えました。

次にその派生クラスとして、TestCombatActionクラスの作成を行います。

以下に実装内容を示しますが、GameCharacterクラスにあるBeginExecuteAction()関数とExecuteAction()関数の実装を写しただけですね。

f:id:kazuhironagai77:20200323010345p:plain

教科書には、CombatActionインターフェイスから派生したクラスがCombat Engine内で動くのか試すためのテストのためなので、GameCharacterクラスにあるBeginExecuteAction()関数とExecuteAction()関数の実装をそのまま写したと説明されていました。

今度は、GameCharacterクラスを直します。

GameCharacterクラスからCombatActionインターフェイスの派生クラスであるTestCombatActionクラスを呼び出し、そのインスタンスをGameCharacterクラスのBeginExecuteAction()関数とExecuteAction()関数で使用します。

ここで、私が分からないのが、

f:id:kazuhironagai77:20200323010415p:plain

をしているにもかかわらず、

f:id:kazuhironagai77:20200323010435p:plain

Forward Declarationをしている所です。

GameCharacterのヘッダーファイルで同様にForward Declarationをしている

f:id:kazuhironagai77:20200323010502p:plain

はCircular dependencyを避けるためと分かりましたが、こちらのCombatActionはincludeしているにも関わらず、Forward DeclarationをしていますのでCircular dependencyを避けるためではありません。

教科書にはCombatActionをincludeして更にclass ICombatActionを追加しましょう。としか説明されていません。

ここまで、書いて思ったんですが、CombatActionクラスそのものはインターフェイスで実装出来るクラスじゃないですね。その場合、生のC++でもこういう風に書かないといけないのかもしれないと。

調べます。

サラッとググっただけでは出て来ませんでした。

これも後で調べます。

この後、GameCharacterクラスの戦闘に関係する全ての関数、BeginMakeDecision()関数、MakeDecision()関数、BeginExecuteAction()関数、そしてExecuteAction()関数の実装部を書き換えていきます。

MakeDecision()関数の実装部が、

f:id:kazuhironagai77:20200323010549p:plain

にここでなっています。

しかし、教科書にその事に関しての解説はありませんでした。

この節の最後は、以下のようにまとめられていました。

f:id:kazuhironagai77:20200323010616p:plain

私、今もってプレイヤーの意思決定が、どのようにCombat Engineに伝えられているのかが分かっていません。その部分を次の節で作成するようですね。

2.Making Decision 決定をする

以下に示す、GameCharacterクラスの関数、BeginMakeDecision()関数とMakeDecision()関数の実装を担当するインターフェイスをまず作成します。

f:id:kazuhironagai77:20200323010701p:plain

これはCombatActionインターフェイスを作成したのと同じ理由です。

この辺のやり方は、CombatActionインターフェイスを作成した時と全く同じなので、スキップします。

全部、読みましたが、この節では、前節と同じ事を、GameCharacterクラスの関数、BeginMakeDecision()関数とMakeDecision()関数の実装に対して行っただけのようです。次の節に期待しましょう。

3.Targetを選定する

来ました。Combat Engineを実行すると攻撃対象のモンスターを選択する画面が表示されますが、それをどのように作成したのか覚えていませんし、コードを見直しても良く分かりません。どうやらその部分のコードはこの節で作成されているみたいです。

読んでみます。

f:id:kazuhironagai77:20200323010730p:plain

あれ、これ読むと単に敵のグループの最初のヤツをターゲットに選んでいますね。これって私がコードを読んで理解した内容と同じですね。この時点では、まだ攻撃対象のモンスターを選択する画面が表示されるようには作成していないみたいですね。

このSelectTarget()関数に対して一つだけ疑問があります。何でGameCharacterクラス内のメンバー関数なんでしょう。

この関数を使用する必要があるのは、TestCombatActionクラスです。TestCombatActionクラス内でこの関数を実装する方が、自然な気がします。

これも、色々な職業や種族が存在してそれぞれに対応したCombatActionクラスを制作しないといけないと考えると理由が分かります。そうした場合、インターフェイスであるCombatActionクラスに作成する必要がありますが、そこで実装する事は出来ません。のでGameCharacterクラスで実装したと考えると納得出来ます。

現状、SelectTarget()関数に対する質問はこれだけですので次に行きます。

4.ダメージを扱う

ダメージを計算するコードを実装させているだけなのでスキップします。

5.戦闘時のUIをUMGで作成する

以下に示したCombatUIWidgetクラスの作成をします。

f:id:kazuhironagai77:20200323010934p:plain

あれ、こんなクラス、UE4C++で作成した事すっかり忘れていました。

今週は、ちょっと短いですが、ここで一端中止して、来週、このクラスの復習からやります。

3.感想とまとめ

今週は、少し量が少ないですが、教科書を久しぶりに読み直してCombat Engineについて学び直しました。久しぶりにC++のコードを読んだら間違えて読んだりしてました。

Circular dependencyは分かりますが、あるインターフェイスをあるクラス内で変数として宣言する場合、そのインターフェイスをインクルードするだけでなくforward declarationしなければならないのかどうかは分かりません。これは来週調べます。

UIのUE4C++のクラスはすっかり忘れていました。これも来週勉強します。

今週は以上になります。