UE4の勉強記録

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

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

f:id:kazuhironagai77:20200315202404p:plain

<前文>

最近、ゲームデザインの勉強と言って、アニメについて語っていましたが、基本的に英語圏のアニメファンの話しか見た事なかったですが、日本人でも本当にアニメが好きな人達は、私にとっても益する話があるはずと調べてみました。そしたら結構、面白い意見がありました。その中でビースターズは、面白いから絶対みるべき。と言うのがあり、全く聞いた事がないアニメなのでびっくりしました。今から全部見るにはちょっと長すぎるのでYouTubeビースターズの感想だけ見たのですが、幾人かの鋭い考察を述べる英語圏のアニメファンが、30分も40分も語っているのを見て、日本だけでなく英語圏でも大変評価されているアニメだと言う事は分かりました。ただ全然内容が分かりません。マンガを買ってみるか、どこかでアニメを見るしかありません。

日本人のアニメが好きな人達のブログやYouTubeチャンネルを見て思ったのは、日本人と英語圏のアニメが好きな人達の好きなアニメは大体一緒ですが、好きな理由が大分違うと言う事です。プログラマーで英語が分からない人は能力不足と思いますが、日本人のアニメが好きな人が、英語が分からなくても問題ないですし、むしろ英語圏のアニメが好きな人達が日本語勉強しろよと思います。それで英語圏のアニメが好きな人達が何故そのアニメが好きなのかを日本語で紹介するのは意義があるのかな。と思いました。

これは昔、誰かから聞いた話なのですが、人間には余暇を過ごすのに、自分からアクティブに活動するタイプと、自分からは行動せず受け身なタイプがいるそうです。英語で言うextrovertとintrovertというやつです。でこのextrovertな人はゲームみたいにアクティブに自分から関係する遊びが好きでintrovertな人は読書や映画鑑賞みたいに受け身な遊びが好きなようです。

で、私は典型的なintrovertで、自分からアクティブに活動する遊びが嫌いなんです。何で余暇に余計に疲れなければいけないんだと。それでゲームですらあんまり興味がなかったです。だけど読書は大好きでした。このブログはUE4の勉強を記録するためのものですが、正直、ゲームを制作するに当たって、ゲームが趣味の人達が納得するようなゲームが作れるとは思っていないです。何と言うかextrovertな人の好みが良く分からないんです。

最も、それに対する対策も考えています。一つ目は先週書いた、英語の発音が聞き取れる様になるゲームで、遊びながら何かが身に付くゲームです。

もう一つ考えているのが、他人がデザインしたゲームをUE4で制作する方法です。ネットに紙と鉛筆で作成したカードゲームやボードゲームが結構あります。その中には、日の目を浴びていないだけで、ゲームが趣味の人達がうなるような凄いゲームデザインがあるような気がします。それをUE4を使ってコンピューターゲーム化するんです。ネットに上がっている紙と鉛筆で作成したカードゲームやボードゲームなんですが、部外者の立場から見た限りですが、何か結構凄いように見えます。なんでコンピューターゲームにならないのか不思議ですが、そう言えば、ポケモンを作った人が、最初はゲームのアイデアを考える会社だったのですが、どんな凄いゲームデザインを考えても、ゲーム会社はゲームにしてくれない。ので自分たちで作るしかない。となった。とどこかで読んだような気もします。

まあ、そうやって自分の弱点はどうにか補おうとは考えていますが英語圏のアニメが好きな人達が何故そのアニメが好きなのかを日本語で紹介するブログを書いて、introvertなままでメジャーデビュー出来るならそれもありかなとかも思いました。

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

<本文>

1.先週の復習

先週、勉強した内容で復習が必要な部分やバグみたいな個所がありました。それを箇条書きにしてここにまとめ直しておきます。

  1. 敵、味方の人数を増やした場合のテストが必要。TestCombat()関数を改良して作成するべきか?
  2. CombatEngineクラスのコンストラクター内で、最後にtickTargetIndex変数に0 をセットしSetPhase()関数を実行しますが必要ないと思います。確認します。
  3. 味方全員の攻撃が終わってから生き残った敵が攻撃するのはオカシイ。
  4. CombatUIWidgetクラスの関数からBP内で呼ばれるAddPlayerCharacterPannel()Spawn Character Widget関数内のノードでBaseCharacterCombatPanelWidgetでキャストしていますがPlayerCharacterCombatPanelWidgetでキャストするのが正しいのでは?
  5. Canvas Panelを使用したUUserWidgetHorizontal Boxなどの子クラスとして追加した場合、そのHorizontal Box内に表示されるのか、それともwidgetの全体に表示されるのか分からない。
  6. UPROPERTY()で修飾されているTArray変数は、時期が来たら勝手に開放してくれるのか、それともTArrayの場合はEmpty()関数を使用してそれぞれの要素を開放しないとメモリーリークになってしまうのか?
  7. レベルが上がった時、キャラのそれぞれのパラメーターが1上昇しますが、武器を外したら元に戻ってしまいます。

