UE4の勉強記録

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

12章13節 GameplayTasks API – GameplayTasksを使用して実現する

<前文>

f:id:kazuhironagai77:20190210122704p:plain

<本文>

<目的>

今回のレシピはGameplayTasks API のGameplayTasksを使用して何かを実現するそうです。GameplayTasks APIのUE4C++からの使用方法の一例を学ぶのが目的になります。

GameplayTasks APIそのものの目的は何でしょうか?

教科書では、GameplayTasks目的は、以下に示すように説明されていました。

f:id:kazuhironagai77:20190310182842p:plain

GameplayTasksは、再利用可能なオブジェクト内のあるgamplayの関数をラップするために使用されます。

フーンと言った感じですか。取りあえず意味は分かります。ざっとHow to do it…を読んだところ、実装出来そうなのでやってみながらGameplayTasks APIの目的についても考えていきます。

<方法>

Step.0

新しいプロジェクトでやります。名前はChapter12part12です。アクタークラスの派生クラスを作成するみたいなので、Basic codeを選択します。Versionは4.22.0 preview 3です。

Step.1

f:id:kazuhironagai77:20190310182954p:plain

はい。当然ですね。以下のように追加しました。

f:id:kazuhironagai77:20190310183021p:plain

Step.2

f:id:kazuhironagai77:20190310183053p:plain

以下に示すように、GameplayTasksを選択します。

f:id:kazuhironagai77:20190310183130p:plain

名前は、教科書通りにGameplayTask_CreateParticlesとして、Publicを選択しました。

f:id:kazuhironagai77:20190310183151p:plain

Publicを選択した理由はサンプルコードではPublicで作成されていたからです。

以下に示すように、何故かcppファイルでエラーになっています。

f:id:kazuhironagai77:20190310183215p:plain

試しにビルドしてみると、

普通に出来たので、そのまま行きます。

f:id:kazuhironagai77:20190310183236p:plain

Step.3

f:id:kazuhironagai77:20190310183318p:plain

f:id:kazuhironagai77:20190310183327p:plain

サンプルコードは以下のように書かれていました。

f:id:kazuhironagai77:20190310183347p:plain

教科書とサンプルコードが微妙に違います。まずUFNCTION()が教科書の例にはありません。

教科書はUGameplayTask_CreateParticles::ConstructTaskですが、サンプルコードは単にConstructTaskです。これは教科書はCppファイルの説明でしょうか?

UParticleSystem*がエラーを吐いているので、

f:id:kazuhironagai77:20190310183413p:plain

を追加しました。

f:id:kazuhironagai77:20190310183435p:plain

勿論エラーは消えました。

この後、ビルドしたらエラーになってしまい、DLLファイルにアクセス出来ないとメッセ―ジが出て来たので、以下の方法でプロジェクトから作り直しました。

1.  以下に示すフォルダーから.vs、Binaries、Intermediates、Saved、slnを消す。

f:id:kazuhironagai77:20190310183502p:plain

2. uprojectをクリックしてそれらのファイルを新しく作成する。

3. 最後にuprojectを右クリックしてVSのsolutionを新しく作成。

f:id:kazuhironagai77:20190310183617p:plain

普通にビルド出来ました。

この後、サンプルコードの方のUGameplayTask_CreateParticlesのcppファイルを見ると、案の定、実装部が書かれていましたのでそれをコピーしました。

f:id:kazuhironagai77:20190310183642p:plain

ParticleSytemとLocation変数はまだ宣言していないので、ヘッダーファイルに追加します。

f:id:kazuhironagai77:20190310183709p:plain

試しにビルドすると

f:id:kazuhironagai77:20190310183733p:plain

成功しました。

Static constructorは初めて知った言葉なのでちょっとだけ調べて見ます。

C#で使用されている用法らしくて、このサイトによれば、

f:id:kazuhironagai77:20190310183800p:plain

スタテックコンストラクターはあらゆるスタテックなデータを初期化するため、もしくは、一回きりの行いが必要な特定のアクションを行うために使用されます。

