1年ほど趣味や業務を通して、気づいた点について色々触れていきます。
はじめに
この記事はセットアップが終わって、少しずつ分かってきたかな?くらいのヒトが対象です。
範囲が非常に広いので、UGameplayAbility クラスのみに絞ります。
GameplayAbility についてより詳しく知りたいヒトは、サンプル付きの非公式ドキュメント決定版みたいなすごいの(語彙力)があるので読んでおきましょう。有志が日本語訳も作ってます。
アビリティの発動
UGameplayAbility の発動インターフェースは大きく分けて2つあり、用途により使い分ける必要があります。一つのアビリティに対してBPで両方のイベントを作ることはできますが、その場合は ActivateAbility が優先され FromEvent は無効となります。
UAbilitySystemComponent::TryActivateAbility
能動的にアビリティを発動させる場合はこちらを使います。プレイヤーの入力、AI 行動から、など。
TryActivateAbility 内部から CanActivateAbility で発動できるかのチェックが入り ActivateAbility イベントが呼び出されます。
注意点としては、発動時に引数を渡すことはできません。その場合は後述の ActivateAbilityFromEvent を使うことになります。
例えば「地上版と空中版で再生するアニメが違うだけ」みたいな場合でも、アビリティクラスを分けることになります(少し冗長的に感じますが)。
もしくは実際のアビリティは別で用意して、それらを発動させるだけの中継アビリティを作るのもよしです。CanActivateAbility で条件のチェック、ActivateAbility 内で分岐して SendGameplayEvent を呼ぶ、別アビリティとして発動させ、自身はそのまま EndAbility する。
UAbilitySystemComponent::TriggerAbilityFromGameplayEvent
イベントや引数を受け取って受動的に発動させる場合はこちらです。着地した、ダメージを受けた、など。
実際には UGameplayAbility::SendGameplayEvent や UAbilitySystemBlueprintLibrary::SendGameplayEventToActor などのインターフェースから呼び出すことになります。
TryActivateAbility と違い、CanActivateAbility は走りません ウソウソ、走ってました。代わりに ShouldAbilityRespondToEvent でチェックします。CanActivateAbility と違い、デフォルト実装ではコストやクールタイムなど関与しません(基本的にスルー)。
Payload(GameplayEventData)is 何
汎用的な引数コンテナです。ただしこれだけだと拡張性がないので、ContextHandle や TargetData の構造体を継承し、独自にデータを盛り込んでいくことになります。
メイン処理は ActivateAbility だけ
アビリティクラス自体はステートを持ちません。Activate されたら EndAbility まで完全に繋ぐ必要があります。Tick もないため最初は戸惑うかも。非同期処理は全て AbilityTask が担います。
中身の処理順は BP イベントグラフで書くことをおススメします。といっても、小分けにした処理(AbilityTask や BlueprintCallable 関数)を繋ぐだけです。BP ヘビィでノードを連ねるのは、ただ超巨大グラフになるばかりなので気を付けましょう。
Prediction(予測)の理解
GAS はクライアントサイドの prediction (予測)のサポートする機能を備えています : しかしながら、すべてを predict (予測)するわけではありません。 GAS のクライアントサイドの prediction (予測)は、クライアントはサーバーの「 GameplayAbility の有効化と GameplayEffects の適用」の許可を待つ必要がないことを意味します。 (それは)サーバーが(それに)これをする許可を与えることを 「 predict (予測)」 でき、 GameplayEffects を適用するであろうターゲットを predict (予測)する事ができます。 次に、クライアントが有効化した後、サーバーは GameplayAbility のネットワークの遅延時間を実行し、クライアントの predictions (予測)が正しいかそうでないかをクライアントに伝えます。 もしクライアントが predictions (予測)のいずれかで間違っていた場合、サーバーと一致するように「 mispredictions (予測ミス)」変更を「ロールバック」します。
リンクにあるように、予測アビリティではクライアントはサーバーの応答を待たずに処理を実行し、それをトレースするようにサーバーで遅延実行されます。これにより操作の応答性が維持され、自然な入力が可能となります。そういった実行処理の大部分がプラグインのシステム根幹に内包され、しかも十分なテストと最適化がされた堅牢な設計であり、(インターフェースを正しく理解する必要があるが)ネットコードをほとんど気にせずとも動作するのは、このプラグインを使う大きな利点となるでしょう。
ローカルとサーバーで処理をどう分ける?
基本的にクライアントとサーバーで同じ処理が走るように組みますが、いくつかはローカルクライアント/サーバーのみ、としたい場合があります。
WaitInputPress を例にすると、クライアント/サーバーどちらも同じノードを通り、次の処理を待ち受けます。AbilityTask 内ではローカルクライアントのみイベントを処理し、サーバーへ送信します。GameplayAbility 自体はネットコードを意識せずに、"On Press" で先に進むだけです。マルチでもシングルでも同様に動作します。このデザインは実装が本当にシンプルになり推せます。
![278d0708eae9c81068f8aa6889de683f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/e0a95286-76ec-b7b9-1eab-436adee0e174.png) 実際には「分岐先で遅延実行したい」などがあり、AbilityTask を拡張して色々作りました。自由度爆上がりです。非同期処理をこんなにシンプルに書けるなんてすごくない?天才では?Blueprint肯定派になりました。 pic.twitter.com/JQdQmlL4SW
— こおりのなか (@koorinonaka) April 15, 2021
InstancingPolicy, NetExecutionPolicy, NetSecurityPolicy
ふぇぇポリシィ多いよぉ・・・。
NetPolicy はそのままチート対策に繋がるので、各アビリティごとによく検討するとよいでしょう。
InstancingPolicy
アビリティが Activate されたときに、どのようにインスタンス化されるかを示します。デフォルトは "Instanced Per Execution" で実行毎にインスタンス化されますが、ドキュメントにある通り "Instanced Per Actor" がおススメです。"Non Instanced" では多くの制約がかかるのでツライです(UGameplayAbility::GetAvatarActorFromActorInfo が使えないなど)。
NetExecutionPolicy
クライアント/サーバーの実行許可を与えます。
- LocalPredicted ... 主にクライアントから実行するアビリティに設定します。サーバーから実行した場合も同様に動作します。
- LocalOnly ... 中継アビリティ(別アビリティを呼び出すだけの分岐アビリティ)などに利用しました。あまり用途がないかも。
- ServerInitiated ... サーバーでのみ発行されるイベント(ダメージなど)を受けて、サーバー主体で呼び出されクライアントでも同様に動作する必要があるもの、などに利用しました。
- ServerOnly ... パッシブなど、サーバーのみでしか動作しないものに設定します。
NetSecurityPolicy
クライアント/サーバーでアビリティの開始/終了の実行可否を与えます。クライアントから途中でアビリティをキャンセルできるかどうか、など。
OnAvatarSet
EventBeginPlay 的なやつ。GiveAbility、もしくは InitAbilityActorInfo でアバターが変わると走ります。
初期化処理とかはここで行う。終了処理は?ないんだな、それが。
void UMyGameplayAbility::OnAvatarSet(
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec )
{
Super::OnAvatarSet( ActorInfo, Spec );
if ( bActivateAbilityOnGranted )
{
ActorInfo->AbilitySystemComponent->TryActivateAbility( Spec.Handle, false );
}
}
パッシブアビリティの発動にも使えます。
ネットワークにおける Montage 再生
アニメの再生は全て AbilitySystemComponent を通して再生する必要があり、システム内で Montage 再生情報のレプリケーション制御が行われます。MontageStop/MontageJumpToSection/MontageSetNextSectionName などについても同様です。
注意点としては、LocallyControlled なクライアントはサーバーと同様の Montage 再生を自身で呼ぶ必要があるということです。例えばサーバーのみで PlayMontageAndWait を呼んだとしても、再生の呼び出しは SimulatedClient/Server でのみ行われます。そのため Montage 再生を行うアビリティは LocallyControlledClient/Server どちらにも存在する必要があり、NetExecutionPolicy は LocalPredicted/ServerInitiated に設定しなければなりません。
CommitAbility
GameplayAbility には Cost/Cooldown の設定ができますが、CommitAbility を呼ぶまで消費されません。これは多様なゲームデザインで役に立ちます。
- 攻撃がヒットした瞬間に消費する
- 詠唱が完了したら消費する
- 遅延実行のアビリティで実際に消費するときに CommitAbility を呼び、結果値が false だった(実行時は Cost が満たしていたが、デバフで足りなくなった)とき EndAbility する、などなど。
発動時に形式的に CommitAbility を呼ぶだけではなく、デザインに合わせて検討するのがよいでしょう。Cost/Cooldown(その他制約)の設定がなければ呼ばなくてもよいです。
Async Action はいいぞ
ココから持ってきてそのまま追加するだけでヨシです。
Cost/Cooldown の状態を UI に反映することはよくあると思いますが、Latent ノードを使えばロジックを内包して非同期処理を作ることができます。UBlueprintAsyncActionBase により非同期ノードを作るのはとても簡単になっているので、使いこなせると出来ることが一気に広がりますよ!
[UE4]簡単なLatentノードの作り方 でも紹介されているのでぜひぜひ辿って見てみてください。
InputPressed|Released
GiveAbility 引数の FGameplayAbilitySpec.InputID で通知が行われます。これは少し様式的でセットアップに迷うかもしれません(UAbilitySystemComponent::BindAbilityActivationToInputComponent あたりが参考、あとコレ)。
1アビリティ1アクションで設計されています。引数がないので複数アクションの通知を受けることはできません。
単に InputPressed イベントを発行したいのであれば、UAbilitySystemComponent::AbilityLocalInputPressed を直接呼ぶでもよいです。