まず、先週の残りのこれらの問題から解決していきます。

5. Canvas Panelを使用したUUserWidgetをHorizontal Boxなどの子クラスとして追加した場合、そのHorizontal Box内に表示されるのか、それともwidgetの全体に表示されるのか分からない。

これが一番、気になるので、これからテストします。

まず、以下に示すUIを作成します。

f:id:kazuhironagai77:20200315202528p:plain

左の赤い部分には、CanvasPanelのないwidgetをchild classとして追加するvertical box、右の青い部分には、CanvasPanelのあるwidgetをchild classとして追加するvertical boxを追加します。

f:id:kazuhironagai77:20200315202545p:plain

CanvasPanelがある場合は、親クラスである青い部分のvertical box内にボタンが表示されるのかどうかを調べるために、敢えて、ボタンの位置を真ん中にしました。

f:id:kazuhironagai77:20200315202609p:plain

これでテストします。

f:id:kazuhironagai77:20200315202631p:plain

やはり、CanvasPanelがあるWidgetを子クラスとしてVertical boxに追加する場合は、そのvertical box内に表示はされますが、何か変です。

では、子クラスであるwidgetを何回も呼ぶ場合はどうなるのでしょうか?

試してみます。

f:id:kazuhironagai77:20200315202650p:plain

加える回数を2回増やして見ました。もしこれで重なって現れるのならば、教科書のやり方でも、パーティメンバーが二人以上の時にwidgetが重なって現れるはずです。

f:id:kazuhironagai77:20200315202716p:plain

何なんでしょう。重なっては現れないですね。

今度は。数を6個に増やしてみます。

f:id:kazuhironagai77:20200315202738p:plain

6個に増やした場合は全部は表示されませんね。この隙間はどのように定義されているのでしょうか?

ひょっとするとanchorからの距離かもしれません。以下のようにanchorからの距離を0(この場合はトップ)にして見ました。

f:id:kazuhironagai77:20200315202800p:plain

そうみたいです。

f:id:kazuhironagai77:20200315202818p:plain

どちらを使用するのが良いのでしょうか?

CanvasPanel有りの場合で、更に複雑な場合を作成してみました。

f:id:kazuhironagai77:20200315202836p:plain

f:id:kazuhironagai77:20200315202845p:plain

f:id:kazuhironagai77:20200315202901p:plain

これで綺麗に表示されれば、大満足ですがどうでしょうか?

f:id:kazuhironagai77:20200315202926p:plain

うん。されていますね。

ウィンドウのサイズを変化させた場合はどうでしょうか?

f:id:kazuhironagai77:20200315202958p:plain

それなりには、対応しているみたいです。

断言は出来ませんが、一応CanvasPanelがあるWidgetを子クラスとしてVertical boxに追加しても大丈夫な事は確認出来ました。

1. 敵、味方の人数を増やした場合のテストが必要。TestCombat()関数を改良して作成するべきか?

これは、もう作成してしまいましょう。

f:id:kazuhironagai77:20200315203039p:plain

何体も試す前に一体だけモンスターを増やしてみます。

f:id:kazuhironagai77:20200315203105p:plain

敵一体増やすだけなので、ループにはしないで、FEnemyInfoクラスの変数、row2を作成して、そこに2番目のモンスターのデータを読み込ませました。

f:id:kazuhironagai77:20200315203124p:plain

UGameCharacterクラスであるenemy1変数を作成してそこに、row2のデータを追加します。

そしてenemy1をこのクラスの変数であるenemyPartyに追加します。

f:id:kazuhironagai77:20200315203144p:plain

ざっとコードを見た結果、それ以外の改造は必要なさそうです。これでテストしてみます。

f:id:kazuhironagai77:20200315203211p:plain

あれ、結構、格好いいですね。

f:id:kazuhironagai77:20200315203240p:plain