最初のインスタンスが作成される前に、もしくはいずれかのスタテックなメンバーが参照されると自動的に呼ばれます。

一回限りしか使用しないアクションを持つ関数を持つクラスを初期化する場合に使用するみたいです。

C++がstatic constructorがない理由とちょっと工夫するとC++でも作成出来る例がネットに載っていました。後、C++の仕事の面接でstatic constructorについて質問された例も載っていました。これは結構濃い内容みたいですので後で勉強します。

ConstructTask()関数のパラメーターですが、

f:id:kazuhironagai77:20190310183848p:plain

この関数を少し調べて見たいです。まずTScriptInterfaceテンプレートについて、そしてIGameplayTaskOwnerInterfaceについてです。

TScriptInterfaceテンプレートはUE4C++のAPIによると、

f:id:kazuhironagai77:20190310183918p:plain

FScriptInterfaceのテンプレートバージョン。アクセサーとオペレーターを提供します。

と書かれています。と言うかインターフェイスをクラスみたく扱う事が出来るのでしょうか?今の今まで、全くインターフェイスをクラスのようにテンプレートに使用する事、その変数を作成する事について疑問を持ちませんでした。ひょっとして結構今までもインターフェイスをクラスのように使用していたのでしょうか?

インターフェイスをクラスみたく扱う事についても、少し調べただけでは良く分からないので後で勉強します。

Step.4

f:id:kazuhironagai77:20190310184132p:plain

f:id:kazuhironagai77:20190310184145p:plain

以下に示すように、そのまま入れるとエラーですね。

f:id:kazuhironagai77:20190310184207p:plain

ちなみに、サンプルコードの方は、

f:id:kazuhironagai77:20190310184226p:plain

となっていて、少し違います。

まず、Activate()関数が本当にUGameplayTaskクラスにあるのか確認します。

ありました。ただし、protectedでした。

サンプルコードの方はActivate()関数をpublicに置いていましたが、最新のバージョンではprotectだったですね。

f:id:kazuhironagai77:20190310184250p:plain

直しました。

UE4C++のUGameplayStaticsのAPIをみると、SpawnEmitterAtLocationのオーバーロードは3つありましたが、どれも教科書のパラメ―ターとは違いました。

f:id:kazuhironagai77:20190310184306p:plain

となっていて最初のパラメーターはGetWorld()でいい事が分かります。次のパラメーターはUParticleSystemなのでサンプルコードのように、ParticleSystemをそのまま渡せばいい事が分かります。三番目が問題です。SpawnEmitterAtLocation関数は、FTransform構造体を望んでいるのに、FVectorを渡しています。

以下に示すように直しました。

f:id:kazuhironagai77:20190310184325p:plain

ヘッダーファイル上でFTransform変数を宣言します。

ソースファイルのコンストラクター内で、初期化します。

f:id:kazuhironagai77:20190310184344p:plain

このFTransform構造体をSpawnEmitterAtLocation関数にパスします。

f:id:kazuhironagai77:20190310184401p:plain

これでエラーは消えました。残り二つのパラメーターはデファルト値があるようです。

f:id:kazuhironagai77:20190310184418p:plain

ビルドしても大丈夫でした。

Step.5

f:id:kazuhironagai77:20190310184442p:plain

うーん。アクターの派生クラスにGameplayTaskComponentを追加とありますが、BPエディターから追加するとあります。となるとActorの派生クラスをC++で作成。そこからBPを作成。そのBP内からGameplayTaskComponentを追加するとなるのでしょうか?

それ以外のやり方は思いつかないので、それで行きます。

f:id:kazuhironagai77:20190310184458p:plain

一応は出来ましたが、Actorの方の設定はどうすべきなんでしょうか。SceneRootすら指定していないのですが。取りあえず出来る所まで、これでやって無理だったらアクターの派生クラスをもう一度作り直します。

Step.6

f:id:kazuhironagai77:20190310184531p:plain

f:id:kazuhironagai77:20190310184538p:plain

