MiniAbilitySystem

以前の記事で紹介したGameplayAbilitySystemは大変便利なのですがC++必須でアビリティ、アトリビュート、エフェクト、タスクと機能も豊富なため、プログラムに馴染みのない人にはちょっとハードルが高く取り回しも難しい機能かと思います。

そこで、機能を必要最低限のアビリティに絞り、BPだけで構築した超簡略版のMiniAbititySystemというものを作ってみたので紹介したいと思います。

MiniAbilitySystemとは

そもそもMiniAbilitySystemって何なのよ?って話から始めたいと思いますが、みなさんがアクションゲームをプレイするとき、キャラクターは基本的な移動のほかに、「攻撃」「コンボ」「回避」「魔法」といったアクションを行うかと思います。

また敵から攻撃を受ければ「ダメージ」状態にもなりますし、HPが0になれば「死亡」状態になります。

ゲーム制作においてはこれらのステートが適切に遷移するようにプログラムで制御を書いていくわけですが、この辺の処理はさまざまなロジックが絡み合って複雑なプログラムになりがちです。

MiniAbilitySystemでは、これら各ステートごとにアビリティという形で個別にBPを作成します。それぞれのBPにそのステート専用の動作を記述していくことによりブループリントがスパゲティ状態になりづらくします。

また、各アビリティにはタグ(GameplayTag)を設定することができ、アビリティを実行する際に「このタグがあるアビリティが存在している間は、このアビリティは起動できない」、「このアビリティを実行するためには、このタグが付いているアビリティが存在しないといけない」「このアビリティが起動すると、このタグを持ったアビリティはキャンセルされる」といった排他制御を行うことができます。

今回紹介する例では、攻撃用に3種類のアビリティを作成しますが、これらは3連コンボで発動させる予定です。

攻撃1は攻撃ボタンが押されたら起動し、攻撃2も攻撃ボタンが押されたら起動するように設定します。ただ、このままだと攻撃1,2が同時に実行されてしまいますよね?

これを防ぐために攻撃2は、攻撃1で設定されるタグがない状態では発動しないように設定します。これにより攻撃ボタンを押していくと、かならず攻撃1、攻撃2の順でアビリティが実行されるようになります。攻撃3も同様で、攻撃2のタグがないと実行されないようにしておきます。これで3連コンボの完成です。

また反対にダメージを受けた場合は、攻撃1でも2でも3でも、それらは全部キャンセルしてダメージアビリティを実行するのを最優先にしなくてはなりません。これもタグによる排他制御で制御可能です。ダメージアビリティ実行時は攻撃1,2,3が保持しているタグを持つアビリティは全部キャンセルさせるようにタグの指定を書いておけばいいのです。

このように、大抵の状態遷移はタグの設定により管理可能なので、各ステートによる条件の分岐をBPで書かずにすむ、というメリットを享受できます。

ま、とはいえ実物を見ないことにはイメージもわかないと思いますので、以降、順を追って実際にBPを組みながら説明していきたと思います。

あ、なおUE4エディタは個人的な理由で言語が英語になっております。日本語を使われている方がほとんどだと思いますので、その辺は日本語版機能に読み替えて読んでいただければと思います。

環境のセットアップ

MiniAbilitySystemを入手する

github.com

MiniAbilitySystemはここから入手できます。UE4のバージョンは4.22です。

git使ってる方はcloneしてもよいですし、使ってない方はzipをダウンロードでもOKです。

ThirdPersonのプロジェクトを用意する

次にベースとなるThirdPersonプロジェクトを新規作成してください。

で、先ほど用意したMiniAbilitySystemのプロジェクトからMiniAbilitySystemフォルダをMigrateでもってくるか、手作業でContentディレクトリにコピーしてください。

ContentBrowserでは

f:id:nca03132:20190410153808p:plain

こんな感じになるかと思います。

Shinbiをプロジェクトに追加・設定する

今回はShinbiを使います。ParagonアセットのShinbiをプロジェクトに追加してください。

f:id:nca03132:20190410154124p:plain

追加が終わったらThirdPersonCharacterのスキンメッシュとアニメをShinbiに入れ替えます。