これって、ちょっと改良したら使えそうな感じですね。攻撃ボタンを押してみます。

f:id:kazuhironagai77:20200315203301p:plain

攻撃対象のモンスターにゴブリンだけでなくトロールが現れました。

f:id:kazuhironagai77:20200315203324p:plain

トロールを選択してみましょう。

f:id:kazuhironagai77:20200315203358p:plain

画面の展開が速すぎて、全部は追えないですが、見えた限りでは、トロールには全くダメージを与えられず、その後、ゴブリンとトロールの攻撃を食らって死んでしまったようです。

その後、色々数値をいじってみましたが、トロールに攻撃はしている事は確認出来ました。

味方のキャラを増やした場合もテストしようかと思いましたが、そうすると、装備の問題などを全部直さないといけなくなるので、今回のゲームでは、味方はいない事にします。

2. CombatEngineクラスのコンストラクター内で、最後にtickTargetIndex変数に0 をセットしSetPhase()関数を実行しますが必要ないと思います。確認します。

コメントアウトしてテストしてみます。

f:id:kazuhironagai77:20200315203451p:plain

普通に出来ました。いらなかったです。

3. 味方全員の攻撃が終わってから生き残った敵が攻撃するのはオカシイ。

先程、味方を追加した場合、戦闘システムの改良だけでなくそれぞれの味方の装備や道具を全て直さなければならなくなるので、主人公の味方は無しにすると決定しました。敵が複数いるのに主人公は一人で戦わなければならず、その場合、常に先手を取ってもそれくらいは許されると思います。のでこれは直しません。

4. CombatUIWidgetクラスの関数からBP内で呼ばれるAddPlayerCharacterPannel()Spawn Character Widget関数内のノードでBaseCharacterCombatPanelWidgetでキャストしていますがPlayerCharacterCombatPanelWidgetでキャストするのが正しいのでは?

そうです。これはチェックしないといけません。

f:id:kazuhironagai77:20200315203540p:plain

BaseCharacterCombatPanelWidgetでキャストのノードをPlayerCharacterCombatPanelWidgetでキャストに変更しました。

これでテストしてみます。

f:id:kazuhironagai77:20200315203604p:plain

敵のモンスターが表示されなくなってしまいました。前の方があっていました。戻します。

だったら、キャストしないでそのまま使用してもいいはずです。それも試します。

f:id:kazuhironagai77:20200315203625p:plain

これでテストしてみます。

普通に動きました。これでいいみたいです。

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

これ、一番調べなければならない事です。

この日本語のサイトに詳しく解説されていました。

Empty()関数は要素を開放し、メモリーはキープしておくそうです。このサイト、分からなければ実際のコードを読めばいい。という大変ストレートな解法を使用していました。言われてみれば確かにその通りですが、それをやりたくないからAPIを漁っているのですが、やっぱりある程度のレベルになったらそうするのが一番早いみたいですね。

こっちの公式のサイトのTArrayに簡単な解説がありました。

今回は、この公式のサイトを読んで復習するにとどめておきます。

読んでいて思い出したのですが、ContainerとUPROPERTYは何かあったはずです。うろ覚えですがcontainerとstructはGCが作用しない。みたいな話があったような。

前、探した時は非常に詳しい解説をしたサイトがあったような気がするんですが見つかりません。

取りあえず、このサイトに以下のように説明されていました。

f:id:kazuhironagai77:20200315203707p:plain

と言う事は、少なくともTArrayの場合は、要素がUObjectの派生クラスから作成されたインスタンスの場合は開放されると考えていいんでしょうか。

このサイトに何か関係している話が紹介されていました。

簡単に説明しますと、GameInstanceクラスにTArrayとUPROPERTYを使用してUObjectの派生クラスが要素の変数を作成したのに、mapを変更したらTArrayの変数は残っていたけど中身が全部消えちゃってしまった。と。GCもTArrayのPointerは考慮するんはずじゃ。と聞いています。

うん?

と言う事は、上記のContainersの説明は、GCはTArrayの時だけContainerでもPointerを気にしますよ。と言う事ですか?つまり、TArrayが保持している変数は、勝手にGCの対象になりませんよ。

逆に理解していた。

Structと違ってContainerは中の要素が開放されない保証はしません。ただしTArrayは違いますと言う意味みたいです。

となると増々分かりません。こっちはGCにTArrayを中身ごと開放してもらいたいんです。それはやってくれるんでしょうか?