いや、これは絶対オカシイです。C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらないでしょう。イヤ、変わるんでしょうか?

サンプルコードの方をみると、Warrior.hファイルには、C++からしっかりとUGameplayTaskComponentの変数を作成しています。

f:id:kazuhironagai77:20190310184558p:plain

こっちが絶対正しいでしょう。

しかし「C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらない」が真実であるかは確認したほうが良い気がしますので、以下のテストをします。

以下の変数をアクタークラスの派生クラスであるmyActorクラスに追加します。

f:id:kazuhironagai77:20190310184625p:plain

f:id:kazuhironagai77:20190310184643p:plain

ビルドします。そしてそのmyActorクラスから前に作成したBPであるBPmyActorクラスのBPエディターを開きます。そこのVariablesを見ると、

f:id:kazuhironagai77:20190310184700p:plain

あれ?出来てる。

・・・・

変わるみたいですね。

C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらない」は間違っていました。

確認して良かった。

C++のアクターの派生クラスとそこから作成したBPは同じものなので、後で、C++にコードを足してもBPの方は自動で更新されます。」が正しかったです。

となると、教科書のやり方でもサンプルコードのやり方でもどちらでも正しいと言う事ですね。

教科書の方法でやろうと思ったのですが、教科書の方法だと初期化の仕方が分からない。BPで初期化しなければならないのかもしれないし、しなくてもいいのかもしれない。のでサンプルコードの方法でやります。

サンプルコードではWarrior.cppファイルのコンストラクター内で以下の方法でGameplayTaskComponent変数を初期化しています。

f:id:kazuhironagai77:20190310184720p:plain

ので以下に示したようにGameplayTaskComponent変数を初期化しました。

f:id:kazuhironagai77:20190310184736p:plain

残りのコードはPostInitializeComponents()関数内に実装されているので、まずPostInitializeComponents()をmyActorクラスに作成します。

f:id:kazuhironagai77:20190310184802p:plain

f:id:kazuhironagai77:20190310184812p:plain

ビルドも成功したので、これで行きます。

Step.7

f:id:kazuhironagai77:20190310184834p:plain

はい。

<結果>

では、試してみます。

myActorBPをレベル内に配置します。

f:id:kazuhironagai77:20190310184856p:plain

配置したmyActorBPを選択してDetailsをみます。

f:id:kazuhironagai77:20190310184913p:plain

GameplayTaskComponentはここで選択しないでいいはずですが、ParticleSytemはここで指定しないといけないはずです。スターターキッドについているP_Explosionを追加しました。

f:id:kazuhironagai77:20190310184935p:plain

Playを実行すると、

f:id:kazuhironagai77:20190310184954p:plain

何も見えませんね。では、直していきましょう。

デバックしてみると、taskが初期化されていないようです。

f:id:kazuhironagai77:20190310185011p:plain

この TaskOwnerがIGameplayTaskOwnerInterfaceであるべきなんですが、サンプルのコードはActorの派生クラスをパスしているんです。

f:id:kazuhironagai77:20190310185027p:plain

これが、taskが作られない原因みたいです。

とここまで書いて、Actorの派生クラスをIGameplayTaskOwnerInterfaceで継承すればいいじゃないか。と気が付きました。

サンプルコードにあるWarriorクラスをもう一度見直すと、

f:id:kazuhironagai77:20190310185043p:plain

思いっきり継承していました。

f:id:kazuhironagai77:20190310185057p:plain

今度はIGameplayTaskOwnerInterfaceをアクターの派生クラスに継承させて試してみます。

今度はexceptionが投げられて停止しました。ただしVSのoutputには以下のように書かれていました。

f:id:kazuhironagai77:20190310185114p:plain

IGameplayTaskOwnerInterface.hの26行目をみると、以下の関数が書かれていました。

f:id:kazuhironagai77:20190310185134p:plain

この関数をオーバーライドしないといけないのかなとサンプルコードの中のWarrior.hを見てみると、

f:id:kazuhironagai77:20190310185224p:plain

