第15回ぷちコン「たぬ吉の大冒険」振り返りその4<GameplayCue編>
この記事はこの記事の続きです。
この記事では第15回ぷちコンの振り返りとして、「GameplayCue」の解説をしていきます。
お品書き
・GameplayCueEditorとGameplayCueManagerについて(GameplayCueを扱う準備)
・GameplayCueについて
GameplayCueはGamplayAbilitySystemにおいて視覚エフェクト(パーティクル等)やサウンドエフェクトなどを司るシステムです。
GameplayCueのHandlerはObjectであるGameplayCueNotify_StaticとActorであるGameplayNotifyCue_Actorの2つに分かれ、それぞれに役割があります。ただ扱い方をちょっと間違えると意図しない動作を引き起こしたりと、結構難しい部分のあるシステムだなと感じました。
・GameplayCueEditorとGameplayCueManagerについて(GameplayCueを扱う準備)
GameplayCueは使用するノードなどを見ると、一見普通のGameplayTagで呼び出せそうに見えますが、実は普通にやっても呼び出せません。GameplayCueを扱うには2つの前段階が必要です。それはGameplayCueEditorを使用してGameplayCue.~のTagを作成することとGameplayCueNotifyPathsにGameplayCueHandlerのフォルダを登録することです(ちなみに厳密にはどちらも必須ではないみたいなのですが、話がややこしくなるので必須という体で進めます。ま、大抵これをやるよってことで)。
まずGameplayCueEditorです。これは「Window」のタブメニューから呼び出すことのできるEditorの一つです。
これを開くとこのような画面になります。
AddNewGameplayCueTagを開くと…
このようにGameplayTagの追加が行えます。そしてGameplayCueで扱うGameplayTagは必ず「GameplayCue.~」で始まる必要があります。
それではとりあえずGameplayCueEditorから新たなGameplayTagを登録します。名前は「GameplayCue.Attack01」とします。
Handlersという項目がありますが、GameplayCueとTagが関連付けられると、ここに関連付けられたHandlerが表示されます。それだけでなく「AddNew」をクリックすると、予め関連付けられたHandlerを作成することができます。
説明文も出てとても親切ですね。
さて、先程HandlerとGameplayTagが関連付けられている、と述べましたが、実はこの関連付けはEditor上だけのもので、Game上は全く紐付けされてはいないようです。
で、Handlerを操作するにはいくつかのノードを介するかGameplayEffectのDisplayで行うのですが、
いずれもGameplayTag情報のみから呼び出す形を取っています。そしてGameplayAbilityにおけるAbilityListのようにいくつかのGameplayCueHandlerを登録する、といった機能はありません。
じゃあどうやってTag情報だけでそれぞれのHandlerを探しているのかというと、GameplayCueの呼び出しはデフォルトの状態では(GameplayCueManagerというクラスが)プロジェクトファイル内の全てのファイルを走査して同じGameplayTagを持っているHandlerを探しています。
どうやら2回目以降は紐付けがなされているらしく素早く探せるようになるのですが、なんとプロジェクトを一旦閉じ、再起動して実行すると再度この処理が必要になります。プロジェクトファイルが大きくなればなるほど、プロジェクトを起動してGameplayCueを初回発動した時にフリーズしたような停止時間が挟まるというとんでもない状態になります。
これを防ぐためにはGameplayAbilityがAbilityListを使用したように、走査する範囲を限定する必要があります。そのためGameplayCueを1箇所もしくは何箇所かのフォルダに集め、GameplayCueNotifyPathsにそのフォルダを登録します。
このようなフォルダを作成し、DefaultGame.iniに書き込みます。
以降、GameplayCueのHandlerは全てこのフォルダに投入します。
まだよく分かっていないのですが、どうやら起動時にこの走査処理を予めまとめて行う解決法もあるようです。このあたりの選択肢を残すためにわざと初期状態だと問題が起こるようにしているのかもしれません。
・GameplayCueNotify_Actorについて
GameplayCueNotify_ActorはGameplayCueのHandlerの一つでその実態は普通のActorです。どのぐらい普通のActorなのかというと、本来これ自体は見えないものとして作られているにも関わらず、StaticMeshComponentなどを追加すると可視化することができたり、Level上にドラッグ&ドロップで普通に配置できたりします(どっちもあまり意味はありません)。基本的には実装されている"OnActive"や"OnRemove"あるいは"WhileActive"といったFunctionをOverrideして使用します。一定の期間パーティクルを出し続けたりなど、ある程度継続してエフェクトを実行し続けたい時に使用します。
・GameplayCueNotify_Staticについて
こちらもGameplayCueのHandlerの一つなのですが、Actorではなく、どちらかというと生成するActorの情報を持っているObjectです。このObjectから生成されたGameplayCueNotify_Actorは一瞬で生まれ、また一瞬でDestroyされます。基本的には"OnExecute"をOverrideして使用します。例えば爆発の視覚エフェクトや効果音などの、瞬間的なCueの実行に向いています。
・GameplayCueを実装してみる
それでは実装してみましょう。GameplayCueEditorからGameplayCueHandlerを作成するのは先程説明したので、あえて普通のBlueprintClass作成から作っていきましょう。GameplayCueNotify_Staticを作成します。
名前はGQS_Explosionとします。もちろんですが「GameplayCue」フォルダに作成することを忘れないでください。
GQS_Explosionを開くとこうなります。
EventGraphは何も書き込まないので意味がありません。ここで注目するのは右のClassDefaultsです。
GameplayCueTagという項目がありますが、これがトリガーとなるGameplayTagになります。さて、GameplayCueNotify_StaticはOnExecuteをOverrideすることで機能を実装します。早速OnExecuteを開きましょう。
OnExecuteの中身はこのようになっています。今回は用意が面倒なので視覚エフェクトや効果音はStarterContentから取ってきます。このクラスがExecuteで呼び出されると自身を中心に爆発のエフェクトと効果音が鳴るようにしていきます。
このように繋げます。
MyTargetとParametersはParentノードに繋げないとエラーになります。これで中身は作ったので、後は紐付けです。先程のGameplayCueTagを"GameplayCue.Attack01"にします。
そして今度はGPA_Attackに移動し、ExecuteGameplayCueOnOwnerでGQS_Explosionを呼び出します。
これで準備完了です。実行しましょう。
Zキーを押すと剣を振るとともに上のように爆発のエフェクトと効果音が鳴ります。ここでは使用していませんがExecuteGameplayCueWithParamsOnOwnerというノードもあります。これはParametersというStructureを通してかなり様々な情報をHandlerに送ることができます。
ただこのParametersはこのノードを介さなくてもGameplayEffectからの実行時などにある程度の情報を運んでいるようです。ちょっとだけ見てみましょう。
今は使用していないGPE_ReduceHealthにGQS_Explosionを紐付けます。MagnitudeAttributeはGASAttributeSet.Healthにしましょう。
再びCキーに繋げます。GQS_Explosion側ではParametersからRawMagnitudeを表示するようにします。
これで実行してCキーを押すと…
GQS_Explosionの発動とともに、このようにHealthの変化値である-80.0が表示されます。ただここは僕もまだ良く分かっていないのと、詳しく調べるときりがなさそうなので、誰か調べてください(丸投げ)。
さてCキーのノードは再びWeaponExpandのノードに元のように繋げまして…
今度はGameplayCueNotify_Actorを使っていきます。これを使用して剣を大きくしている際には常時炎のエフェクトを出すようにしましょう。
まずはGameplayCueNotify_Actorを作成します。
名前は"GQA_Flame"としました。
中身を見てみましょう。
違いとしてActorなのでViewportやConstructionScriptがある点と、右のClassDefaultsがGameplayCueNotify_Staticに比較してかなり充実している点が挙げられます。
ViewportやConstructionScriptは正直使わないので、ClassDefaultsを見ていきましょう。
注目したいのはCleanupとGameplayCueの2カテゴリです。ここがGameplayCueの動作に関わってきます。GameplayCueTagはStaticの場合と同じですし、多重発動の有無など細かい項目もありますが、とりあえず重要なのはCleanupカテゴリのAutoDestroyOnRemoveとGameplayCueカテゴリのAutoAttachToOwnerです。
AutoDestroyOnRemoveはAbilityからの操作でRemoveした場合、自動的にその時点でActorがDestroyされるように設定します。場合にもよると思いますが、AbilityからはAdd、Execute、Removeの三種類の動作しか行えないため、多くの場合Trueにされることが多いです。
AutoAttachToOwnerはチェックするとこのActorがOwnerであるActorにAttachされるようになるオプションです。なんでこの項目があるのかというと、例えばOwnerActorにパーティクルをAttachしようとした場合。
こんな形でOwnerActorのRootComponentに直接EmitterをAttachすることが考えられます。これはできることはできるのですが、問題が発生します。
UE4において外部ActorからAttach済みのComponentを操作、削除するのは制限が掛けられており、GameplayCueNotify_Actorからでは一度発動したEmitterの動作を操作したり停止させることができなくなってしまいます。
これではHandlerの意味がないので、こういう場合は…
自身のRootComponentにEmitterをAttachし、自分自身をOwnerActorにAttachすれば、操作も可能で、削除する際はComponentをDestroyすれば削除されます。
では実際に炎のエフェクトを追加していきましょう。
…と思ったけど、ちょっと作成してみたところ意外に複雑だったので、まずは追って説明はしないで完成した品を見てもらいます。
<GQA_Flame-OnActive>
<GQA_Flame-OnRemove>
<GQA_Flame-ClassDefaults>
<BP_GASCharacter-BeginWeaponExpand>
<GPA_ExpandWeapon>
実行画面<通常時>
実行画面<武器拡大時>
基本構造ですが、Payload(GameplayEvent)とParametersどちらも使用しています。Payloadは武器拡大のTimelineが終わった際にそのDirectionをfloatに変換した形でAbility側に通知し、それによってCueをActiveにするのかRemoveするのかを決定します。
ParametersはAbilityからSkeletalMeshのComponent情報をCueに通知し、CueはそこからBone情報を読み取って"hand_r"にHandlerであるActorをAttachします。これで"hand_r"のBoneから炎が発生するようになりました。OnRemove側の実装はオートでDestroyされるので一見必要ない用に見えますが、どうやらComponent情報はDestroyされても保持されているらしく(もしかしたらHandlerはプーリングされる仕組みになってるのかも)、これがないと繰り返す度にどんどん炎が強くなってしまうので入れています。
どうでしたでしょうか?個人的にはGameplayCueは構造自体はそこまで難しい話ではないのですが、まだ整理されきっていないのか罠や落とし穴が多くて、そこに嵌ると(情報量が少ないのも相まって)なかなか抜け出せないな、と感じました。
しかし、共通のエフェクトや効果音を一つにまとめて管理できるため、間違いなく有能なシステムではあるので、頑張って身につける価値はあると思います。
・あとがき
この記事群についてですが、「GameplayAbilitySystemについて包括的な情報がないから(ぷちコンである程度使ったし)自分で書こうかな?」みたいな軽い気持ちで初めました。
しかし、調べていくうちに次々新しい要素が見つかり、それも書かなきゃ、みたいなことが重なり、結局全体で3万字を超えるという代物になりました。
あんまり長すぎると読む人が苦痛じゃないかと心配になってくるわけですが、かといって備忘録の役割もあるので省くのもなぁ、というジレンマもありこのぐらいの長さになっています。(これでも一部省いてはいます。最初はAIへの導入とかも書くつもりだった)
注意ですが、これでもGameplayAbilitySystemは全然網羅しきれてません。今回Targetingやネットワーク関連は全く手を付けていませんが、本来GameplayAbilitySystemはここで威力を発揮するシステムなのです。なので僕が把握しているのは狭い狭い範囲に過ぎません。
GameplayAbilitySystemの学習での一番の壁はその膨大さだと思います(多分二番は情報の少なさ)。これを書く前は僕も「なんか色々資料はあるけれど分かりにくな…ActionRPG関連も概念の説明ばっかりだし…」と思ってたんですが、書いてみると膨大すぎて「これを分かりやすく書くのは無理だわ…」となりました。適用範囲も書く前より大分広がって見えていて、「むしろこのシステムが適用できないゲームって何かあるかな?」と思っています。
ただGameplayAbilitySystemはBPからUC++への導入として考えるとかなりいいのではないかと思います。ある程度決まった書き方ができれば導入できる感じなので、「全てBPで書くと把握しきれないぐらい自分の制作しているゲームが複雑化してきた」「でもUC++使うのはまだ怖いし、どこに使ったらいいのかも分からない」というぐらいの製作者が導入するのにぴったりで、しかもちゃんと効果的であるというシステムなのではないでしょうか?
僕も今回のこの記事を書いてネットワーク関連を全く学んでいなかったことを再認識して、次はこれを中心軸にして学ぼうと目標を立てることができました。ネットワーク頑張るぞ!…とりあえずEpic公式のMultiplayerのチュートリアル見るか…
そんなこんなで第15回ぷちコン振り返りでした!ありがとうございました!
というか3月締切のぷちコンの振り返りが5月にやっと公開というね…
ご指摘、ご質問、ご意見などあればコメントにお願いします。