GCがレファレンスのないメモリーをどんどん開放していくなら、TArrayで作成された変数が開放された時にその要素も他からレファレンスされていなければ開放されるはずです。

分かりました。

Empty() 関数を使用して開放しているTArray変数であるenemyPartyは、GameModeクラスとCombatEngineクラスにそれぞれ別な変数としてあったんです。でGameModeクラス内のTick()関数内で、既にCombatEngineクラスのインスタンスを開放しているのに、何でその後で、CombatEngineクラスのメンバー変数であって既に開放されているはずのenemyPartyにempty()関数を使用しているのか?となったんですが、このempty()関数を使用しているenemyParty変数はGameModeクラスの変数だったので、別に開放されている訳ではなかったです。

ので、empty()関数を使用してenemyParty変数は中身の要素を空にする必要があったわけです。

CombatEngineクラスのenemyParty変数

f:id:kazuhironagai77:20200315203732p:plain

GameModeクラスのenemyParty変数

f:id:kazuhironagai77:20200315203800p:plain

GameModeクラスでEmpty()関数を使用しているのは、

f:id:kazuhironagai77:20200315203818p:plain

GameModeクラスの方でした。

これで、全部解決したと思ったら、

f:id:kazuhironagai77:20200315203838p:plain

GameModeクラスのenemyParty変数UPROPERTYを指定していませんね。

f:id:kazuhironagai77:20200315203856p:plain

f:id:kazuhironagai77:20200315203903p:plain

enemyPartyに追加する要素は、NewObjectを使用していますが。

UPROPERTYを指定していなくても、empty()関数を使用すれば、結果は変わらないと思いますね。兎に角、GameModeクラスとCombatEngineクラスにそれぞれ別な変数としてenemyParty変数はあって、GameModeクラスのenemyParty変数は普通に存在していてその中身を空にして次の戦闘に備える必要があったというだけでした。

これ以上理解するためには、実際にコードを書いてテストする必要があるかもしれませんが、この程度の理解で今回は十分ですのでこれでお終いにします。

7. レベルが上がった時、キャラのそれぞれのパラメーターが1上昇しますが、武器を外したら元に戻ってしまいます。

これは、もう少し、ゲームデザインが決まってから考える事にします。現状でも攻撃と防御は武器を変えてもレベルが上昇した値を維持していますが、それ以外のパラメーターはレベルの上昇に影響受けなくてもいいかもしれませんし。

f:id:kazuhironagai77:20200315203949p:plain

レベルだけは、武器を外して下がってしますのはおかしいですね。

ただ、現状のようにキャラのパラメーターを全て、GameInstanceクラス内に保持させておくのが良いのか、考える必要があると思います。

f:id:kazuhironagai77:20200315204009p:plain

兎に角、この問題は来週以降考えます。

2.CombatEngineの復習

前回は、流れを簡単に追いましたが、今週はCombatEngineのそれぞれのクラスを見てみます。理由は自分でターン制のCombatEngineを自作したいからです。

先週も述べましたが、

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

が出来るようにしたいです。

更に詳しく言えば、上記の目的はゲームデザインからの観点で、プログラマーとしては、UE4C++に自作のエンジンを追加する方法を学習したいと言う部分もあります。この場合、純粋なC++のみを使用する時と、UE4C++も使用する時でどのような事に気をつけないといけないのかなどです。

では、まずCombatEngineとして作成されたクラスにどんなのが在るのかを見てみます。

f:id:kazuhironagai77:20200315204109p:plain

Combatエンジンのフォルダー内には、更にActionsフォルダーとDecisionMakersフォルダーがありました。その他にはCombatEngineクラスのヘッダーファイルとCppファイルがありました。思っていたより単純な構造でした。

f:id:kazuhironagai77:20200315204134p:plain

DecisionMakerのフォルダー内では、IDecisionMakerクラスとTestDecisionMakerクラスがありました。こっちも内容的には単純でした。

f:id:kazuhironagai77:20200315204159p:plain

Actionフォルダーの内部もCombatActionクラスとTestCombatActionクラスがあるだけですね。これなら全体の構造とそれぞれのクラスの役割を理解する事が出来そうです。

1CombatEngineクラス

CombatEngineクラスのコードを見る前に、気を付けておきたい事は、GamemodeクラスでCombatEngineクラスの変数を初期化する時にnewを使用している事なんです。

f:id:kazuhironagai77:20200315204228p:plain

勿論、開放する時はdeleteを使用しています。

