UE4の勉強記録

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

Unreal Engine 4.xを使用してRPGを作成する」の足りない部分を作成する Combat Engineの自作

f:id:kazuhironagai77:20200503203346p:plain

<前文>

今週も思いついた話をダラダラと書いておきます。

まだまだC-何とかウィルス関連のお話

とうとうアルコール消毒液が無くなりました。マスクはとっくに底をついていて全然C-何とかウィルスが収束しないのに我が家の備蓄の方が先に底をついてしまいました。何故か厚生省が効果を保証した濃度の界面活性剤を含む洗剤は大量にスーパーに残っていて手洗いだけはしっかり出来ています。死んだら死んだでしょうがない。みたいな考えで今まで生きていたのでC-何とかウィルスに個人で対策しようとは思わなかったのですが、流石に市内でそれ関連の死者が出た話を聞いたら焦って来ました。それでその洗剤を水で薄めてアルコール消毒液の代わりにスプレーで吹きかける事にしました。そしたらアルコール消毒液とは違い勝手に蒸発はせず一々拭き取らないといけない事が分かりました。かなり億劫で使用回数がガンと減ってしまいました。

もうこうなったらアルコール濃度の高い酒でも蒸留してアルコール消毒液を自作するしかない。と思ったのですが、蒸留中にアルコールに引火したら炎が見えないので大事故は免れません。下手したら新聞ネタです。やらないリスクとやるリスクのどちらが高いのかの査定が必要です。そしたら次亜塩素酸水というアルコール消毒液より効果があってしかも食品の消毒に使用出来るほど安全というほとんど魔法の水みたいなものがある事を知りました。ネットで注文しようとしたら勿論売り切れでした。正確に言えば売り切れではないですが売っているのがかなり怪しいです。一応、生成方法を調べたら食塩水を電気分解したら出来るとあります。え。そんなんで出来るのと、更に調べていたら身近な道具だけ使用して更に一工程で生成出来る方法を思いついちゃいました(素人が真似をするいけないので方法は書きません。)こっちは事故っても手がビリとするぐらいで済みそうです。ただ塩素が細菌やウィルスの炭素を攻撃するのがちょっと心配です。ほとんどはHClになると思いますが有機化した塩素は全く生成されないんでしょうかね。有機化した塩素って人体に結構危険だと思うんです。

ZOOMなどについて

短期的とはいえ一般の企業でも在宅勤務を強制されるようになって、使用者のデータだか秘密鍵だかを全部中国に送っているのではないのか?みたいな疑惑があるにもかかわらずzoomというオンライン会議のソフトが非常な高頻度で使用されるようになっているそうです。

この話を聞いて思ったのですが、正しいプログラミングとは何ぞやと言う事です。私は正しいソフトウェアデザインかつ会社のプロトコールに従って書かれているプログラミングのみが正しいプログラミングと思っていました。しかし今回のzoomの一件が教えてくれたのは、ユーザーが欲しがっているサービスを提供出来るのが正しいプログラミングだったと言う事でした。

正しいソフトウェアデザインやアルゴリズムを使用する事は、ユーザーが欲しがっているサービスを実現するための必要なツールであってそれ自体が目的ではなかったです。プロトコールに至っては単なるパワハラの温床の可能性すらあります。よくプロトコールを守らないとハッキングされるとか言われますがzoomを見てもデータを抜かれる可能性があっても必要なサービスを提供するソフトはみんなから使用されてるじゃないですか。ソフトウェアデザインやプロトコールはあくまでユーザーが欲しがっているサービスを提供している前提で語られる話であって、それ以前の話ではないと言う事です。

それでは今週も勉強していきましょう。

<本文>

とうとうCombat Engineを自作出来る環境が整いました。

2020-04-12のブログで新しいCombat engineについてまとめましたが、もう一度新しいCombat engineに必要な機能を以下にまとめます。

まず以下に示した4つの機能を従来のCombat engineに追加します。

