じゃっくぽっとラボ

UE4の技術ログあつまれ~

GASのデザインパターン まとめ

概要

本記事はUnreal Engine (UE) Advent Calendar 2021 24日目の記事になります。

qiita.com

GameplayAbilitySystem(以下GAS)はプレイヤーに紐付いた大量のアクションの実行可否を簡単に設定できたり、エフェクトやサウンドなどのアセット参照を抑えて設計できるなど沢山のメリットがあります。
とはいえ、いざ自分のプロジェクトに使おうとしたとき使いこなせるかは別。
そこで、自分が普段から使っているデザインパターンをまとめました。

※この記事には目次が2種類あります。用途に合わせてお選びください。

※本記事ではGameplayAbilityのみについて取り上げます。GameplayAttributeやGameplayEffectなどについては触れません。

目次

デザインカタログ

実例で記事を追いたい方はこちらからどうぞ。対応したロジックの場所へ飛びます。

GASの導入【初心者向け】

今回は実例を載せるのが趣旨なので、導入は他のサイトを参考にして下さい。 自分が導入の際に参考にした記事を羅列します。
okawari-hakumai.hatenablog.com

wvigler.hatenablog.com

後々GASを自分で拡張する時に見直すことが多いので今でも重宝してます。

最終的に下記の状態になったことを前提で進めます。

  • キャラクターにGASコンポーネントが実装されている
  • キャラクターにGameplayAbilityクラスの配列がある。
  • Try Activate GameplayAbility By Tagを使うことが出来る。 f:id:JackpotDevelop:20211223123721p:plain

デザインパターン解説

アビリティ起動時のGameplayTagを処理判定に使う

使用例:ダメージリアクション、被ダメージエフェクト切り替え

通常、アビリティが実行されるとActivateAbilityイベントが実行されます。
このイベントに似たActivate Ability From Event というものがあります。
これの良いところは、GameplayEventDataという構造体データをアビリティ起動元からアビリティクラスへ渡せるんですね。

参考: 第15回ぷちコン「たぬ吉の大冒険」振り返りその1<AbilitySystemComponent、GameplayAbility、GameplayTag編> - そうだ、ゲームを作ろう

このイベントを使うことで、アビリティの起動に使われたGameplayTagを利用して処理を切り替えることが出来ます。

例:ダメージリアクション

アクションゲームでは攻撃されてダメージを受けた時、そのダメージの威力によって吹っ飛んだり少しよろけたりします。
このリアクションをひとつのアビリティで実装します。

GameplayTagの作成

攻撃された時、受けたダメージの大きさ(大・小)をGameplayTagで判定します。
新しいタグ Damage.Hit、さらにHitの下の階層にHeavy, Smallと2つのタグを作ります。
エディタのツールバー[設定]→[プロジェクト設定]、[プロジェクト]の中の[GameplayTags]からタグを作成できます。

ダメージの大きさ タグ
Damage.Hit.Heavy
Damage.Hit.Light

f:id:JackpotDevelop:20211223124347p:plain

GA_DamageHitの作成

新しいアビリティ GA_DamgeHitを作成。
f:id:JackpotDevelop:20211223124735p:plain

作成したGAクラスの詳細欄、AbilityTagsにはDamage.Hitを指定します。
起動条件に親階層のHitを設定しておけば、Hitの下の階層にあるHeavyとLightどちらが指定されても起動するアビリティとなります。

f:id:JackpotDevelop:20211223124850p:plain

さらに詳細欄の下の方にある[Triggers]の項目に設定を追加します。
AbilityTriggersの+マークを押して配列を一つ増やし、またDamage.Hitタグを追加します。 f:id:JackpotDevelop:20211223125512p:plain

ここまで行うことで、Send Gameplay Event to Actor ノードでGameplayTagを指定された時に起動するアビリティとなります。 またGAクラスの中身は何も書いていません。ダメージを与える側

次にノードの実装です。
ActivateAbility From Eventからパラメータを取得し、
プレイヤーがどんな種類の攻撃をしたのかこの構造体から取得します。
取得したタグをSwitchノードで分岐します。
f:id:JackpotDevelop:20211223130250j:plain

f:id:JackpotDevelop:20211223130444p:plain

あとで再生するアニメーションモンタージュの変数を作り、分岐先でそれぞれ指定します。
これでダメージを受けた際のアニメーションが切り替わるようになりました。
※Switchノードの前にSequenceノードを追加しました。 f:id:JackpotDevelop:20211223133305p:plain

テスト

新しいBPクラス BP_DamageHitTest アクタを作成します。
f:id:JackpotDevelop:20211223133905j:plain

StaticMeshコンポーネントとHitイベントのみのシンプルなアクタです。
こいつにプレイヤーがヒットしたらダメージリアクションを取るようにしてみます。
StaticMeshは何でもよいです。自分は1MCubeを選びました。 DoOnceノードは、ヒットしたときに何度も実行されるのを防止するために挟んでいます。

  • Damage.Hit.Light と Damage.Hit.Heavy の2つを用意しました。
  • それと、アニメーションモンタージュはAnimBPにスロットノードを追加しておかないと再生されません。ですので、忘れず追加しておきましょう。
  • ThirdPersonCharacterのBPに作成したGA_DamageHitを追加します。