f:id:kazuhironagai77:20200315204247p:plain

更に、宣言する時も、UPROPERTYを使用していません。

f:id:kazuhironagai77:20200315204307p:plain

完全に、単なるC++のクラスとして存在しています。

f:id:kazuhironagai77:20200315204352p:plain

CombatEngine.hのクラスの宣言の部分を見ても、UE4C++のどのクラスも継承していません。完全に単なるC++のクラスとして作成してますね。

そう言えば、クラスの宣言の前に、EnumとしてCombatPhaeを宣言しています。これでターン制の戦闘のフェーズを4つに分けています。

f:id:kazuhironagai77:20200315204610p:plain

この発想って、ゲームを普段している人達には当たり前なんでしょうか?これ、私は自分では全く思いつかなかったです。昔、オセロのような対戦型のゲームを作ろうとして、一人で動かす部分は、ほとんど作れたんですが、どうやって自分と相手のプレイを分ければいいのか分からなくて、最後断念したんです。こんな簡単な方法で、分けれるなんで思いもしませんでした。

更にその上には、Includeがありました。

f:id:kazuhironagai77:20200315204639p:plain

CoreMinimal.hをインクルードしていると言う事は、CombatEngineクラスそのものはUE4C++を継承してはいませんが、CombatEngineクラスの中では、UE4C++のクラスは使用すると言う事になるんでしょうか。この場合、UPROPERTYを使用する事でメモリーは開放してもらえるんでしょうか?分かりません。続きを見てみましょう。

まず、メンバー変数が宣言されています。最初にTArrayが使用されています。

f:id:kazuhironagai77:20200315204717p:plain

TArrayやint32を使用しているので、UPROPERTYを使用していないのかと思ったら、UGameCharacterでもUPROPERTYは使用されていません。このクラス内ではUPROPERTYは使用しない(出来ない?)みたいです。それぞれの変数の初期化の方法と開放の方法に注目して続きを読んでいきましょう。

その後で、メンバー関数が宣言されています。

f:id:kazuhironagai77:20200315204743p:plain

どの関数もUFUNCTIONは使用されていません。そう言えばクラスの宣言の時もUCLASSが使用されていませんでしたね。

一つおかしい事に気が付いたのですが、SetPhase()関数があるのですが、Phase変数はPublicなので別にSetterを使用しなくても値を変更出来ますよね。

ConstructorとDestructorがあって、Tick()関数があります。この辺は極めて普通のクラスです。そしてPhase変数のセッターがあり、次のキャラクターを選択する関数があります。これらも特別な関数ではないですね。こんなんで戦闘を管理できるでしょうか?

実装部をみてみましょう。

f:id:kazuhironagai77:20200315204805p:plain

まず、コンストラクターの最初ですが、これって全部コピーしてるんですかね。

TArrayクラスの宣言の所に解説がありました。

f:id:kazuhironagai77:20200315204827p:plain

これを読むとTArrayの要素は全部、ポインタータイプじゃないといけないみたいですね。それなら全部コピーしてもそんなに大変じゃないですね。

=でコピーしているんですが、これってHeap領域に保持されていないですよね。開放する必要もないんでしょうか?デストラクターをみてみます。

f:id:kazuhironagai77:20200315204849p:plain

思いっきり一つ一つ開放していますね。いや開放(delete)はしていませんが、ポインターを外していますね。しかしPlayerParty変数に対しては何もしていません。何故なんでしょう?

まあ、delete[] enemyPartyみたいなのはないのですね。でもそれも良く分からないですね。UPORPERTYを使用しない場合のTArrayの正しい使用方法自体が良く分からないですし。

まあ。今週は軽くいきます。あまり細部に拘って全体が見えなくなっても困りますので。

以下にgamemodeクラスでの、CombatEngineクラスの開放時のコードを示しますが、開放した後で、CombatEngineクラスのenemyPartyをEmpty()しています。

f:id:kazuhironagai77:20200315204933p:plain

PlayerParty変数に対しては何もしないが、enemyParty変数の一つ一つの要素へのポインターを外しているのは、この辺が影響しているのかもしれません。

CombatOrder変数のcombatInstanceをnullptrにするのは当然ですね。このインスタンスが消滅しても、PlayerParty変数内のそれぞれのキャラはGamemodeクラスのPlayerParty変数のキャラとして存在しつつけるから、その間、存在しないCombatEngineクラスのインスタンスを指してしまいますから。

f:id:kazuhironagai77:20200315204955p:plain