f:id:kazuhironagai77:20200503203722p:plain

更に、戦闘中は別のマップに飛んで戦闘が終わると戻ってるようにします。

色々考えたのですが、正直どうやって実装するのがベストなのか分かりません。ので基本的に教科書に載っているCombat engineと同じやり方で作成し、そのCombat engineに1から4の機能を追加する事にします。戦闘中に別のマップに飛ぶ機能は、GameModeクラスのTestCombat()関数内に実装します。

1.Combat engineの基を教科書を参考にして作成する

教科書の3章4節Turn-based Combatを参考にして作成していきます。

最初に親クラスが無い状態でCombatEngineクラスを作成します。ここは、UObjectを親クラスにした方が後々使用しやすいのかなとも思いましたが、具体的な例が思いつかないので教科書と同じにしました。

中身を作成していきます。基本的には教科書のやり方を世襲して、ch3で変更した内容に合わせるために細部は変更していきます。

戦闘における4つのフェーズをEnumで作成します。

f:id:kazuhironagai77:20200503203749p:plain

変数を追加します。

f:id:kazuhironagai77:20200503203832p:plain

戦闘の順番を示すcombatantOrderは教科書に書かれている通りに、敵と味方を示す変数は、教科書と違い常に一対一で戦うのでTArrayを外しました。戦闘のフェーズを表すphaseは絶対必要なので教科書に載っているままに宣言します。

次にProtectに以下に示す2つの変数を追加しました。

f:id:kazuhironagai77:20200503203857p:plain

確かcurrentTickTargetは現在選択されている敵もしくは味方のキャラを指し、tickTargetIndexはそのキャラのcombatantOrder内での順番を指していたと思います。あんまり自信はありません。これらの変数は一対一の戦闘では必要がないと思われるので実際の戦闘のコードを書く特に、本当に必要か考えます。必要なかったら消します。今は一応残しておきます。

Publicに関数を追加します。

f:id:kazuhironagai77:20200503203947p:plain

特に説明が必要な関数はないので次にいきます。

Protectedに二つの関数を追加します。

f:id:kazuhironagai77:20200503204022p:plain

前にCombat engineを勉強した時これらの関数をあえてこのクラスで宣言する理由は分かりませんでした。社長が部下に独立されないために管理していると考えると辻褄が合うみたいな事を考えたと思います。

それではコンストラクターから実装していきます。

f:id:kazuhironagai77:20200503204058p:plain

基本的には教科書と同じですが一対一の戦闘なのでcombatantOrderの初期化の方法が変わっています。

次にTick()関数を実装します。

Tick()関数は、戦闘のフェーズによって実行する内容が変化します。ので4つのフェーズのそれぞれを実装します。

長いので以下に最初のフェーズDecisionを示します。

f:id:kazuhironagai77:20200503204123p:plain

取りあえず上記のように実装しました。SelectNextCharacter()関数はもしかしたら要らないかもしれません。この辺は全体が大体完成してから直していきます。

f:id:kazuhironagai77:20200503204152p:plain

Actionも大体同じです。

フェーズがGameOverとVictoryの場合です。

f:id:kazuhironagai77:20200503204233p:plain

Switchの外側のコードです。GameOverかVicotryかを調べたりしています。

f:id:kazuhironagai77:20200503204255p:plain

Tick()関数は以上です。

SetPhase()関数を実装します。

f:id:kazuhironagai77:20200503204314p:plain

教科書と全く同じです。

SelectNextCharacter()関数の実装です。

f:id:kazuhironagai77:20200503204333p:plain

こちらも取りあえずは教科書と全く同じにしておきます。これで動かない訳ではないからです。

次は、これらのクラスを実行する関数をGameCharacterクラス内に作成します。

前にも言いましたが、このクラス、ゲーム内のキャラのパラメーターを設定するクラスなのに、何故か戦闘のフェーズ事の作業も担当する事になっています。

この辺は、全く教科書に書かれている通りなのでコードだけ載せます。

