<前文>
の3章3節の4、ターン制の戦闘(turn based combat)を勉強します。Basedは制と訳せば良かったんですね。ウーン。勉強になりました。
<本文>
<目的>
3章3節の4、ターン制の戦闘(turn based combat)を勉強します。3章3節の4は大変長いだけでなく大切な小節なのでゆっくり勉強しようと思います。
<方法と結果>
<Step.0>
前回まで使用していたCh2をそのまま今回も使用します。念のためビルドしてみると普通に成功しました。のでこのままで行きます。
<Step.1>
教科書の最初にターン制のゲームはプレイヤーの行動を決定するDecisionとその決定に基づいて実際の活動をするActionの二つがありますと説明されています。本当に分かり易いです。アマゾンにおけるこの本の評価は5段階で1とか2だったんですが私は最初からその評価はオカシイと感じていました。と言うかひょっとするとこの本は凄い良い本じゃないのかなとすら思っていました。
教科書の指導通りにやって行きます。まずCombatフォルダーをCh2フォルダー内に作成します。
そこにCombatEngineクラスを作成します。
あれ?UE4エディターの方にはフォルダーが出来てない。仕方ないのでVisual studio のCombatフォルダーは消して、UE4エディターから作り直します。
作れない…。うーん。サンプルコードを見てみましょう。BuildingAnRPGWithUnreal4X_Code\Chapter3\Source\RPGにしっかりとCombatがありました。
そういえばDataフォルダーも前回作成しなければならなかったのですが4.22だと必ずPrivateとPublicを分けなければならないので却って複雑になるので止めたのでした。
サンプルコードをUE4エディターから開いて更に細かく調べてみます。UE4は4.12、VSは2015です。
やっぱりCombatフォルダーはサンプルコードでもここには表示されていませんでした。しかしVSの方では
しっかりフォルダーが表示されています。と言う事はVSからフォルダーを作成すれば良いと考えられます。
後、サンプルコードのSourceファイルにはPublicとPrivateフォルダーがないのでこれについてもここで調べてみます。
サンプルコードのGameCharacterクラスのヘッダーファイルをみると明らかにPublic で作成されていますが、Publicフォルダーはありませんでした。
うーん。PublicとPrivateフォルダーを作成しない何か特別な理由があるのでしょうか?
UE4.12はC++クラスを作成する時にPublicかPrivateを選択しない選択が出来ますが、
UE4.22はC++クラスを作成する時にPublicかPrivateか必ず選択しないといけないので
ここは直せませんね。
それではcombatフォルダーをVSから作成しましょう。
ここで思い出したのですがVSは普段はバーチャルなフォルダーを表示していて実際のフォルダーを表示するには何かしなければならなかったはずです。
確かこのボタンだったはずです。
名前はsolutions and foldersとなっています。これをクリックしてみます。Folder view になりました。そしてCombatフォルダーを追加します。
もう一度クリックしてSolution viewに戻すと
Combatフォルダーは消えていますが、
実際のフォルダー内にはしっかり作成されています。
UE4C++ウィザードから空のC++を選択してPathをCombatに指定します。
*4.22でもPathを別なファイルに指定した後はPublicとPrivateを指定しなくて良いみたいですね。
コンパイルが終わってVSを見てみると
CombatEngineクラスがCombatフォルダー内に出来ていました。更にバーチャルな階層を示す普段の状態に戻しても
Combatフィルター内にCombatEngineクラスが配置されています。
最後に確認のためにもう一度サンプルコードを起動します。
UE4エディター上ではCombatフォルダー並びにCombatEngineクラスは表示されていません。
代わりに、VS上では
バーチャルなフォルダーを示す普段のSolution Explore内での階層でもSource->RPG->Combatと表示されその中にCombatEngineクラスがあり
実際のフォルダーの階層を表した場合でもSource->RPG->Combatと表示されその中にCombatEngineクラスがあります。
これを私の作成したCh2と比較してみると、UE4エディター上ではサンプルコードと同じようにCombatフォルダー並びにCombatEngineクラスは表示されていません。
更にVS上では
バーチャルなフォルダーを示す普段のSolution Explore内での階層でもSource->RPG->Combatと表示されその中にCombatEngineクラスがあり
実際のフォルダーの階層を表した場合でもSource->RPG->Combatと表示されその中にCombatEngineクラスがあります。全くサンプルコードと同じ構成でCombatフォルダーの追加とCombatEngineクラスの作成を行う事が出来ました。
以下のようにコードを追加しました。
教科書にこのコードの解説が書かれているのでそれを簡潔にまとめます。(教科書の内容をまとめた部分は水色で表示します。)
- このcombat engineはエンカウンターが発生した時に割り当てられ戦闘が終わった時に消去されるようにデザインされています。
ウーン。なるほど。Combat engine は戦闘を担当するクラスなので普通のRPGで考えればエンカウンターが発生した時に割り当てられ戦闘が終わった時に消去される訳です。このクラスに対してかなり具体的なイメージが出来ました。
次に以下に示す3つの配列についての解説が書かれています。
しかしこれは教科書を読まなくてもcombatantOrderが戦闘に参加する敵味方を攻撃する順番で整理した配列、playerPartyが味方のメンバー、enemyPartyが敵のメンバーと言う事は分かります。それ以上の事が書かれているのでしょうか?
書いていませんでした。次に行きます。
Phase変数はCombatPhase列挙型クラスから作成されDecision、Action、Victory、GameOverの4種類があります。これは戦闘中、Playerがどの状態であるかを示すために使用されると考えられます。教科書によれば、
Decision: 全てのキャラクターはどのアクションを取るかを選択できるフェーズ。
Action: 全てのキャラクターがDecisionで選択したアクションを実行するフェーズ。
GameOverとVictory: 全ての敵もしくは味方が死んだ時、別の状態に移すためのフェーズ。
とありました。
currentTickTarget変数はその時に対応しているキャラクターを示していると思われます。これがActionの時を指すのかDecisionの時もしくは両方なのかは分かりませんが。そのキャラクターの配列上の順番を保持するのがtickTargetIndexと考えられます。教科書はこれらの変数の説明は飛ばして、
の解説に行ってしまいました。教科書の解説によればこの関数は戦闘中ではフレーム毎に呼ばれ戦闘後にはTrueを返すそうです。
ここから、currentTickTarget変数とtickTargetIndex変数についての解説が始まりました。
以下に示すような解説がありました。
Decision時currentTickTarget変数は一人のキャラクターを指しています。フレーム毎にそのキャラクターはどのような行動を取るのかを聞かれます。もしそのキャラクターが行動を決定したらある関数がTrueを返します。するとcurrentTickTarget変数は次のキャラクターを指します。全てのキャラクターを指し終わったらActionに移ります。
特に予想と違う事が書かれている箇所はありませんね。これからCppファイルの作成に入ります。CppはStep.2で行います。
<Step.2>
まずコンストラクターから実装するそうです。以下に示すように教科書のサンプルコード通りに実装しました。
このコードをみるとアクションの順番はプレイヤーのメンバーそして敵のメンバーで決まってしまってますね。このままだとちょっとゲームとして古すぎるかもしれません。キャラクターのパラメーターにスピードを足してそれが速い順にアクションの順番を決める改良が必要になるかもしれません。
次にTick関数を実装します。
最初の部分のSwitchを実装します。
このコードを見ると完全にアクションの順番もcombatantOrderの配列通りですね。
残りのコードも実装します。残りは味方か敵が全滅したかどうかを判断しているだけですね。
これをフレーム毎に行うのはコストが高いような気がしますが他に選択がないのでしょうか?
次はSetPhase関数を実装します。
最初コードを見た時はActionとDecisionのcaseが良く分からなかったですがTick関数で実際にSetPhase関数が呼ばれている所からコードを追っていくと簡単に理解出来ました。
最後にSelectNextCharacter()関数の実装です。
最初何で次のキャラクターを選ぶのにForループが必要なのか分からなかったですが次のキャラクターが死んでいる場合があるのですね。納得しました。
<Step.3>
次はキャラクターが行動を決定したり決定した行動を実行したり出来るようにします。
Float型であるTestDelayTimer変数をGameCharacter.hに追加します。この変数、サンプルコードのGameCharacterクラスでは使用していないです。教科書にはテスト用の変数と書かれているので最後には外すのでしょうか?
追加しました。
更に以下の関数を追加しました。
- 最初の関数はアクションの決定や実行を告げます。
- 二番目の関数はアクションを決定するまでやアクションが終了するまでそのキャラクターを質問(query)します。
まずqueryが質問すると言う意味ではないと考えられますが本来の意味を忘れてしまいました。確かデータべースで使ってた気がしますが。そこは敢えて無視してもこの説明文は理解しにくいです。最初の関数とはBeginMakeDecision()とBegineExecuteAction()を指しているのでしょうか?それともBeginMakeDecision()とMakeDecision()を指しているのでしょうか?
ウン。ここで考えてみても分かりません。この部分の説明を理解するには実装してからそのコードを読んで検討するしかないみたいです。なので実装をします。
しました。しましたがこのコードはテスト用のコードで、これからそれぞれの関数の役割は推測出来ません。
<Step.4>
循環参照を避ける。
次にGameCharacter .h内にCombatEngineクラスのポインターを出来る様にします。これは普通に作成してしまうとCombatEngineクラスがGameCharacter.hをインクルードしているため循環参照を生んでしまいます。これを避けるために
をGameCharacter.hに加え、
はGameCharacter.cppに追加します。
ビルドしても成功しました。
循環参照は私がC++で書いた最初のプロジェクトで犯した間違いでした。それまではC++は本当に小さなプログラミングしか書いた事がなくその時は何故エラーになるのか全く理解出来ませんでした。兎に角、循環参照しているらしいのでClassをForward declaration しなければならないと言われましたが何の事か全く分からなかったです。今でもその時の事を思い出したら冷や汗が出て来ます。
<Step.5>
CombatEngineがDecision関数とAction関数を呼べるようにする。
CombatEngine.hにbool型であるwaitingForCharacter変数を追加します。
この変数、CombatEngine.hを作成した時に付録のダウンロードしたサンプルコードには厳然と書かれていたのに教科書には書かれていなくて何なのだろうかと思っていたのですがここで追加されるのですね。
この変数は教科書によればBeginmakeDecisionとMakeDecisionをスイッチするのに使用されるそうです。
Tick関数内のDecisionフェーズとActionフェーズを変更します。
まずはDecisionフェーズから変更しました。Case内を{}でくくらないとビルドした時にエラーになります。(教科書にしっかり書かれていました。親切すぎる。)
更にSelectNextCharacter()関数も直します。
waitingForCharacterの値を追いながらコードを読んでいけば不思議な所は特にはないです。
ただし教科書で述べられた「CombatEngineがDecision関数とAction関数を呼べるようにする。」のがこの部分を指しているのかは良く分からないです。良く分からないですが次に行きましょう。
<Step.6>
GameCharacter.h内にcombatInstanceを指すポインターを作成する。
しました。ビルドしてみます。
勿論成功しました。
何で言うかこの教科書は親切すぎますね。教科書通りにコードをコピーするだけで、私が循環参照で味わった地獄を全く知る事なくGameCharacter.h内にcombatInstanceを指すポインターを作成出来てしまうのは。
<Step.7>
CombatEngine.cpp内でコンストラクターとデストラクタ―を書き直す。
GameCharacterクラスのcombatInstance変数はこのCombatEngineを指さなければなりません。これをCombatEngineクラスのコンストラクター内で実装します。
デストラクタ―でenemyPartyとcombatantOrderを開放する必要があります。
しました。
今回はここまでにして考察します。
<考察>
<Step.1>
- フォルダーの作成方法について
VS上のsolution explorer内からFilterを選択してその中にC++ファイルを作成してもそのフォルダーは存在しない事をすっかり忘れていました。Solutions and foldersをクリックするだけで本当のフォルダー階層が表示されフォルダーの作成出来るので忘れないようにしたいです。
前回、Dataフォルダーがどのように作成されているのかUE4エディターからみて、更にVSから見て、最後に実際のフォルダーをチェックして良く分からなくなってしまいましたがこのバーチャルな階層が表示されているのか原因かもしれません。となると前回良く分からなかったGameと言うフォルダーも実際に存在しているのかもしれません。
調べて見ると、
内に
Dataフォルダーがありその中には
がありました。
UE4エディター上で表示されているデータテーブルは
Content->Dataフォルダーに配置されています。これはVSの場合とは違い、
実際に存在するフォルダーとファイルです。しかしUE4エディター上でこれらのファイルを参照する場合はautosaveフォルダー内に存在する同じファイルを参照するようになっているようです。
混乱しないようにしたいです。
- VSで表示されるフォルダーの階層はバーチャルな階層で実際のフォルダーと一致しない。実際のフォルダーを表示させるためにはsolutions and foldersをクリックすれば良い。
- UE4エディターのContent Browser で表示されるフォルダーの階層は実際のフォルダーである。しかしそのフォルダー内のファイルのパスはContent Browser で表示されるフォルダーの階層と一致しているとは限らない。
- combat engineクラスについて
combat engineクラスは戦闘を担当するクラスです。言われてみれば目から鱗ですが戦闘を担当するクラスを作成する発想がなかったです。のでもし自分が戦闘を担当するクラスをゼロから作成してみたらどんな変数や関数が必要かここで考えてみます。
- 戦闘に参加する全てのキャラクターを保持する配列。
- 戦闘はターン制なので今の戦闘がどのターンなのかを示す変数。
- それぞれの変数にアクセスするゲッターやセッター。
恥ずかしながらこれくらいしか思いつきませんでした。もう一度combat engineクラスを見てみます。
まず列挙型クラスCombatPhaseを作成してゲームの状態を指定しています。この教科書はたまに強烈な主張を挟んできます。今回のそれはターン制ゲームの状態は4つしかない。それはDecision、Action、Victory、そしてGameOverであると宣言している所です。これによってこれからの戦闘に必要な要素が非常に特定しやすくなりました。この主張は個人的な意見なのでしょうか?それとも客観的な真理なのでしょうか?兎に角才能のある人はこのような概念を簡単に言語化します。
当然戦闘中の状態を表す変数は必要です。
次にキャラクターのパラメーターを保持しているUGameCharacterクラスの配列を3つ作成しています。プレイヤーのパーティーの配列と敵のパーティーの配列そして最期に戦闘の順番を決めた配列です。combat engineクラスは戦闘が開始される瞬間に初期化されるはずですから、プレイヤーのパーティーと敵のパーティーは配列としてすぐにcombat engineクラスに渡せるはずです。実際、combat engineクラスのコンストラクターは、
その様に作成されています。
次の3つの変数ですが、これは多分他のクラスや関数との関係から便宜的に作られた変数に思えます。のでここではスキップします。
フレーム毎の行動を指定する関数は当然必要ですね。
SetPhase関数はPhase変数のためのセッターと考えられます。次のSelectNextCharacter関数はキャラクターが死亡する事を考えてなかった私はこんなの本当に必要なのかと思っていました。
<Step.2>
- アクションの順番
教科書ではアクションの順番は味方のメンバー敵のメンバーの順に決定してますが、これはちょっといただけません。ここだけは後で変えようと思います。今の所の考えではUGameCharacterクラスに新しいパラメーター、speedを追加してそれの値を基にして順番を決めようと考えています。
- Tick関数内の効率化
Tick関数の実装部も後で効率化出来たらしたいと思います。フレーム毎に計算するわけでなるだけ負担は軽くしたいです。
<Step.3>
- BeginMakeDecision()、MakeDecision(float DeltaSeconds)、BeginExecuteAction()、ExecuteAction(float DeltaSeconds)の目的について
これはまだ、教科書でも述べられていません。来週以降にこれらの関数の実装が追加された時に明らかになると思っています。
<Step.4>
- 循環参照について
循環参照によるエラーの直し方はそれぞれのサイトに沢山載っていますのでここでは省略します。しかし一つ考察したい事は循環参照は悪いデザインの結果起きると断言されている事です。これが昔の私を大変苦しめたのです。だって循環参照によるエラーは直せても循環参照を生み出した悪いデザインは直せないからです。これって絶対本当なんでしょうか?
ここに同様な質問をした人がいます。しかし運営側はこの質問に対する回答は単なる主観であるとの決定から質問は打ち切られてしまいました。打ち切られてしまいましたがある程度の活発な議論は行われていたのである程度の結論は出ていました。それによると循環参照はやはり悪いデザインである。その理由としてメモリーのリークを起こしやすい。卵が先か鶏が先かの状態になる。などがありました。ただし循環参照を沢山使用したコンパイラーの紹介もされていて絶対悪いとは言えないかもしれない可能性もありそうです。
ここからは私の考察ですが循環参照をする事で間違った解答を得るならば循環参照は悪いデザインであると考えています。しかし循環参照を使用しなければ解けない問題がありその問題を正しく解くのに循環参照が必要なら循環参照は正しいデザインと考えられると思ってます。ここで参考にしたいのが数値解析の問題です。
数値解析でAの値を解くのにBの値が必要でそのBの値を解くのにAの値が必要と言う絵に描いたような循環参照は結構あります。この時最初にBの値を適当に決めてAの値を求めます。その求めたAの値を元にもう一度Bの値を求めます。これを繰り返すと結構正確なAの値が求まる場合があります。
ので循環参照は悪いデザインであるとは限らないと個人的には考えています。
<Step.5>
- Case内を{}でくくらないとビルドした時にエラーになる事について
Compiler Error C2360が起こってしまい結構慌てました。解決策は単に{}でcaseを覆えば良い事をこのサイトで知りましたが、驚いたのは教科書を後で読んだらこの間違いについて解説されていました。
<Step.6>
- 教科書が親切すぎる件について
循環参照は今でも覚えているくらい悩んだエラーでしたがこの教科書通りにコードを書いたら全く問題なくForward declaration で解決していまいました。Step.5のCaseを{}で囲まないとエラーになる件も教科書にわざわざ書かれていました。ここまで親切に設計されていると本来滑った転んだで大変な思いをして学ばないといけない事を軽く飛ばしてしまうかもしれないと言う不安です。
<Step.7>
- 循環参照もう一度
これは何が言いたかったかと言うと循環参照を使用したソフトウェアデザインはないのでしょうか?と言う事が言いたかったのです。Step.4で数値解析においては循環参照と同様の方法を用いて正しい解答を得る場合があるからので循環参照は悪いデザインであるとは限らないと結論づけました。だったらもう一歩進んで循環参照を使用したソフトウェアデザインもあるんじゃないのかな。と思っただけです。