UE4の勉強記録

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

「Unreal Engine4.xを使用してRPGを作成する」の3章の3.4を勉強する part 2

<前文>

f:id:kazuhironagai77:20190526204904p:plain

今回はちょっとだけ政治について語りたいと思います。『学んで思わざれば則(すなわ)ち罔(くら)し思うて学ばざれば則ち殆(あやう)し』という言葉を引用して<考察>では考える事に集中し、<方法と結果>では学ぶ事に集中する事の大切さを述べようと思ったのですがある事を思い出したのでその話を書いておく事に変更しました。

この漢文を知ったのは渡部昇一氏の著書からだったと思います。右翼とは怖い人だがバカな人達のイメージしかなかった私でしたが氏の著書を読んで氏のキレッキレの内容に目から鱗で世の中の見方が変わったのを覚えています。氏はその著書で難しい事を何度も述べていますが言いたい事は単純です。それは「人として生きる限り義を通さなければならない。」です。今ほど日本の政治で右が支持されている時代はないと思われますが、現在の右派の人達は何故か渡部昇一氏の著書で述べた日本人のあるべき姿の真逆を行っているように私には見えます。

それでは勉強して行きましょう。

<本文>

<目的>

前回に引き続きの3章3節の4、ターン制の戦闘(turn based combat)を勉強していきます。

<方法と結果>

前回はcombat engineクラスのコンストラクターとデストラクタ―を書き換えた所で終了したのでその後の続きから行っていきます。

<Step.0>

前回まで使用していたCh2をそのまま今回も使用します。

<Step.1>

f:id:kazuhironagai77:20190609204419p:plain

教科書通りに作成しました。ここではTArrayでUGameCharacterクラスのenemyParty変数も作成しました。しましたが教科書の説明ではUPROPERTYに指定して開放はガーベージコレクション(garbage collection)に任せると書かれています。しかしサンプルコードではenemyParty変数はUPROPERTYで指定はされていません。どっちが正しいのでしょうか?

<Step.2>

RPGGameModeクラスに追加したTick関数を実装します。

f:id:kazuhironagai77:20190609204525p:plain

UGameplayStaticsがエラーになっているので、

f:id:kazuhironagai77:20190609204615p:plain

を追加します。

f:id:kazuhironagai77:20190609204637p:plain

今度はGetWorld()がエラーになりました。直します。直しますがこのGetWorld()関数、AActorクラスのGetWorld()かUWorldクラスのGetWold() 関数のどちらなのでしょうか?

UE4C++のGetPlayerController()関数のAPIを見てみると

f:id:kazuhironagai77:20190609205924p:plain

と書かれていました。うーん。分かりません。とりあえずUWorldクラスの方でインクルードします。

f:id:kazuhironagai77:20190609205950p:plain

f:id:kazuhironagai77:20190609210022p:plain

見事にエラーは消えました。ビルドしても以下に示すように成功しました。

f:id:kazuhironagai77:20190609210129p:plain

GetWorld()にカーソルを重ねると何故か以下に示すようにAActorクラスのGetWorld()関数であると表示されました。

f:id:kazuhironagai77:20190609210201p:plain

エラーにもならないしビルドも成功しているのでこれは無視して行きます。

教科書にenemyParty変数のUPROPERTYとガーベージコレクション(garbage collection)についての解説が載っていました。

f:id:kazuhironagai77:20190609210333p:plain