f:id:JackpotDevelop:20211223134659j:plain

f:id:JackpotDevelop:20211223134940p:plain

f:id:JackpotDevelop:20211223135106p:plain

結果

youtu.be

無事ダメージリアクションの切替が出来ました。
今回は2種類のダメージで作成しましたが、Damage.Hit.Electroとか Damage.Hit.Fireなどの属性攻撃も追加出来ます。モンタージュだけでなく受けた攻撃のエフェクトも差し替えることも可能です。
リアクションを増やしてもSwitchノードで分岐を作るだけですからGAクラスはひとつで済みますね。

さらに応用としては、このアビリティを親にした子クラスを作成して、プレイヤー用、敵A用、敵B用とアニメーションを用意して、モンタージュの変数に入れることで使い回すこともできます。

WaitGameplayEventノードを使う

使用例:ジャストガード、ノックバック終了判定
アビリティ起動後に外部からイベントを受け取ったタイミングで処理を実行することが出来ます。
「能力実行中に攻撃を受けたら、この処理を実行」「起動後この状態になったら終了」といったロジックが組めます。

例:ジャストガード

敵から攻撃を受けた際にプレイヤーがタイミングよくガードをしていれば、ジャストガード判定となりダメージを無効化できるという能力です。
厳密にいえばアニメーションモンタージュの特定のフレームの間に攻撃を受ければ発動というものですが、シンプルに説明するためにモンタージュ中全てをジャストガード判定フレームとします。

Gameplayタグの作成

敵の攻撃、プレイヤーが行うガード、ジャストガードされた敵のリアクション。
それぞれGameplayタグにします。

説明 タグ
敵の攻撃 Action.Melee
敵の攻撃ヒットで送るタグ Damage.Hit.Melee
プレイヤーのガード Action.Guard
敵のリアクション Action.Parryed

GA_Guard、GA_Meleeの作成

GA_Guard
AbilityTagsとActivationOwnedTagsの2つにAction.Guardを指定します。
AbilityTagsはプレイヤーからこのアビリティを呼び出すために、
ActibationOwnedTagsはアビリティ実行中、オーナーであるプレイヤーにこのタグを付与させるために指定します。
敵の武器が当たった時このタグが付与されているのであれば、ダメージは与えられずガードします。
f:id:JackpotDevelop:20211223183846p:plain
ガードのモンタージュを再生した後「Wait Gameplay Event」ノードをつなぎます。
Event Received からSend Gameplay Event to Actor ノードで攻撃した相手に「ガードしましたよ」というイベントを送ってあげます。
相手の情報はWait Gameplay Eventの payloadからGameplayEventDataから取り出します。
といってもまだ相手側の処理を書いていませんので現時点では何もデータが入っていません。
f:id:JackpotDevelop:20211223161423p:plain

GA_Melee
攻撃する敵のアビリティです。
攻撃のモンタージュを再生後、弾かれたイベントを待つための処理を書きます。
ジャストガード判定のためのBool変数 Parryedを作成しています。ジャストガードが確定した時点で最初のモンタージュ終了がアビリティ終了のフラグにならないようにしています。 f:id:JackpotDevelop:20211223163900p:plain

モンタージュを再生しただけではプレイヤーに攻撃がヒットしませんので、敵キャラクターに武器としてStaticMeshコンポーネントを追加しました。
このメッシュがプレイヤーにオーバーラップしたとき、SendGameplayEventtoActorノードでDamage.Hit.Meleeタグを送っています。
同時にPayloadには自分の情報をInstigatorとして入れています。 f:id:JackpotDevelop:20211223183716p:plain

忘れる前にプレイヤーのBPにアクタタグ「Player」を追加します。 f:id:JackpotDevelop:20211223165331p:plain

テスト

プレイヤー側にガードアビリティ実行のための処理を書きます。
また、作成したアビリティをプレイヤーのBPに追加します。
f:id:JackpotDevelop:20211223185401p:plain f:id:JackpotDevelop:20211223193106p:plain

レベルBPに「繰り返し敵がアビリティを実行する処理」を書きます。 f:id:JackpotDevelop:20211223185517p:plain

結果

youtu.be

通常時は攻撃されるとダメージリアクションを取り、ガードするアビリティ実行中に攻撃されると相手がのけぞります。
Wait Gameplay Event の他の使い道としてはダメージを受けて吹き飛ばされた時に地面に着地したタイミングでアビリティを終了するようなことに使うことが出来ます。

状態判定タグを使ってアビリティの実行可否を切り替える

使用例:状況でアクション切替|コンボ攻撃

例:状況でアクション切替

「状況によって違うアビリティを呼びたい」ときのロジックです。 例としてジャンプで説明します。
下記2つの状況でジャンプボタンが押されたとき

  • 地面に足がついている時→通常ジャンプ
  • 崖にぶら下がっている時→崖上に登る