f:id:nca03132:20190410154548p:plain

このThirdPersonCharacterを開いて

f:id:nca03132:20190410154646p:plain

ComponentsのMeshを選択して

f:id:nca03132:20190410154817p:plain

詳細のSkeletalMeshを「Shinbi」、AnimClassを「Shinbi_AnimBlueprint」に設定します。

これでコンパイル&実行すればマネキンの代わりにShinbiが歩くようになっているはずです。

あと最後に、ShinbiのアニメーションBPは常に最初にレベルスタートポーズをとるようになっているので、外してしまいましょう。

f:id:nca03132:20190410155719p:plain

このShibi_AnimBlueprintを開いて、EventGraphのBeginPlayにつながっている

f:id:nca03132:20190410155838p:plain

このMontagePlayを削除しておきます。

f:id:nca03132:20190410160048p:plain

これで下準備は完了です。

アビリティマネージャの追加とアビリティの作成

キャラクタへのアビリティマネージャの追加

では、キャラクタBPにアビリティマネージャを追加しましょう。

f:id:nca03132:20190410165216p:plain

このThirdPersonCharacterをダブルクリックで開いて、コンポーネントの追加でBP_MiniAbilityManagerComponentを追加しましょう。

f:id:nca03132:20190410165514p:plain

こんな感じになります。

アビリティクラスの作成と登録

次にアビリティクラスを作成します。

ThirdPersonBP→Blueprintsの中で右クリックからBlueprint Classを作成し、親クラスには

f:id:nca03132:20190410165904p:plain

このBP_MiniAbilityを選択しましょう。

作成したブループリントの名前はBP_Attack1Abilityとしておきます。

f:id:nca03132:20190410170037p:plain

次のこのBPを開いて攻撃に関するノードを追加していきます。

アビリティクラスからアビリティインスタンスが生成された直後に呼ばれるInitializeイベントをオーバーライドして

f:id:nca03132:20190410173242p:plain

GetAbilityManagerでMiniAbilityManagerComponentを取得しGetOwnerでオーナーであるActorを引っ張ってきてCharacterにキャストしCharacter変数に保存。さらにCharacterからSkeletalMeshコンポーネントを引っ張ってきてMesh変数に保存します。

次に、アビリティインスタンスがアクティベートされたときにくるActivateAbilityをオーバーライドして

f:id:nca03132:20190410174013p:plain

Mesh変数を使ってPlayMontageを呼び出します。再生するモンタージュ(Montage to Play)にはPrimaryMelee_B_Slow_Montageを指定します。OnBlendOut,OnInterruptedのピンの先にEndAbilityを配置しモンタージュ再生が終了次第、アビリティインスタンスが終了するようにします。

注意点として、アビリティは終了時に絶対に「EndAbility」を呼ぶ必要があります。

でないとアビリティインスタンスはずっとメモリに残り続け動作も重くなりいずれメモリがあふれます。

最後にこのアビリティクラスは何のタグを受け取るとアビリティインスタンスを生成・アクティベートするかの設定を行います。

f:id:nca03132:20190410174853p:plain

クラスのデフォルト設定を選択し、詳細の

f:id:nca03132:20190410175026p:plain

このActivationTagsに起動するタグを指定します。今回は「Activation.Attack」というタグを設定します。

Edit→Add New Gameplay Tagを選択しNameのところに「Activation.Attack」と記述します。

f:id:nca03132:20190410175511p:plain

設定できると

f:id:nca03132:20190410180628p:plain

こんな感じになります。

このアビリティクラスを先ほどThirdPersonCharacterに追加したアビリティマネージャに登録します。

ThirdPersonCharacterのBPを開きComponentsの中のBP_MiniAbilityManagerComponentを選択し、詳細の中のMiniAbilityManagerの中にあるInitialAbilityClassesに追加します。

f:id:nca03132:20190410180946p:plain

このプラスを押して0の項目にBP_Attack1Abilityを設定します。

f:id:nca03132:20190410181037p:plain

これで、アビリティマネージャがアビリティクラスを保有した状態となりました。