この二つの変数は、destructorでは何もされていませんね。普通のC++と同じ扱いと考えて問題ないでしょう。

次は、SetPhase()関数の実装をみます。この関数は、Phase変数のフェーズを変えるだけでなく、SelectNextCharacter()関数を読んでいるんですね。

f:id:kazuhironagai77:20200315205045p:plain

SelectNextCharacter()関数を見てみます。

f:id:kazuhironagai77:20200315205105p:plain

この関数は、単純に次のキャラをcombatantOrderから選んでいるだけなんですが、それ以上に大切な事があるんです。

currentTickTarget変数は、UGameCharacterクラスから作成されたインスタンスで、UPROPERTYを使用しないUE4C++クラスの初期化と開放をどうするのかがこの変数を追う事でわかるはずなんです。

f:id:kazuhironagai77:20200315205224p:plain

そう思ってこの関数内でどのようにcurrentTickTarget変数が扱われているかを見ると、単にGameModeクラスの変数が持つアドレスを渡されているだけですね。

更に全てのキャラのHPが0の場合でもnullptrになるだけですね。

f:id:kazuhironagai77:20200315205311p:plain

最後はTick()関数についてですね。

Tick()関数は最も複雑な実装していますが、それぞれのphaseですべき事を実装しているだけです。

f:id:kazuhironagai77:20200315205410p:plain

だたし、CPHASE_GameOverとCPHASE_Victoryの場合はtrueを返して残りの作業はGameModeクラスのTick()関数内で行います。

2DicisionMakerクラス

DicisionMakerクラスは単なるインターフェイスですね。

f:id:kazuhironagai77:20200315205436p:plain

このままでは使用出来ないので、TestDecisionMakerがこのクラスを継承して使用出来る様にしているのでしょう。

3TestDecisionMakerクラス

以下にヘッダーファイルのクラスの宣言の部分を示しますが、やっぱりそうでした。

f:id:kazuhironagai77:20200315205502p:plain

関数の実装部をみて実際に何を行っているのかも確認します。

f:id:kazuhironagai77:20200315205521p:plain

UGameCharacterクラスのSelectTarget()関数を呼び出しTargetをセットします。そしてそのtargetをargumentとしてTestCombatActionクラスからの変数をnewを使用して初期化します。

UGameCharacterクラスのSelectTarget()関数から見てみましょう。

f:id:kazuhironagai77:20200315205603p:plain

UFUNCTIONもついていない生の関数ですね。

実装部をみてみます。

f:id:kazuhironagai77:20200315205621p:plain

要するに、味方ならenemyPartyから一人、敵を、敵ならplayerPartyから味方を選択する関数ですね。もう戦う相手がいない場合は、nullptrを返すみたいです。

次のnew TestCombatAction()はTestCombatActionクラスの時に調べます。

最後にMakeDecision()関数ですが、trueしか返しません。

f:id:kazuhironagai77:20200315205655p:plain

後で、何か実装する予定だったんでしょうか。

4CombatActionクラス

このクラスもインターフェイスですね。

f:id:kazuhironagai77:20200315205720p:plain

実際の実装をみるために、TestCombatActionクラスを調べます。

5TestCombatActionクラス

以下に示したのが、このクラスのコンストラクターです。

f:id:kazuhironagai77:20200315205743p:plain

こんだけですね。

次に関数を見ていきます。まずBeginExecuteAction()関数からです。

f:id:kazuhironagai77:20200315205803p:plain

この関数は、まず攻撃対象の敵が死んでいないか確認して、死んでいた場合は次の敵を対象にします。もし全部の敵が死んでいたら終了します。

f:id:kazuhironagai77:20200315205830p:plain

最後にdelayTimerを1.0にセットして終了します。

f:id:kazuhironagai77:20200315205851p:plain

f:id:kazuhironagai77:20200315205900p:plain

次に、ExecuteAction()関数をみます。

f:id:kazuhironagai77:20200315205924p:plain

Argumentととしてパスされた時間にマイナスをつけてdelayTimer変数に保持します。そしてdelayTimerの値が0以下の場合はtrueを返します。

この関数はどんな目的で使用するのか、忘れてしまいました。

これで一応CombatEngineの全部のクラスを確認しました。

3.まとめと感想

一応CombatEngineの全部のクラスを確認しました。何故かUE4C++だけでなく単なるC++も結構忘れてしまいました。C++のコードも普段から触れていないと忘れてしまいますね。今週はもう少し深くCombatEngineのコードを見ていこうと思ったのですが、ダラダラやっていたら時間がなくなってしまいました。

