<前文>
<本文>
<目的>
今回のレシピはGameplayTasks API のGameplayTasksを使用して何かを実現するそうです。GameplayTasks APIのUE4C++からの使用方法の一例を学ぶのが目的になります。
GameplayTasks APIそのものの目的は何でしょうか?
教科書では、GameplayTasks目的は、以下に示すように説明されていました。
GameplayTasksは、再利用可能なオブジェクト内のあるgamplayの関数をラップするために使用されます。
フーンと言った感じですか。取りあえず意味は分かります。ざっとHow to do it…を読んだところ、実装出来そうなのでやってみながらGameplayTasks APIの目的についても考えていきます。
<方法>
Step.0
新しいプロジェクトでやります。名前はChapter12part12です。アクタークラスの派生クラスを作成するみたいなので、Basic codeを選択します。Versionは4.22.0 preview 3です。
Step.1
はい。当然ですね。以下のように追加しました。
Step.2
以下に示すように、GameplayTasksを選択します。
名前は、教科書通りにGameplayTask_CreateParticlesとして、Publicを選択しました。
Publicを選択した理由はサンプルコードではPublicで作成されていたからです。
以下に示すように、何故かcppファイルでエラーになっています。
試しにビルドしてみると、
普通に出来たので、そのまま行きます。
Step.3
サンプルコードは以下のように書かれていました。
教科書とサンプルコードが微妙に違います。まずUFNCTION()が教科書の例にはありません。
教科書はUGameplayTask_CreateParticles::ConstructTaskですが、サンプルコードは単にConstructTaskです。これは教科書はCppファイルの説明でしょうか?
UParticleSystem*がエラーを吐いているので、
を追加しました。
勿論エラーは消えました。
この後、ビルドしたらエラーになってしまい、DLLファイルにアクセス出来ないとメッセ―ジが出て来たので、以下の方法でプロジェクトから作り直しました。
1. 以下に示すフォルダーから.vs、Binaries、Intermediates、Saved、slnを消す。
2. uprojectをクリックしてそれらのファイルを新しく作成する。
3. 最後にuprojectを右クリックしてVSのsolutionを新しく作成。
普通にビルド出来ました。
この後、サンプルコードの方のUGameplayTask_CreateParticlesのcppファイルを見ると、案の定、実装部が書かれていましたのでそれをコピーしました。
ParticleSytemとLocation変数はまだ宣言していないので、ヘッダーファイルに追加します。
試しにビルドすると
成功しました。
Static constructorは初めて知った言葉なのでちょっとだけ調べて見ます。
スタテックコンストラクターはあらゆるスタテックなデータを初期化するため、もしくは、一回きりの行いが必要な特定のアクションを行うために使用されます。
最初のインスタンスが作成される前に、もしくはいずれかのスタテックなメンバーが参照されると自動的に呼ばれます。
一回限りしか使用しないアクションを持つ関数を持つクラスを初期化する場合に使用するみたいです。
C++がstatic constructorがない理由とちょっと工夫するとC++でも作成出来る例がネットに載っていました。後、C++の仕事の面接でstatic constructorについて質問された例も載っていました。これは結構濃い内容みたいですので後で勉強します。
ConstructTask()関数のパラメーターですが、
この関数を少し調べて見たいです。まずTScriptInterfaceテンプレートについて、そしてIGameplayTaskOwnerInterfaceについてです。
TScriptInterfaceテンプレートはUE4C++のAPIによると、
FScriptInterfaceのテンプレートバージョン。アクセサーとオペレーターを提供します。
と書かれています。と言うかインターフェイスをクラスみたく扱う事が出来るのでしょうか?今の今まで、全くインターフェイスをクラスのようにテンプレートに使用する事、その変数を作成する事について疑問を持ちませんでした。ひょっとして結構今までもインターフェイスをクラスのように使用していたのでしょうか?
インターフェイスをクラスみたく扱う事についても、少し調べただけでは良く分からないので後で勉強します。
Step.4
以下に示すように、そのまま入れるとエラーですね。
ちなみに、サンプルコードの方は、
となっていて、少し違います。
まず、Activate()関数が本当にUGameplayTaskクラスにあるのか確認します。
ありました。ただし、protectedでした。
サンプルコードの方はActivate()関数をpublicに置いていましたが、最新のバージョンではprotectだったですね。
直しました。
UE4C++のUGameplayStaticsのAPIをみると、SpawnEmitterAtLocationのオーバーロードは3つありましたが、どれも教科書のパラメ―ターとは違いました。
となっていて最初のパラメーターはGetWorld()でいい事が分かります。次のパラメーターはUParticleSystemなのでサンプルコードのように、ParticleSystemをそのまま渡せばいい事が分かります。三番目が問題です。SpawnEmitterAtLocation関数は、FTransform構造体を望んでいるのに、FVectorを渡しています。
以下に示すように直しました。
ヘッダーファイル上でFTransform変数を宣言します。
ソースファイルのコンストラクター内で、初期化します。
このFTransform構造体をSpawnEmitterAtLocation関数にパスします。
これでエラーは消えました。残り二つのパラメーターはデファルト値があるようです。
ビルドしても大丈夫でした。
Step.5
うーん。アクターの派生クラスにGameplayTaskComponentを追加とありますが、BPエディターから追加するとあります。となるとActorの派生クラスをC++で作成。そこからBPを作成。そのBP内からGameplayTaskComponentを追加するとなるのでしょうか?
それ以外のやり方は思いつかないので、それで行きます。
一応は出来ましたが、Actorの方の設定はどうすべきなんでしょうか。SceneRootすら指定していないのですが。取りあえず出来る所まで、これでやって無理だったらアクターの派生クラスをもう一度作り直します。
Step.6
いや、これは絶対オカシイです。C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらないでしょう。イヤ、変わるんでしょうか?
サンプルコードの方をみると、Warrior.hファイルには、C++側からしっかりとUGameplayTaskComponentの変数を作成しています。
こっちが絶対正しいでしょう。
しかし「C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらない」が真実であるかは確認したほうが良い気がしますので、以下のテストをします。
以下の変数をアクタークラスの派生クラスであるmyActorクラスに追加します。
ビルドします。そしてそのmyActorクラスから前に作成したBPであるBPmyActorクラスのBPエディターを開きます。そこのVariablesを見ると、
あれ?出来てる。
・・・・
変わるみたいですね。
「C++のアクターの派生クラスとそこから作成したBPはもう完全は別物なのですから、今更、C++にコードを足してもBPの方は何も変わらない」は間違っていました。
確認して良かった。
「C++のアクターの派生クラスとそこから作成したBPは同じものなので、後で、C++にコードを足してもBPの方は自動で更新されます。」が正しかったです。
となると、教科書のやり方でもサンプルコードのやり方でもどちらでも正しいと言う事ですね。
教科書の方法でやろうと思ったのですが、教科書の方法だと初期化の仕方が分からない。BPで初期化しなければならないのかもしれないし、しなくてもいいのかもしれない。のでサンプルコードの方法でやります。
サンプルコードではWarrior.cppファイルのコンストラクター内で以下の方法でGameplayTaskComponent変数を初期化しています。
ので以下に示したようにGameplayTaskComponent変数を初期化しました。
残りのコードはPostInitializeComponents()関数内に実装されているので、まずPostInitializeComponents()をmyActorクラスに作成します。
ビルドも成功したので、これで行きます。
Step.7
はい。
<結果>
では、試してみます。
myActorBPをレベル内に配置します。
配置したmyActorBPを選択してDetailsをみます。
GameplayTaskComponentはここで選択しないでいいはずですが、ParticleSytemはここで指定しないといけないはずです。スターターキッドについているP_Explosionを追加しました。
Playを実行すると、
何も見えませんね。では、直していきましょう。
デバックしてみると、taskが初期化されていないようです。
この TaskOwnerがIGameplayTaskOwnerInterfaceであるべきなんですが、サンプルのコードはActorの派生クラスをパスしているんです。
これが、taskが作られない原因みたいです。
とここまで書いて、Actorの派生クラスをIGameplayTaskOwnerInterfaceで継承すればいいじゃないか。と気が付きました。
サンプルコードにあるWarriorクラスをもう一度見直すと、
思いっきり継承していました。
今度はIGameplayTaskOwnerInterfaceをアクターの派生クラスに継承させて試してみます。
今度はexceptionが投げられて停止しました。ただしVSのoutputには以下のように書かれていました。
IGameplayTaskOwnerInterface.hの26行目をみると、以下の関数が書かれていました。
この関数をオーバーライドしないといけないのかなとサンプルコードの中のWarrior.hを見てみると、
してました。のでこれをコピーします。
またexceptionが投げられました。今度は、
が問題だそうです。実際のコードをみてみると、
がエラーを発したようです。
うーん。良く分からん。のでサンプルコードの中のWarrior.hをもう一度みたら
GetGameplayTasksComponent関数以外にも関数を追加しているので、これも追加します。
Info(FS…の部分は省きました。
も一度やって見ると同じでした。
これがexceptionを投げているようです。
やっぱり元に戻りますが、
これを直さないと先に進めないです。
それぞれのパラメーターが何を指しているのかは分からないですが、bCareAboutPriorityがfalse、かつRequiredResources.IsEmpty()がtrue、更にClaimedResources.IsEmpty()もtrueなはずです。
Taskのproperties をデバックで調べて見ると、bCareAboutPriorityは0, RequiredResourcesも0, そしてClaimedResourcesも0の値を保持しています。
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などもここから指定すべきなのでしょうか?
それとも、これらの値は他のTaskを作成する時にパスしたパラメーターが正しければ、自然と指定されるのでしょうか?
いつもならここから、ネットで検索かけてGameplayTasksのチュートリアルを片っ端から当たるのですが、それをやると来週もGameplayTasksの勉強になってしまいます。3月中にはこの教科書の勉強を終わせる予定から遅れてしまいます。更に、今回の勉強の目的は絶対この教科書に書かれているレシピを再現するのではなく、この教科書を使用してUE4C++の勉強をするのはどうなのかを判断するためなので、ある程度で諦めるのも必要です。更に絶対再現出来ないコードである可能性もあるので、今回はここで中止します。
<まとめ>
今回はGameplayTasksをアクタークラスからの派生クラスから読んで実行する方法について勉強しました。しかし実際にはレシピの言っているようには動きませんでした。ただしサンプルコードとレシピの説明をみると、何かを加えれば正しく動く可能性もあります。
<おまけ>
- スタテックコンストラクターについて
NewTask関数が正しく働かないで直している時に気が付いたのですが、これはシングルトンの一種ですね。あるクラスを作成してそのオブジェクトを一個しか作成したくない時、どうするかの問題ですね。ここでの解決策は、コンストラクターをprivateにして、そのコンストラクターにアクセス出来るメンバー関数を一つだけ作成してそれをstaticにすれば、そのクラスのオブジェクトは一個しか作成されません。これがここで言うスタテックコンストラクターでした。
こんなのいきなりJob Interviewで聞かれたら舞い上がって答えられないですが、知っていればフーンで済む事でした。それよりもシングルトンそのもの使用の是非についての方が大切と思います。
- インターフェイスをクラスみたく扱う事
今回のレシピでテンプレートでインターフェイスをクラスのように使用しているのですが、こんなの可能なのかと驚いたのですが、実際はそのインターフェイスを継承したクラスをパスしていました。これなら納得です。