アビリティの起動

ではマウスを左クリックしたらBP_Attack1Abilityがアクティベートされるようにノードを書き足してみましょう。

ThirdPersonCharacterのBPを開いてマウスイベントのLeftMouseButtonをオーバーライドします。

アビリティマネージャが、指定したタグのアビリティをアクティベートさせる関数はActivateAbilityClassesWithTagsです。

アクティベートタグにはさきほどの「Activation.Attack」を指定し、LeftMouseButtonからつなぎます。

f:id:nca03132:20190410181615p:plain

これでマウスを左クリックするとアビリティマネージャが自分が保持しているアビリティクラスリストの中で、起動タグに「Activation.Attack」を持つものをインスタンス化しアクティベートするようになりました。

コンパイルしてプレイしてみてください。

左クリックでShinbiがクルリと剣を振れば成功です。

タグによるアビリティの排他制御

さて、Shinbiが攻撃モーションとってくれるようになりました。まずはめでたい。

が、実は現時点で左クリックを連打してみると・・・・モーションがキャンセルされて何度も最初から開始されます。

これは考えてみれば当然の話で、アビリティインスタンス実行中に、再度左クリックされると(同じBP_Attack1Abilityクラスではあるけど)別のアビリティインスタンスが生成されモンタージュを最初から再生します。これにより以前から動作していたアビリティは、同スロットで再生されたモンタージュによりPlayMontageが中断されEndAbilityに処理が流れて終了します。

これを防ぐためにはアビリティ実行中は別のアビリティを実行しないようにする排他制御が必要です。

具体的にはアビリティが実行されたときに「実行中だよ」的なステートのタグを保持するようにして、再度アビリティが起動されそうになったときに「実行中だよ」というステートタグを持つアビリティが存在したら起動しないように設定します。

アクティベート中に保持するタグを設定

まずは実行開始したアビリティが「実行中だよ」というステートを持つようにタグを設定します。

BP_Attack1AbilityのClassDefaultsをクリックし

f:id:nca03132:20190410191912p:plain

詳細のActivationOwnedTagsのところに「State.Montage.UpperBody.Attack1」というタグを追加します。

f:id:nca03132:20190410192214p:plain

これでBP_Attack1Abilityがアクティベートされたあと上記タグを保持するようになります。

アビリティのアクティベーションをブロックするタグを設定

次に実行中のアビリティが「実行中だよ」というタグを持っていたら、自分自身のアビリティをアクティベートしないようタグ設定を行います。

詳細のActivationBlockedTagsに「State.Montage」というタグを追加します。

f:id:nca03132:20190410192945p:plain

排他の動作原理 

これでコンパイルして実行すると、左クリックを連打してもキャンセルが発生しなくなります。

原理としては、まずアビリティインスタンスがアクティベートされたときに「State.Montage.UpperBody.Attack1」というタグを持つようになります。さらに次のアビリティインスタンスがアクティベートされそうになったとき、すでにアクティベート済みのアビリティインスタンスの中でActivationBlockedTagsで指定されてた「State.Montage」というタグを持っているものがいないか調べます。

最初のアビリティは「State.Montage.UpperBody.Attack1」を持っているので「State.Montage」も持っているものと判定され(GameplayTagsでhasTags系の判定をするドットの後半部分はワイルドカード判定となる)アビリティのアクティベートが中断されます。

最初のアビリティインスタンスモンタージュ再生が終わるとEndAbilityによりアビリティはいなくなりブロックタグもひっかからなくなるので、アビリティのアクティベートが再度実行されるようになります。

アビリティのアクティベーションに必要なタグ

次に、アビリティをアクティベーションするために必要なタグについて考えてみましょう。

先ほどの排他の場合は、すでにアクティベーション済みのアビリティが特定のタグを持っている場合は自アビリティがアクティベーションできないという状況でしたが、反対にすでにアクティベーション済みのアビリティが特定のタグを持っていないと自アビリティがアクティベーションできない、というタグも存在します。

ここではそれを使ってコンボを作ってみましょう。

攻撃2のアビリティクラスの作成