…そして敵のパーティのリストを消去します。(これで敵はガーベージコレクションに適用されるます。何故ならそのリストはUPROPERTYのマクロによって修飾されているからです。

と書かれていました。なのでやっぱりenemyParty変数にはUPROPERTY()が必要な気がします。

f:id:kazuhironagai77:20190609210428p:plain

と念のために書いておきます。(ここは考察で検討する事にします。)

<Step.3>

UGameCharacterクラスに敵のキャラクターを作成する関数を追加します。

f:id:kazuhironagai77:20190609210509p:plain

f:id:kazuhironagai77:20190609210519p:plain

敵にはClassはないのでClassInfoはnullptrです。敵は魔法も使えないので0です。今気が付いたのですがゴブリンは名前じゃないですよね。

f:id:kazuhironagai77:20190609210557p:plain

種族なのでしょうか?英語だとcreaturesが正しいのでしょうか?

UGameCharacterクラスのポインターのオブジェクトであるcharacterを初期化する場合、NewObject関数が使用されています。確か前も復習しましたがUnreal Engine 4 Scripting with C++ Cookbookの第2章クラスの作成に大変詳しい解説が載っています。もう一度勉強しましょう。

f:id:kazuhironagai77:20190609210627p:plain

AActorクラスの派生ではないUObject由来のクラスの初期化にはUWorld::SpawnActor<>は使用しません。

代わりに特別なグローバル関数、ConstructObject<>もしくはNewObject<>を使用します。

生のC++のキーワードであるnewUE4UObjectクラスの派生クラスの新しいインスタンスのための割り当てには使用しないでください。

はい。あれ?CreateDefaultSubobject()関数との関係が書かれていませんね。うーん。すっかり忘れてしまいました。この続きは考察で行います。

<Step.4>

コンバットシステムをテストするためにUnrealのコンソールから呼ぶ事の出来るRPGGameMode.h内に新しい関数を作成します。

f:id:kazuhironagai77:20190609210757p:plain

作成しました。Execは初めて見ますね。教科書では

UFUNCTION(exec)マクロはこの関数がコンソールのコマンドで呼ばれる事を許可します。

と説明されていました。

次に教科書通りにTestCombat()関数を実装しました。

f:id:kazuhironagai77:20190609210844p:plain

この後、教科書ではcombat システムのテストを実行しています。まずそのテストを実行してみましょう。

f:id:kazuhironagai77:20190609210904p:plain

キャラクターの名前が読めませんね。全角で打ってしまったのかもしれません。

キャラクターの名前を打ち直します。

f:id:kazuhironagai77:20190609210927p:plain

もう一度トライします。

f:id:kazuhironagai77:20190609210956p:plain

綺麗に出て来ました。教科書の結果と同じですね。

TestCombat()関数の実装部を見ていきましょう。

最初のコードは敵の情報を読み込むだけで前にやったキャラクターの情報を読み込むコードと全く同じなので無視します。

ここからコンバットシステムをテストするためのコードが始まります。

f:id:kazuhironagai77:20190609211028p:plain

アクターのtickを止めます。うーん。何でこれが必要なのでしょうか?

f:id:kazuhironagai77:20190609211058p:plain

最後から2番目のコードがCombatEngineを初期化してここでコンバットシステムのテストを開始しています。それ以外のコードはこのコードを動かすために必要なオブジェクトなどを作成するためのものですね。

切りが良いので今週はここで終わりにします。

<考察>

<Step.1>

UPROPERTYとenemyParty変数について

ここでもう一度

f:id:kazuhironagai77:20190609211205p:plain

について考えてみようと思います。これってUPROPERTYとUGameCharacterクラスの関係だけでなくUPROPERTYとTArrayの関係でもあるんですね。

f:id:kazuhironagai77:20190609211225p:plain

f:id:kazuhironagai77:20190609211233p:plain

配列を空にします。もし必要ならデストラクタ―を呼びます。

とありました。この説明が正しければUPROPERTYが無くてもEmpty()関数が呼ばれた時に少なくとも配列内の要素は開放されていると考えられます。

簡単なテストをしてみます。

新しいプロジェクトを作成してGameModeクラスのコンストラクター内に以下のコードを追加しました。

f:id:kazuhironagai77:20190609211327p:plain

UMyObjectクラスは私が作成したUObjectクラスから派生したクラスです。

ヘッダーファイルでは以下の様に変数は宣言されています。TArrayにはUPROPERTYは付けていません。

f:id:kazuhironagai77:20190609211353p:plain

これでデバックを行います。

f:id:kazuhironagai77:20190609211414p:plain

今、test.Empty();の手前で止まっています。&testと打ち込みtest のメモリー領域を表示させます。

f:id:kazuhironagai77:20190609211443p:plain

Step overを押してEmpty()を実行します。

f:id:kazuhironagai77:20190609211522p:plain

0になりました。

と言う事は少なくともTArray内の配列の要素は開放されていると言う事でしょうか?

もう少し細かく見てみます。

前回と同様にtestのメモリー領域を表示しました。

f:id:kazuhironagai77:20190609211557p:plain

しかしここに表示されているアドレスはmyObjectのアドレスと同じではありません。

f:id:kazuhironagai77:20190609211621p:plain

Testに表示されているアドレスを見てみます。

アドレスを逆に打ち込みます。little endianと big endianというヤツですね。

f:id:kazuhironagai77:20190609211704p:plain

今度はmyObjectと同じアドレスを指しています。

Step overを押してEmpty()を実行します。

f:id:kazuhironagai77:20190609211723p:plain

アドレスが変わりましたね。

ここからが考察になりますがmyObject変数はUPROPERTY()が付いているので時期がくれば開放されるはずです。しかしTArrayの変数であるtestからのポインターがmyObjectの実態を指していたらスマートポインターの関係で開放されないんじゃないかなーと思います。しかしempty()関数をTArrayの変数であるtestが既に使用していたらもうtestはmyObjectの実態を指していないのでその時は開放出来るかも。と考えられます。

となるとempty()関数が配列を空にするのでUPROPERTYを特別につけなくてもTArrayは良いのかもしれません。

あくまで可能性ですけど。

しかしもっと実践的に考えるとUPROPERTYを付けておいて悪い理由はないですよね。良く分からなければつけておいた方が良いと言う考え方も出来ます。

<Step.2>

GetWorld()関数について

実はGetWorld() 関数はアクタークラスからの関数と判明した後でインクルードを”Runtime/Engine/Classes/Engine/World.h”から” Runtime/Engine/Classes/GameFramework/Actor.h”に変えたんです。そしたらエラーになってしまい良く分からないので基に戻しました。これがおかしくない事を理解したいです。

GetWorld() 関数は今一良く理解していない関数です。更にAActor::GetWorld()関数とUWorld::GetWorld()関数があります。ここで勉強し直します。

f:id:kazuhironagai77:20190609211844p:plain

あれ?これは前にも訳した記憶があります。となると考える方が足りないのかもしれません。

ちょっと自分だけで考えてみます。

UWorld::GetWorld()関数はUWorldを返す関数ですがそのためにUWorldに予めアクセスする必要があるのか無理がありそうです。となると実際はアクターからUWorldにアクセスするしかないのかもしれません。となるとGetWorld() 関数はAActor::GetWorld()関数にならざるえないのかもしれません。

インクルードを”Runtime/Engine/Classes/Engine/World.h”から” Runtime/Engine/Classes/GameFramework/Actor.h”に変えたらエラーになるのは当たり前でした。返し値のタイプであるUWorldクラスのインクルードが必要なのですから。

これは考察でやるほど大した問題ではなかったかもしれませんね。

UPROPERTYとenemyParty変数についてもう一度

f:id:kazuhironagai77:20190609211932p:plain

Step.1の考察中に気が付いたのですが教科書のサンプルコードではTArrayの要素であるenemyはcpp内で宣言されているんです。これって勝手に開放されるんですか?UPROPERTYの無い変数の取り扱いはどうなっているのでしょうか?

Step.1のテストに使用したTArrayの配列の要素であるUMyObjectをcpp内で宣言初期化してみました。

f:id:kazuhironagai77:20190609212015p:plain

{}の外側では思いっきりエラーになっていますね。開放されるみたいですね。強引にビルドしましたがエラーになりました。

となると

f:id:kazuhironagai77:20190609212039p:plain

f:id:kazuhironagai77:20190609212047p:plain

はOKですが、

f:id:kazuhironagai77:20190609212109p:plain

は駄目であるとかの理由が良く分かりますね。

<Step.3>

ゴブリンは名前じゃないよね。

これって日本語だと種族で終ですが、英語だと何なんでしょうか?racesとかcreaturesとか出てくるんですが何かしっくりしません。第一raceだったらゴブリンは人間になってしまうじゃないですか。ゴブリンにも人権があるのでしょうか?そしたら冒険者の方が野蛮な侵略者になってしまうじゃないですか。これはまずいでしょう。そういえばアメリカ人のルームメイトでゴブリンはcreatureで人じゃないと強行に主張していた人がいたのを思い出しました。全然関係ないかもしれませんが彼は白人でした。ゴブリンが人だとしてもこの敵の種族にドラゴンが入るのは確実なのでraceは使えません。でもcreatureもしっくりしません。日本語の種族にあたる英語はなんでしょうか?Speciesですよね。でも全くgoogleで引っかからない。Fantasy creaturesに変換されてしまいます。

CreateDefaultSubobject()関数とConstructObject<>関数もしくはNewObject<>関数についての復習

これは書いている間に思い出したのですが、コンストラクター内で初期化する場合はCreateDefaultSubobject()関数、それ以外で初期化する場合はConstructObject<>関数もしくはNewObject<>関数だったと思います。後どちらかの関数で初期化する変数を持つクラスをこの関数で初期化した場合ある条件だとエラーになってしまうみたいな話しがあったと思います。結構覚えていたので復習はまた今度します。

<Step.4>

UFUNCTION(exec)について

このサイトに詳しい解説が書かれていました。

この関数を書いていた時は全く初めて見るexecに対して絶対に考察が必要と思っていましたが、何回もtestCombat関数をplay実行中にコマンドで使用するうちに完全に馴染んでしまいました。慣れてしまえばどうと言う事はないです。

アクターのtickを止めるのは何故?

ソースフォルダー内にある全てのアクターを見たのですがTick関数で特別な事をしているアクターは見つかりませんでした。

分からないので消して試してみます。

f:id:kazuhironagai77:20190609212218p:plain

問題なく動きました。

f:id:kazuhironagai77:20190609212236p:plain

理由は不明です。