第15回ぷちコン「たぬ吉の大冒険」振り返りその1<AbilitySystemComponent、GameplayAbility、GameplayTag編>
お疲れさまです。Wviglerです。
今回ヒストリア様主催の第15回ぷちコンに参加させて頂きました。
締切に遅刻したのですが、ありがたいことに発表会で紹介されました。この記事ではその中で得られた知見についての記事です。今回はGameplayAbilitySystemについての知見が多く学べたのでそれについての記事になっています。
そのうち、今回はAbilitySystemComponent、GameplayAbility、GameplayTagについての記事になります。
兎にも角にもお品書きを
注》この連なる一連の記事は全てGameplayAbilitySystemについて書かれています。正直英語が読める人なら
このページか
このページの方が網羅的でオススメです。
・AbilitySystemComponentについて
さて全てGameplayホニャララで名付けられているGameplayAbilitySystemはアクションRPGやMOBA、果てはADVやSTGなど、かなり広い範囲のゲームで活用できるシステムです。docs.unrealengine.com
このドキュメント翻訳がアレ過ぎてさっぱり分からんな…
このシステムの利点としてはオフラインゲームではとにかく混乱を抑えられるということが挙げられます。BPの処理を分割してお互いをシンプルにできるので、管理や修正がとてもしやすいです。AIへの導入も割と簡単なのでそれも嬉しいところです。
オンラインだともっと色々利点があるようですが、ここらへんはあまり詳しくないのでわかりません。(勉強しなきゃorz)
現状C++での記述が必須だったりして、まだ少し面倒なところはあるんですが、現状でも十分導入メリットの多いシステムだと感じました。
以前からこの記事やこの記事などを参考にしながら、実験用のプロジェクト内でGameplayAbilitySystemを扱ったことはあったのですが、GameplayAbilitySystemを自分の作品内で本格導入するのは初となりました。
さて、GameplayAbilitySystemは8つの柱でできています。AbilitySystemComponent、GameplayAbility、GameplayTag、GameplayAttribute、GameplayEffect、GameplayTask、GameplayEvent、GameplayCueです。このうち最も根幹となるのが今から紹介するAbilitySystemComponent(以下ASC)です。
とは言ってもASC自身が特に何かをするという訳ではありません。ASCはあくまで他のGameplayAbilitySystemの橋渡し役としてアクセスを提供するだけです。つまりはインターフェイスのようなものですが、実際内部的には"AbilitySystemInterface"というC++インターフェイスを介しています。
・とりあえず導入してみる
注》この記事は基本的にオフラインオンリーの記事となっています。
自分の復習も兼ねて1からGameplayAbilitySystemを導入していきます。
GameplayAbilitySystemの導入にはC++のプロジェクトが必須です。なのでC++コーディングができるプロジェクトから作成していきます。
TempleteはThirdPersonTempleteを使用します(ホント便利だなこいつ)
C++プロジェクトにするのを忘れずに…
プロジェクト名は今回「GAS_Test」としました。
まずはプラグインの追加をします。
プラグインをオンにしたら一旦再起動します。
次にエディタを開いて、"GAS_Test.Build.cs"に"GameplayAbilities","GameplayTags","GameplayTasks"のモジュールを追加します。モジュールについてはこちら
モジュールを追加したら一旦プロジェクトの入っているフォルダを開き、プロジェクトファイルを右クリックしてGenerateVisualStudioProjectFilesをクリックして、プロジェクトを再構成します。(ここ要らないかも…でも検証するの面倒臭い…)
それが終わったら次はいよいよC++クラスの作成です。
Characterの派生クラスを作成します。
名前は「GASCharacter」としました。
とりあえずASCを追加していきます。
GASCharacter.hにAbilitySystemInterface.hをincludeし、(~.generated.hは最下段に無いとコンパイル時エラーになることに注意)
GASCharacterにIAbilitySystemInterfaceを追加します。
Component実装に使うUAbilitySystemComponent*の変数とAbilitySystemInterfaceに定義されているGetAbilitySystemComponent()を定義します。
ここは実は…
このようにUPROPERTYの引数が空でもさほど問題は起きないのかなとは思います。(というか恐らくこっちが正式では?)BP上からASCを直接参照できなくなりますが、その場合GetAbilitySystemComponentを介して呼べば問題ありません。ただいちいちSelfノードとGetAbilitySystemComponentノードを呼ばなくてはならないのはとても面倒くさいので今回はアクセスを可能にしています。
ヘッダはここまでにして、次にGASCharacter.cppファイルを弄っていきます。
まずは"AbilitySystemComponent.h"をincludeします。
最後にコンストラクタでヘッダで宣言したAbilitySystemをコンポーネントとして登録します。
以上で実装終了です。コンパイルしましょう。
コンパイルが終わったらBPクラスを作成します。
名前はBP_GASCharacterとしました。
中身を開くと…
ちゃんとComponentにAbilitySystemが追加されていますね。
前に述べたとおり、ASCはこれだけで何かをするというものではありません。なのでこのBPクラスは現状ではただのCharacterクラスと何も変わりません。具体的な機能を付けるのはこれからです。
…なんですけれども、その前にこのままだと味気ないので、少しBP側で色々やりしょう。
まずは新規にGameModeを作成して、DefaultPawnClassにBP_GASCharacterを登録します。
そしてこのGameModeをThirdPersonExampleMapのGameModeにOverrideします。
ThirdPersonExampleMap上のThirdPersonCharacterを削除して、代わりにPlayerStartを設置します。
今度はBP_GASCharacterを開いて、SpringArmとその子としてCameraのComponentを追加します。設定はデフォルトで大丈夫です。
次にMeshを追加します。こんな設定にすると…
こうなります。
で、今度は移動をさせたいんですが、まともにやろうとするととても面倒臭いので、
もうこれでいいや(適当)
これでBP_Characterをプレイヤーとして登録して、WASDで最低限の移動ができるようになりました。
・GameplayAbilityについて
GameplayAbilityはAbilitySystemComponentを除けば、GameplayAbilitySystemの中で最も大きな要素かも知れません。攻撃や防御、回避、ダメージなどあらゆるアニメーションやアクションを排他的に管理可能なシステムです。
実は先に挙げた「たぬ吉の大冒険」では主人公だけでAbilityの数が22個にものぼる、というとんでもない状態になっていました。
まあAbility作るの慣れてくると本当に楽しいからね。仕方ないね…でも今度から少し自重しようね。あんまり主人公のやれること増やすと調整も大変だしね…
さて、ぷちコンに遅刻した主な理由が判明したところで、プロジェクトにGameplayAbilityを使うための準備をしていきましょう。
・GameplayAbilityを導入してみる
GameplayAbilityを導入するにはGASCharacter.hを開き、BeginPlay()とPossessedBy(AController* NewControlle)をoverrideする必要があります。
幸いなことにBeginPlay()は作成時に宣言されているので、PossessedByの方を追加で宣言するだけです。
次に使用するAbilityを登録する配列「AbilityList」を宣言します。
TSubclassOf<class>はclass以下の派生クラスがUE4エディタ上でツリー形式で選択できるようになります。
こんな風に表示してくださいね、という意味です。
次にcppファイルに移動して、BeginPlay()とPossessedBy(AController* NewController)を書き換えます。
このあたりの書き方は何種類かあるみたいなんですが、僕はこの記事に載っている方法を丸写しにしています。
やっていることはAbilityListからAbilityを全て取り出してAbilitySystemにGiveAbility関数を使用して登録する、ということです。
その後でInitAbilityActorInfoでActorInfoを更新します。ActorInfoはGameplayAbility内でActorの情報を取得するために使用するStructureです。後で出てきますが、とても有用な機能なのでこの記述は忘れないようにしましょう。
次はPossessedByです。
PossessedByはデフォルトでは記述されてないので、新しく書く必要があります。
PossessedByした際にはController情報が切り替わるので、ActorInfoを更新する必要があります。
以上で本当に最低限ですが、C++側の導入は終わりです。コンパイルしましょう。
まぁ殆どのゲームで使う場合GameplayAttributeの実装が必須になったり、Ability自体からTagを弄れた方が便利だから改造しようとか、なんならComponent側からBPでTagを弄れるようにすればもっと…とか欲張りだすと色々キリがないんですが、最低限これでAbilityのみは機能させることはできます。
さてお次はBP側です。
まずはGameplayAbilityのBPを作成します。
名前はGPA_Testとしました。
中を開くと…
こんな風になっています。右にGameplayTagのリストがありまして、これは非常に重要なものなのですが、それはひとまず置いておいて、とりあえずこれを動くようにしていきます。
とりあえずAbilityが発動した時に「AbilityStart!!」、Abilityが終わった時に「AbilityEnd!!」と出力だけして終了するものです。CommitAbilityはAbilityのコスト計算などをするものなのですが、とりあえずGameplayAbilityにはおまじない的に頭に付けるものだと思ってもらってもあまり間違いではないです。(例外はありますが…)
これをBP_GASCharacterのAbilityListに登録します。AbilityListはBP_GASCharacterのClassDefaultsからアクセスできます。
"Z"のインプットノードにこのように繋げます。
これでプロジェクト実行中にZキーを押すとGPA_Testが発動します。やってみましょう。
このようになります。ただこの場合まだAbilityが始まった瞬間と終わった瞬間に時間差がないため、「AbilityStart!!」が表示された瞬間に「AbilityEnd!」が表示されています。つまりAbilityが始まった瞬間に終わっているということです。
GameplayAbilityの強みの1つはActivateAbilityからEndAbilityまでの間にLatentなノードを挟めることです。使ってみましょう。
今度は「AbilityStart!!」が表示された3秒後に「AbilityEnd!!」が表示されるはずです。流石にTimelineは使用できませんが、SetTimerやAnimation系統を扱えます、中でも一番凄いのはPlayMontageが使用可能なことでしょう。
この2つがヤバイ(語彙がひどい)
またAbilityを所持しているActorの情報を得たいと思ったらGetActorInfoというノードが役に立ちます。これは前にC++内で記述したActorInfoから情報を取得するものです。
~FromActorInfoというノードはいちいちGetActorInfoからBreakするのが面倒な時のためのものです。BPがスッキリするので、基本的にはできればこっちを使用します。試しにちょっと使ってみましょう。
こんな風にすると、
Abilityを所持しているActorであるBP_GASCharacterの名前が表示されます。
文字表示だけだと寂しいので、アニメーションを付けていきましょう。アニメーションはこちらと同じくFrank Action RPG Sword 1 (Basic Set)から取ってきます。
アニメーションリターゲットやスロットアニメーション等に関しては各自で調べていただきまして…
こんな風にノードを組みますと
Zキーを押すとアニメーションが再生できるようになります。ついでに剣をこちらと同じ方法で作成しました。
ただしまだ排他処理ができていないので、例えばZキーを連打すると途中でアニメーションがキャンセルされ、また最初からアニメーションが開始されてしまいます。これを解決するために必要なのがGameplayAbilitySystem内の排他処理を担当するGameplayTagです。
・GameplayTagについて
上記のようにすればGameplayAbilityは使用可能になるのですが、正直言ってこれだけではそこまで強力なシステムではありません。GameplayTagによる強力な排他処理が行えれば有用性がぐんと増します。
GameplayTag自体はGameplayAbilityとは別個の独立したシステムです。通常ActorやComponentに付いているName型のTagと違い、枝分かれしたツリー状のTagであり、普通のVariableの型として使用することもできます。これだけでも普通に有用なシステムだったりします。
またGameplayTagContainerという型が存在します。これは名前の通り、複数のGameplayTagをまとめて扱うための型です。
GameplayAbilityのClassDefaultsにはTagsという項目があり、複数のGameplayTagContainerで排他処理を行う仕組みがあります。
一つ一つ見ていきましょう
AbilityTagsはAbilityそれ自体が所持するTagです。TryActivateAbilityByTagsでAbilityを指定して実行する場合などに使用します。
CancelAbilitiesWithTagはAbilityがActiveになったと同時にこのTagを所持しているAbilityを全て強制終了(CancelAbility)させます。CancelAbilityはEndAbilityと基本的には同じです。ただし、EventOnEndAbilityノードにはWasCancelledというBooleanPinがあるため、EndAbilityした際とCancelAbilityした際で別々の処理をさせることができます。
BlockAbilitiesWithTagはAbilityがActiveになっている間はこのTagを所持しているAbilityを実行させません。
CancelAbilitiesWithTagの場合は新たな実行はできますが、それまでActiveになっていたAbilityは強制終了されます。BlockAbilitiesWithTagはすでにActiveになっているAbilityは強制終了はされませんが、新たに実行ができなくなります。
次はActivationOwnedTagsです。ここに設定したGameplayTagはAbilityの実行中、AbilitySystemComponentに付与され、終了すると無くなります。AbilitySystemComponentに付与されているTagとAbility自身の所持するTagは別物ですので注意しましょう。
このAbilitySystemComponentに付与されているTagはBP上で
これらのノードを使用して調べることができます。ちなみに
こういうノードもあるんですが、それまでに付与されたTagが全部放り込まれているらしく、活用方法が分かりません。誰か教えて下さい。
さて次です。このAbilityをActiveにしようとした際、ActivationRequiredTagsに指定したTagが全てAbilitySystemに付与されていれば、このAbilityを実行することができます。付与されていなければ実行されません。
ActivationBlockedTagsも同様に指定されたTagが1つでもAbilitySystemに付与されていればこのAbilityは実行されません。付与されていなければ実行されます。
そこから下の項目は実は後で別記事で解説するGameplayEventに関わってきます。なので今は必要ありません。説明はその項目で行います。
・GameplayTagを導入してみる
とりあえず先程のAbilityの問題点を解決しましょう。このAbilityの問題点は「Abilityに排他処理がされていないため、動作中に再びZキーが押されると同じアニメーションが最初から再生されてしまう」というところです。つまりAbilityを実行している間はこのAbilityを再生できないようにしてしまえばいいわけです。なのでこうしてしまいましょう。
このAbilityが動作している間、Ability.State.Attack01がASCに付与されます。動作中に再びZキーを押しても、ASCにAbility.State.~が付与されているのでAbilityは実行できません。動作が終わるとAbility.State.Attack01はASCから外れるのでAbilityは再び動作するようになります。
さて、GameplayTagはGameplayAbilityの呼び出しにも使用することができます。こちらの方が使い勝手がいいので実装していきましょう。まずAbilityTagsにAbility.Action.Attack01というタグを登録します。
そしてBP_GASCharacterのTryActivateAbilityByClassノードをTryActivateAbilitiesByTagに入れ替えて、TagContainerにAbility.Action.Attack01を登録します。
特に何の問題もなく動作することが確認できると思います。そして攻撃モーション中にもう一度Zキーを押しても、再び同じモーションが再生されることはありません。
GameplayTagは攻撃のアニメーション管理にも向いていますが、排他処理が強力なためダメージのアニメーション管理にも向いています。「攻撃中でも敵の攻撃を受けた場合ダメージ処理に移行し、ダメージ処理中は攻撃動作を行えない」などの処理を簡単に実現することができます。
実際に作成してみましょう。
Frank Action RPG Sword 1 (Basic Set)には被ダメージ用のアニメーションも用意されています。
BP名は「GPA_Damage」としまして
ノードは…
こうして、Tagの構成は…
こうします。要は動作中はAbility.Action.~を持っているAbilityは発動しない設定にします。これを…
今度はXキーに追加します。実行してみると、Xキーを押すとダメージモーションに移行するのが確認できると思います。それだけでなく例えば攻撃モーション中にXキーを押したり、ダメージモーション中に再びXキーを押してもダメージモーションに移行します。しかしダメージモーション中にZキーを押しても攻撃モーションに移行することはありません。
運用上のTipsなのですが、この記事では攻撃もダメージもアニメーションが一つしか無いため問題ではありませんが、AnimationMontageの強みを活かすためにアニメーションはいくつかまとめて管理し、Sectionで実行したほうがいいと思います。
アニメーション同士のLinkを切るのを忘れないように…
そうすれば管理しやすいのに加えて、例えばダメージを受けた際に「いくつかあるダメージモーションからランダムでモーションを選択する」なんて時に…
こんな風に書けるので…
またRootMotionが設定されているアニメーションならあまり問題になりませんが、攻撃時に一瞬足を止めたい場合やジャンプ不可能にしたい場合などは、BPInterfaceで一纏めにして運用することをオススメします。依存度も下げられますしそもそも…
こんなのいちいち書いてられるか!ってことで。
とりあえずGameplayAbilityとGameplayTagについての知見はこのぐらいです。
次回はGameplayAttribute&GameplayEffectsにしようかGameplayTaskにしようか…
というかEffectsもTaskもよくよく調べてみるとまだ理解度として低いような…
ご意見、ご指摘、ご質問等あればコメントお願いします。
続きの記事はこちら