まずBP_Attack1Abilityを複製してBP_Attack2Abilityとします。

BP_Attack2Abilityを開いてPlayMontageで指定しているモンタージュをPrimaryMelee_C_Slow_Montageに変更します。

f:id:nca03132:20190410195631p:plain

次にタグ設定です。

BP_Attack2AbilityのClassDefaultsをクリックし詳細のタグ設定を確認します。

2段目の攻撃も起動タグはActivation.Attackのままでよいのですが、前提条件として1段目の攻撃実行中である必要があるのでActivationRequiredTagsに「State.Montage.UpperBody.Attack1」を指定します。これでBP_Attack2Abilityは一段目の攻撃を実行中でないとアクティベートされなくなります。

またActivationOwnedTagsは「State.Montage.UpperBody.Attack2」にしておきます。

2段目の攻撃は1段目の攻撃中に発生するためActivationBlockedTagsはクリアしておきます。

上記の結果、以下のようなタグ設定となります。

f:id:nca03132:20190410200827p:plain

攻撃2のアビリティクラスの登録

よく忘れるのですが、アビリティクラスは作成しただけでは実行されません。当たり前ですがアビリティマネージャに登録する必要があります。

ThirdPersonCharacterのBPを開き、コンポーネントでBP_MiniAbilityManagerを選択し、詳細のInitial Ability Classesに先ほど作成したBP_Attack2Abilityを設定します。

f:id:nca03132:20190410201223p:plain

この状態でコンパイルし実行してみてください。

・・・・

はい、攻撃の1段目が出なくなり、常に2段目のみ発生しています。

なぜでしょうか・・・・?

結論から言うと現時点ではMiniAbilitySystemの仕様となっています。

MiniAbilitySystemは登録されているアビリティクラスを配列の頭からサーチしていきマッチした条件のアビリティをアクティベートします。

現状は左クリックが発生すると配列の0にいるBP_Attack1Abilityがアクティベートされるのですが、その時点でBP_Attack2Abilityの実行要件を満たしてしまい、配列の1番目のBP_Attack2Abilityも連続でアクティベートしていまうのです。その結果、常に攻撃の2段目が発生するわけです。

この現象は、1個ずつアクティベート可能なアビリティを見ていくのではなく、アクティベート可能なアビリティを最初にリストアップしておき、そのあとでそれらを連続アクティベートするなどの仕組みで回避することが可能なのですが、それはそれで同一フレームで直前のアビリティの挙動によるほかのアビリティの排他制御ができなくなるため、いろいろ悩んだ末、あえてこのままの仕様としています。

なので後発的に発生するアビリティは意識してリストの頭のほうに持っていくようにしてください。

f:id:nca03132:20190410202145p:plain

さて、BP_Attack2Abilityを先頭に持ってきました。再度気を取り直してプレイしてみてください。

左クリックでBP_Attack1Ability,BP_Attack2Abilityの順番で攻撃が出るはずです。

MontageNotifyによるモンタージュとの連携

2連コンボが出せるようになりましたが、まだ問題があります。

左クリックを連打すると1段目が発生したすぐあと2段目が出せてしまいます。

本来なら1段目の攻撃が終わるぐらいのタイミングで2段目の入力を受け付けたい感じです。これを解消するためにモンタージュのNofityとの連携を行います。

コンボ入力を受け付けるタイミングにMontageNotifyを設定する

PrimaryMelee_B_Slow_Montageを開いてください(PlayMontageで指定している個所の右側の虫眼鏡をクリックすると早いです)。

Shinbiの1段目の攻撃が再生されていると思いますが、この中で2段目の攻撃入力を受け付ける範囲にMontageNofityWindowを設定します。

Notifiesのスライダーで10フレームぐらいの位置で右クリックしAddNotifyState→MontageNotifyWindowを選択します。

f:id:nca03132:20190410210340p:plain

AnimNotify_PlayMontageNotifyWindowが作られますがこれは範囲指定なので終了時間をモンタージュの最後のほうに移動させます。

f:id:nca03132:20190410210708p:plain