何をダラダラやっていたのかと言うと以下のような事を考えていました。

大手、UE4社の子会社であるRPGGames社のプロジェクトマネージャーであるRPGGameModeさんはCombatEngine社に戦闘システムを発注しました。CombatEngine社は小さな会社ですが、この会社だけにある機械、DecisionMakerとCombatActionを持っていて、更にその機械を動かせる専門の技師、TestDecisionMakerさんとTestCombatActionがいます。のでRPGGameModeさんは自社で作成するより良い物が出来ると思い外注を決めたのでした。更に自社で開発するより安上がりなのも魅力でした。

RPGGameModeさんは社内の会議で上司にその事を発注後に報告したのですが、部長が大激怒しました。もう怒って大変です。その部長曰く、もし外注したCombatEngine社が、わざと納期を遅らせたりして支払いの額を余計に要求したらどうするんだ?とか、RPGGameModeさんから見たら難癖にしか思えない事を言ってきます。結局、会議は揉めに揉めて、GameCharacterさんと言う、今までキャラクターの特性を管理していた人が、戦闘システムを担当する事になりました。

と言ったって、もう発注してしまったしGameCharacterさんは戦闘システムの作成方法は全く分からないので、CombatEngine社と共同で開発する以外方法はありません。ただしGameCharacterさんは部長が大激怒した原因は良く分かっています。兎に角、CombatEngine社がイニシアティブが握る事がないようにと考えました。RPGGameModeさんの話によると、CombatEngine社が優れているのは、この会社しかない機械、DecisionMakerとCombatActionを持っていて、更にその機会を動かせる専門の技師、TestDecisionMakerさんとTestCombatActionさんがいるからだと言う事は分かっています。と言うかそれしかGameCharacterさんは分かりません。GameCharacterさんは、まずCombatEngineのみでなくDecisionMakerとCombatActionも自分で管理する事にしました。部長にそれを報告すると、まだ足りないが方向性はそれで良いと、多少機嫌が良くなりました。

CombatEngine社は、RPGGames社でそんな事件が起きているとは想像すらしてなく、既に戦闘システムの作成を開始していました。CombatEngine社の社長であるCombatEngineさんは、ターン制の戦闘が、「行動を決める。」「行動する。」「戦闘に勝利して終わる。」もしくは「戦闘に負けてゲームオーバー。」の4つの状態で行われている事を知っていました。そして、その4つの状態の内、「戦闘に勝利して終わる。」と「戦闘に負けてゲームオーバー。」の二つは、単なる結果で、実際に大切なのは、「行動を決める。」「行動する。」の二つと言う事も知っていました。この二つを実装するために必要な機械であるDecisionMakerとCombatActionの両方持っているのはCombatEngineさんの会社だけです。更にその機械の専門家であるTestDecisionMakerさんとTestCombatActionさんまでいます。素晴らしい戦闘システムが完成するでしょう。

CombatEngine社は頑張って、大まかな戦闘システムの設計を終わらせました。例えば、DecisionMaker のBeginMakeDecisionで実装しなければならないのは、CombatActionの作成である。などです。そしてRPGGames社との打合せの日が決まり、CombatEngine社の社長のCombatEngineさんは張り切ってRPGGames社に行きました。そしたらGameCharacterさんとRPGGames社の部長が対応に出て来て鬼のような顔でこちらを睨んでいます。RPGGameModeさんには会う事も出来ません。CombatEngineさんもこれは何かあったんだなと直ぐに気が付きました。

CombatEngineさんが戦闘システムの説明をするとRPGGames社の部長がイキナリ、「そんな事を勝手にやってもらっては困る。」と怒鳴り出しました。GameCharacterさんも「困りますね。」と言い出しました。CombatEngineさんはどうしていいか分からす鳩が豆鉄砲を食らったような顔になってました。その時、本当はCombatEngineさんは「もうこれは早く退散した方がいいやと。」思ったんですが、RPGGameModeさんとの契約で結構なあなあでやっていた部分があって、このRPGGames社の部長さんを怒らしたまま退散すると後で、契約書の隙をついて訴えられたら困るなと思い黙ってました。