してました。のでこれをコピーします。

またexceptionが投げられました。今度は、

f:id:kazuhironagai77:20190310185255p:plain

が問題だそうです。実際のコードをみてみると、

f:id:kazuhironagai77:20190310185315p:plain

がエラーを発したようです。

f:id:kazuhironagai77:20190310185333p:plain

うーん。良く分からん。のでサンプルコードの中のWarrior.hをもう一度みたら

f:id:kazuhironagai77:20190310185402p:plain

GetGameplayTasksComponent関数以外にも関数を追加しているので、これも追加します。

f:id:kazuhironagai77:20190310185444p:plain

Info(FS…の部分は省きました。

も一度やって見ると同じでした。

これがexceptionを投げているようです。

f:id:kazuhironagai77:20190310185503p:plain

やっぱり元に戻りますが、

f:id:kazuhironagai77:20190310185535p:plain

これを直さないと先に進めないです。

それぞれのパラメーターが何を指しているのかは分からないですが、bCareAboutPriorityがfalse、かつRequiredResources.IsEmpty()がtrue、更にClaimedResources.IsEmpty()もtrueなはずです。

Taskのproperties をデバックで調べて見ると、bCareAboutPriorityは0, RequiredResourcesも0, そしてClaimedResourcesも0の値を保持しています。

f:id:kazuhironagai77:20190310185559p:plain

C++では0はfalse,それ以外の数字はtrueなので、bCareAboutPriorityはfalse、よってbCareAboutPriority==trueはfalse。RequireResourceとClaimedResourcesは0なのでisEmpty()がtrueとなると、RequiredResources.IsEmpty()==falseとClaimedResources.IsEmpty()==falseがtrue == falseになってfalse、つまり(false||false||false) となりfalseが返されています。

ここまでは分かりますが、ならどうやってこれらのプロパティ―の値を変えるのかは全く分かりません。

Taskのプロパティは下記の部分で値を指定していますが、bCareAboutPriorityなどもここから指定すべきなのでしょうか?

f:id:kazuhironagai77:20190310185621p:plain

それとも、これらの値は他のTaskを作成する時にパスしたパラメーターが正しければ、自然と指定されるのでしょうか?

いつもならここから、ネットで検索かけてGameplayTasksのチュートリアルを片っ端から当たるのですが、それをやると来週もGameplayTasksの勉強になってしまいます。3月中にはこの教科書の勉強を終わせる予定から遅れてしまいます。更に、今回の勉強の目的は絶対この教科書に書かれているレシピを再現するのではなく、この教科書を使用してUE4C++の勉強をするのはどうなのかを判断するためなので、ある程度で諦めるのも必要です。更に絶対再現出来ないコードである可能性もあるので、今回はここで中止します。

<まとめ>

今回はGameplayTasksをアクタークラスからの派生クラスから読んで実行する方法について勉強しました。しかし実際にはレシピの言っているようには動きませんでした。ただしサンプルコードとレシピの説明をみると、何かを加えれば正しく動く可能性もあります。

<おまけ>

  1. スタテックコンストラクターについて

NewTask関数が正しく働かないで直している時に気が付いたのですが、これはシングルトンの一種ですね。あるクラスを作成してそのオブジェクトを一個しか作成したくない時、どうするかの問題ですね。ここでの解決策は、コンストラクターをprivateにして、そのコンストラクターにアクセス出来るメンバー関数を一つだけ作成してそれをstaticにすれば、そのクラスのオブジェクトは一個しか作成されません。これがここで言うスタテックコンストラクターでした。

こんなのいきなりJob Interviewで聞かれたら舞い上がって答えられないですが、知っていればフーンで済む事でした。それよりもシングルトンそのもの使用の是非についての方が大切と思います。

  1. インターフェイスをクラスみたく扱う事

今回のレシピでテンプレートでインターフェイスをクラスのように使用しているのですが、こんなの可能なのかと驚いたのですが、実際はそのインターフェイスを継承したクラスをパスしていました。これなら納得です。