f:id:kazuhironagai77:20200503204401p:plain

f:id:kazuhironagai77:20200503204411p:plain

f:id:kazuhironagai77:20200503204424p:plain

特にコメントもないです。

今度はCombatEngine.hをインクルードしたいのですが以下に示すように既にCombatEngineクラス内でGameCharacterをインクルードしています。

f:id:kazuhironagai77:20200503204447p:plain

Circular dependencyを避けるためにForward Declarationを行います。

f:id:kazuhironagai77:20200503204515p:plain

そしてcppファイルでCombatEngine.hをインクルードします。

f:id:kazuhironagai77:20200503204535p:plain

GameCharacterクラスは一端これで終了でまたCombatEngineクラスに戻ります。

今度はCombatEngineクラスから先程作成したGameCharacterクラスのメンバー関数を呼び出します。

もうこの部分のコードを読んだり書いたり何回もしているのですが未だに、GameCharacterクラスとCombatEngineクラスをこんな複雑な関係にする理由が分かりません。分からないので教科書のまま書いておきます。

CPHASE_Decisionのフェーズの場合です。

f:id:kazuhironagai77:20200503204614p:plain

コードそのものに対しての疑問はありませんね。キャラクターと敵のモンスターの2体しかいないのでSelectNextCharacter()関数などを変える事でもっと簡単なコードが書ける気はしますが、その辺は後でします。

今度はCPHASE_Actionです。これも内容的にはほとんどCPHASE_Decisionと同じなので解説はしません。

f:id:kazuhironagai77:20200503204644p:plain

次はSelectNextCharacter()関数を改良します。

f:id:kazuhironagai77:20200503204714p:plain

と言ってもwaitingForCharacterを追加しただけです。次のキャラに変わったらwaitingForCharacterもfalseに戻らないとオカシイというだけですね。

今度は、GameCharacterクラス内にCombatEngineを指す変数を作成し、それをCombatEngineのコンストラクター内でセットします。

まず、GameCharacter.hのpublicに

f:id:kazuhironagai77:20200503204743p:plain

を追加します。次にCombatEngineのコンストラクター内でそれぞれのキャラのCombatInstanceにこのCombatEngineを指定します。

f:id:kazuhironagai77:20200503204900p:plain

この部分は複雑ですがなぜこうしたのかの理由は分かりますね。CombatEngineが初期化される以前からCharacterは作成されているので、CombatEngineが作成された後でないとcombatInstance変数をセットする事が出来ないからです。

となると当然次のCombatEngineのデストラクターの必要性も理解出来ます。

f:id:kazuhironagai77:20200503204947p:plain

味方のキャラはこの後も存在するので、戦闘後に消えるCombatEngineへのポインターを消す必要があります。それをしないとこのCombatEngineを指す変数がずっと残る事になります。

あれ、でもそうすると何で敢えてEnemyMonsterをnullptrに指定する必要があるのでしょう。

簡単に整理します。

まず普通のC++の場合、heap領域にデータを保持した場合、自分でメモリーを開放しなければずっとそのデータは保持されます。UE4C++はGCが勝手に開放していきます。ただしUPROPERTYで指定されている変数が指している住所に保持されているデータは開放されません。

私が現在理解している範囲なので上記の説明自体が間違っている可能性もありますが、取りあえず正しいとして議論を進めます。

まずcombatantOrderのcombatInstanceですが、GameCharacterクラスのcombatInstance変数は以下に示すように、

f:id:kazuhironagai77:20200503205107p:plain

単なるc++の変数です。これはcombatEngineクラス自体が普通のc++である事を考えれば当然です。のでCombatEngineクラスからheap領域に作成されたインスタンスを消去する時は、deleteを使用する必要があるはずです。

教科書のサンプルコードをチェックしてみると以下に示すようにCombatEngineを初期化したGameMode内で消去も行っています。

f:id:kazuhironagai77:20200503205158p:plain