AnimNotify_PlayMontageNotifyWindowをクリックして右側の詳細パネルのNotifyNameを「Ready.Montage」と指定しておきましょう

f:id:nca03132:20190410211144p:plain

これで10フレームと最終フレーム間際で、アビリティのBPに設置してあるPlayMontageのOnNotifyBegin,OnNotifyEndピンに処理が来るようになります。

このときNotifyNameには「Ready.Montage」の文字列が入ってきます。

 アビリティのBPで次アビリティ起動許可用のタグを設定する

今度はBP_Attack1Abilityを開いてください。

先ほど書いたようにMontageNotifyの設定によりOnNotifyBegin,OnNotifyEndに処理が来るようになります。

ここではOnNotifyBeginで次アビリティ起動許可用の「Ready.Montage.UpperBody.Attack2」タグを追加し、OnNofityEndで削除するようにします。AddAbilityTagとRemoveAbilityTagを使います。

f:id:nca03132:20190410213328p:plain

これでコンボ連携可能時間の間「Ready.Montage.UpperBody.Attack2」が存在するようになりました。

このタグを利用するのはBP_Attack2Abilityなので、そちらを開いてください。

現在アビリティを実行するのに必要なタグとしてActivation RequiredTagsには「State.Montage.UpperBody.Attack1」が設定されていますが、これを「Ready.Montage.UpperBody.Attack2」に書き換えます。

f:id:nca03132:20190410213648p:plain

これで適切なタイミングでのみコンボがつながるようになるはずです。

3段目のコンボへ

やり方は同じなので詳細は載せませんが、3段目のコンボも同じ要領で作ることができます。

BP_Attack2AbilityをコピーしてBP_Attack3Abilityを作り、PlayMontageで再生するアニメーションは「PrimaryMelee_D_Slow_Montage」を指定します。

ActivationOwnedTagを「Stage.Montage.UpperBody.Attack3」とし、ActivationRequiredTagsに「Ready.Montage.UpperBody.Attack3」を指定します。

「PrimaryMelee_C_Slow_Montage」を開いて先ほどと同じ要領でコンボのタイミングのMontageNotifyを追加し、BP_Attack2AbilityのPlayMontageのOnNotifyBegin、OnNotifyEndにAddAbilityTagとRemoveAbilityTagをつなげて「Ready.Montage.UpperBody.Attack3」を追加・削除すればOKです。

ダメージによるモンタージュの中断

実装紹介の最後に通常アクションより優先度が高いダメージのアビリティについて、簡単に実装してみたいと思います。

モンタージュの作成

まずダメージのモンタージュを作ります。Shinbiのアニメーションに入っている「HitReact_Front」を右クリックしCreate→CreateAnimMontageを選択します。

f:id:nca03132:20190410215719p:plain

これでHitReact_Front_Montageが作成されるので、開きます。

f:id:nca03132:20190410220626p:plain

ここで、グループ設定を「DefaultGroup.UpperBody」に変更して保存します。

アビリティBPの作成とアビリティマネージャへの登録

BP_Attack1Abilityを複製してBP_HitReactAbilityという名前で保存し開きます。

PlayMontageで再生するモンタージュの指定を先ほど作った「HitReact_Font_Montage」に変更します。

今回はNotify系の処理は行わないのでOnNotifyBegin,OnNotifyEndから先につながっているピン・ノードは削除します。

f:id:nca03132:20190410220734p:plain

タグの設定

ダメージが起動するタグは「Activation.HitReact」とします。

このアビリティ実行中はほかのアビリティのアクティベートを抑制したいのでBlockAbilitiesWithTagで「Activation」を指定しておきます。基本的に今まで作ったアビリティは起動タグを「Activation.***」に設定しているので、このダメージアビリティ実行中はほかのアビリティはすべてアクティベートされなくなります。

また、状態タグであるActivationOwnedTagsには「State.Montage.UpperBody.HitReact」を設定しておきます。

f:id:nca03132:20190410221203p:plain

テスト実行

ではテスト実行をしてみましょう。とはいえ、このサンプルではダメージを与える仕組みを作っていませんので、右クリックしたら敵からダメージを与えられたリアクションをするように設定してみます。