というように、同じボタンでも違うアクションにしたい場合があります。
GAクラスはGameplayTagを使った起動許可・不可、キャンセルといったフィルタリング機能があります。
コレを利用して、状況判定用のGameplayTagを作ってアクションの切り替えを行います。

GameplayTagの作成

GameplayTagとアビリティの関係をそれぞれ表にすると下記のようになります。

アビリティ AbilityTags(起動タグ) Activation Blocked Tags(起動不可タグ) Activation Required Tags(起動許可タグ)
GA_Jump Action.Jump State.Hang なし
GA_Hang State.Hang なし なし
GA_JumpHang Action.Jump なし State.Hang

GA_Jump、GA_Hang、GA_Climbの作成

ジャンプをGAS風に書き換える C++プロジェクトを作成すると、ThirdPersonCharacterはC++で書かれた処理の中でスペースバーとジャンプの処理をしています。
そこでBPでJumpインプットアクションノードを出してPressedでGA_Jumpが呼ばれるように書き換えます。
f:id:JackpotDevelop:20211223194714p:plain

GA_Jump
GAクラスの中でJump関数を呼ぶのはなんだか冗長な気がしますが、アビリティのフィルタリングをするためには仕方ありません。
そして詳細欄のタグ設定ではActibation Blocked TagsにState.Hangを追加しました。
このタグが付与されている状況ではGA_Jump(通常ジャンプ)はしないようにするためです。 f:id:JackpotDevelop:20211223194801p:plain f:id:JackpotDevelop:20211223194819p:plain

GA_Hangの作成
崖につかまるアビリティに加え、掴めるポイントであるBP_HangPointを作成します。
ジャンプ中にプレイヤーとBP_HangPointがオーバーラップしたら、掴まり状態に移行することにします。
流れとしては下記のようになります。

  1. スペースバーを押してジャンプする
  2. BP_HangPointにプレイヤーがオーバーラップしてGA_Hangが起動
  3. その状態でスペースバーを押して崖を登る

BP_HangPoint
BP_HangPointは二つのStaticMeshで構成されています。
ひとつは崖の端に配置するためのメッシュ(黄色)。
もうひとつはプレイヤーが掴む状態になったとき、来て欲しい位置を表すメッシュ。
これの向きと位置にプレイヤーを調整するので、コンポーネントは壁の方を向けておきます(画像で赤いX軸になっている方です)。 f:id:JackpotDevelop:20211223214119p:plain

位置調整用メッシュにプレイヤーがオーバーラップすると、State.Hangのタグをプレイヤーへ送ります。
また、滑らかに位置と向きが調整されるようMoveCharacterHoldPosイベントを作成しました。

f:id:JackpotDevelop:20211223214545p:plain

MoveCharacterHoldPosイベント f:id:JackpotDevelop:20211223214728p:plain

GA_Hang f:id:JackpotDevelop:20211223214836p:plain

ぶら下がりの姿勢を繰り返すアニメーションモンタージュを再生。
掴むポイントへ位置を調整された後、引力で落ちてしまわないようにCharacterMovementのMovementModeをFlyingにします。
ジャンプの慣性を消すためVelocityを0にします。

タグの設定はAbilityTagsと Activation OnwnedTagsの2か所。 この設定でGA_Hangが起動するとState.Hangがプレイヤーに付与されます。
f:id:JackpotDevelop:20211223220323p:plain

GA_Climb State.Hangタグが付いている状態でジャンプを押すと崖を登ります。
登るモンタージュを再生するのと同時に、キャラクターのコリジョンを一時的に無効にします。
登るモーションは崖とプレイヤーがかなり近づくので当たり判定が邪魔になるためです。
アビリティ終了後は、変更していたパラメータをもとに戻します。

f:id:JackpotDevelop:20211223220638p:plain

タグの設定は下記のとおりです。
GA_Hangを終了させるためにCancelAbilityWithTagにState.Hangを設定。
また掴み中のみこのアビリティが起動するようにActivation Required Tags にも設定しています。

f:id:JackpotDevelop:20211223222218p:plain

テスト

結果

youtu.be

同じボタンで違うアクションを呼び出すことが出来ました。
アビリティの起動フィルタリングはコンボ攻撃にも使用できます。
株式会社ヒストリア様の記事をご参照ください。

historia.co.jp

補足
前項目のダメージリアクションのように「GA_Jumpというアビリティ1つの中で通常ジャンプと崖上に登るのを切り替える」という作り方でも出来ます。ただ、「地面が泥沼だったらジャンプ力を下げたい」「崖の途中だったら上に向かって飛びついて欲しい」といった仕様が追加される可能性が考えられます。
その時、アビリティがひとつだと玉石混交のクラスとなるため望ましくありません。
「この機能は別アビリティで作るべきか」 という判断は最初のうちは難しいですが、やり方だけでも知っておくとデザインの引き出しが増えて出来るようになります。

まとめ

「初心者のための導入の方法」とか「GASとは?」みたいな記事は数あれど、実例がまだまだ少ないなと思ったので、ロジックがバンバン出てくる記事を書きました。
素早く実装する力を上げるには、例を見て引き出しを増やすのが手っ取り早いものです。
この記事を見て実装のヒントになれば幸いです。