消去されて意味のあるデータを保持していなくなった住所をまだ指しているポインターの事をdangling pointerと言ってc++で避けなければならない事の一つです。CombatEngineクラスのデストラクターは、これを避けるために

f:id:kazuhironagai77:20200503205239p:plain

をしていると考えられます。

これは理解出来ます。

ですが次のEnemyMonster変数をnullptrに指定する理由が分かりません。まずEnemyMonster変数自体はCombatEngineクラスの変数でCombatEngineクラスのインスタンスが消去される時、同時に消されるはずです。必要ない気がします。

もし仮に必要ならばcombatantOrderもやらないといけないと思います。

うーん。ちょっと分からない。ここも後で考えます。

今度はもう一度RPGGameModeBaseクラスに戻って、CombatEngineを実行するためのコードを追加します。

ここで、もう一個疑問が出て来たので記録しておきますが、この教科書だとoverrideを以下の様に書いてます。

f:id:kazuhironagai77:20200503205333p:plain

Virtualはoverrideする関数に付けるのではなく、overrideされる元の関数に付けると理解していたのですが、この教科書だとoverrideする関数にも付けています。

CppReferenceのoverride specifier (since C++11)でも

f:id:kazuhironagai77:20200503205402p:plain

と説明していてvirtualは元のoverrideされる関数にしか付けていません。

でもUE4C++から派生したクラスをみると、以下に示すように

f:id:kazuhironagai77:20200503205428p:plain

付いています。

そう言えば、どっちでもいいと言う説明を昔、どこかのC++チュートリアルで見た気もします。しかしそれがどのチュートリアルだったのか覚えていません。

これも後で探します。

取りあえず今はどちらも正解として先に進めます。

以下に示す変数を追加します。

f:id:kazuhironagai77:20200503205455p:plain

EmenyMoster変数は前に作成していたのでCategoryをMyMonsterに変更しただけです。教科書の例ではUPROPERTYは使用していませんが追加しました。currentCombatInstance変数はCombatEngineクラスから作成したのでUPROPERTYはありません。

更にTick()関数内でcurrentCombatInstance変数を実行するためのコードを追加します。

f:id:kazuhironagai77:20200503205524p:plain

まずSetActorTickEnabled(true)ですが、元々プレイヤーが操作出来ない様にセットしていませんのでこの部分のコードを足しておきます。

f:id:kazuhironagai77:20200503205548p:plain

TestCombat()関数内に足しました。

私がこれから作成する戦闘システムは別のマップに移動して戦うので、戦闘終了時にGameModeのインスタンスは破壊されます。のでEmenyMonster変数も消滅するのであまり深く考える必要はないのですが一応nullptrに指定しておきました。

よしこれでテストしてみるかと思ったらcurrentCombatInstance変数を初期化していませんでした。教科書のサンプルを見たらTestCombat()関数内で初期化していたのでそれをします。

f:id:kazuhironagai77:20200503205635p:plain

今気が付いたのですが、enemyのスペルが間違っていました。それも直しました。

後、enemyMonster変数は現在、GameModeクラスで管理していますが、本来GameInstanceクラス内で管理しなければなりません。これも後で直します。

それではテストしてみます。

クラッシュしました。

また、pure virtual function being called while application was running…と出ました。

うーん。

先にこれを直します。

と思ったら、今度はクラッシュしません。

しょうがないのでテストします。

GameModeクラスのTick()関数が呼ばれません。

あっそうだ。以下のコードを入れるのを忘れていました。

f:id:kazuhironagai77:20200503205738p:plain

もう一度テストします。

f:id:kazuhironagai77:20200503205801p:plain

あれ、ずっと戦闘状態が続いているじゃないかと思ったら、敵のモンスターのHPもプレイヤーが操作するキャラのHPも全く減らない状態なのでこれが正しかったです。

2.まとめと感想

本当はもう少しやる予定だったのですが、時間が無くなってしまいました。続きは来週やります。