RPGGames社の部長は、とうとうCombatEngineさんに向かって「お前ら底辺の連中は俺たちの生活を破壊しようとしてる。この戦闘システムだってRPGGames社を破壊して俺を困らせるためにやっているんだろう。俺には分かっているんだ。」と言い出しました。そして「全ての戦闘システムはRPGGames社のGameCharacterが管理する。」と言い出しました。

CombatEngineさんは、こんなやべーやつ、さっさと縁を切るべきと思いましたが、RPGGameModeさんとなあなあな契約でやっていたのでそれが出来ません。それでも損切りしても止めるべきかと思ったら、RPGGameModeさんともう一人の老人が突然会議室に入って来ました。その老人が入って来た時、RPGGames社の部長の顔がこわばっていました。その老人はCombatEngineさんに丁寧に「RPGGames社の社長のRPGGamesです。よろしくお願いします。」と挨拶して名刺を渡してきました。その老人の後ろで、RPGGameModeさんがにやにやした笑みをCombatEngineさんに投げかけました。

社長のRPGGamesさんは、部長に対して「お前、戦闘システムは自作できるのか?」とかなり鋭い口調で問い詰めました。部長は「勿論出来ますよ。」と言い返しました。社長は「何時までに?期限ギリギリじゃ間に合わないんだぞ。」と部長に言いました。その瞬間、CombatEngineさんは「その期限までに戦闘システムの作成のノウハウのない会社が自作するのか不可能だと。」思い、無意識に頭を横に振ってしまいました。それを社長は見逃しませんでした。社長の後ろに立っていたRPGGameModeさんもナイスアシストと目でCombatEngineさんに合図を送って来ました。

社長は、CombatEngineさんが頭を横に振るのを見て「絶対戦闘システムを期限内に自作するのは可能である。」と言う部長の言葉を嘘と確信しました。そして今度はGameCharacterさんに向かって「あなたが戦闘システムの作成の責務を負う事になると思いますが、本当に出来るんですか?」と問い詰めました。GameCharacterさんは、もはや部長に側についても益はないと見切ったのか「出来ないと思います。なぜなら我が社には、CombatEngine社が持っている機械、DecisionMakerとCombatActionもなければ、その機械を専門に扱う技師もいないからです。」と言いました。

そしたら、社長は今度は部長に対して「出来ないんなら、出来る人にお願いしてやってもらう以外ないんじゃないの?」と言い放ちました。部長はモゴモゴ言っていましたが、最後には「その通りです。」とはっきり社長に言いました。しかしその後で、部長は戦闘システムがいかにRPGGames社に、取って大切であるかを述べて、それを外部にまかせてしまっては危険過ぎると。そしてそれを防ぐためには、逐一管理をして、不正がしたくても出来ないようにすべきだ。と社長に対して演説を始めました。社長は、部長のその話は結構真剣に聞いていました。そしてCombatEngineさんに向かってこう言いました。

「すいません。わが社ではCombatEngineさんほどの戦闘システムを期限内に自作する事は出来ませんので、こんなん成ってしまいましたが戦闘システムの制作をお願いしたいです。それでよろしいでしょうか?」

CombatEngineさんは「勿論です。」と返事をしましたが、その瞬間RPGGames社の社長はこう続けました。

「申し訳ないがもう一点あります。戦闘システムはわが社にとっても背骨にあたる箇所になります。失敗すれば倒産するかもしれません。のでわが社で逐一管理したい。この条件も飲んでもらえないでしょうか?」

CombatEngineさんは「おお」と感動しました。CombatEngineさんの立場も立てつつ、部長の意見も完全に取り入れた対応をしています。CombatEngineさんは「分かりました。」と返事しました。

そしたら、RPGGames社の社長は、GameCharacterさんに向かって「じゃあ。管理の方はよろしく。」と言って、RPGGameModeさんに何か耳打ちしました。そしてCombatEngineさんに「私も忙しいのでこれで失礼します。細かい打合せは、RPGGameModeとGameCharacterにお願いします。」と言って部長を連れて去ってしまいました。

それでCombatEngine社の作成したCombatEngineですが、一々、GameCharacterを通す仕組みになりました。例えばDecisionMaker のBeginMakeDecisionでのCombatActionの作成ですが、作成するためにGameCharacterにアクセスする。と言ったようにです。次のターゲットを選ぶ時も、GameCharacterにお伺いを立ててから、選ぶような設計になりました。

何で、一々、GameCharacterクラスが戦闘システムの進行に絡んでくるんだと考えていたら、こんな壮大な話を作ってしまいました。

来週もう少し深く考察します。