ThirdPersonCharacterのBPを開いてマウスイベントのRightMouseButtonをオーバーライドします。

アビリティマネージャからActivateAbilityClassesWithTagsにピンを接続して

アクティベートタグには「Activation.HitReact」を指定し、RightMouseButtonからつなぎます。

f:id:nca03132:20190410221620p:plain

これで右クリックするとダメージのリアクションが発生するようになります。

攻撃アビリティを出している最中でも右クリックするとダメージアビリティがアクティベートされ攻撃アビリティは中断します。またダメージモーション再生終了までほかのアビリティのアクティベートを受け付けなくなります。

MiniAbilitySystemの問題点

いろいろ小回りが利いて便利なMiniAbilitySystemにも問題点があります。

  • Delayがない
    BP_MiniAbilityの基底クラスはUObjectなのでDelayノードが使えません。
    何か処理を実行したあと待つことができないのでいろいろ不便な状況に遭遇することがあります。
    どうしても必要であればSetTimerEventは動くので、それでなんとか回避する感じになります。
  • Timelineがない
    これもDelayがないのと同じ原因です。
    Timelineが必要な場合はコンポーネントかキャラ側に処理を逃がしてアビリティからそれを呼ぶ形が無難かと思います。

この辺、使用上は要注意となります。

今回紹介しきれなかったちょっとした小話

  • タグの設計
    今回のサンプルではさらさらと適当にタグをつけているように見えますが、タグ設計は超大事です。ピシッとしたポリシーなしに適当にタグ付けしてると、どんな条件で何が起きるのかさっぱりわからなくなるので、頭の中でタグとアビリティの連携イメージをしっかり固めてから実装しましょう。
  • onEndAbility
    今回使う機会がありませんでしたが、このイベントもすごく大事です。
    アビリティ終了時に必ず通る処理なのでアビリティ実行中でキャラクターのパラメータを一時的に書き換えたりした場合、ここで戻すことが可能です。
    自作のゲームでは飛行状態やロープスイング状態もアビリティで管理していて実行中移動モードを書き換えているので、それを戻すのに使ってたりします。
    あとはダッシュしたあとで移動スピードを戻すのとかにも使えそうです。
  • Tickについて
    一応、標準状態でアクティベートされたアビリティはTickを受け取ることができますが、アビリティの数が多くなると処理速度的にオーバーヘッドが大きくなるため、不要であればコンポーネント側でTickをオフしてしまうことをお勧めします。
    何かしらの状態変化をアビリティ側で確認したいのであれば都度SendMessageで情報を通知するという手が使えるかもしれません。
  • 攻撃の判定
    攻撃の当たり判定などもモンタージュ上で攻撃判定したいタイミングにMontageNotifyを仕込んで、PlayMontageのOnNotifyBeginでコールバックを受けて、そのタイミングでコリジョンチェックを行うことにより実現できるかと思います。
    実際にダメージを与える処理は、ぱっと思いつく感じだと「UE4で実装されているApplyDamageを使う」「ダメージ情報をMiniAbilityParamに設定して、相手キャラクタのアビリティマネージャが保持しているダメージアビリティを都度アクティベートする」「常時ダメージアビリティをアクティベートするようにしておき、そこにSendMessageでダメージ情報を送る」といったあたりが思いつきますが、まぁ好みで構わないと思います。
  • 武器の切り替え
    剣から槍などに装備を変えた場合、当然ながら使用可能なアビリティも変化します。そういったときはRemoveAbilityClassesで旧武器のアビリティを削除し、AddAbilityClassesで新武器のアビリティを追加すれば、比較的容易に武器切り替えシステムが実装できます。

最後に

MiniAbilitySystemとかたいそうなこと言ってますが、ごめんなさい、ぜんぜんちんまいソースです。単にシステムって言ってみたかっただけですw

でもコンパクトでソース見れば仕組みはすぐ理解してもらえると思うので、自分好みに組み替えてどんどん活用・応用していってもらえればと思います。

バグの指摘や、こうしたほうがもっとよくなるよ!などのアドバイスもどんどこお待ちしています。