- Gameplay Ability System コードリーディング ① WaitOverlap - You are done!
- Gameplay Ability System コードリーディング ② SpawnActor - You are done!
- Gameplay Ability System コードリーディング ③ WaitTargetData - You are done!
- Gameplay Ability System コードリーディング ④ WaitTargetData - GameplayAbilityTargetActor (及び Radius) - You are done!
- Gameplay Ability System コードリーディング ⑤ WaitTargetData - Radius の利用 - You are done!
- Gameplay Ability System コードリーディング ⑥ WaitTargetData - Confirmation Type - You are done!
上記の続きになります。
前回で Confirmation Type の Instant
と User Confirmed
の 2 つの確認を行いました。
今回は残った Custom
と CustomMulti
を確認していきます。
EGameplayTargetingConfirmation::Type | Unreal Engine Documentation
Custom と CustomMulti
GASDocumentation の該当部分を改めて確認します。
4.11.2 Target Actors | GASDocumentation/README.jp.md at lang-ja · sentyaanko/GASDocumentation
EGameplayTargetingConfirmation::Type
いつターゲットが確認されるか Custom
GameplayTargeting アビリティは、 UGameplayAbility::ConfirmTaskByInstanceName()
を呼び出すことで、ターゲティングデータが準備できたかどうかを判断します。TargetActor
はUGameplayAbility::CancelTaskByInstanceName()
に応答することで、ターゲティングのキャンセルもします。CustomMulti
GameplayTargeting
アビリティは、UGameplayAbility::ConfirmTaskByInstanceName()
を呼び出すことで、ターゲティングデータが準備できたかどうかを判断します。TargetActor
はUGameplayAbility::CancelTaskByInstanceName()
に応答することで、ターゲティングのキャンセルもします。データ生成時にAbilityTask
を終了しないでください。
上記で出てくる関数は GameplayAbility
にあるようなので確認してみると BP からも呼び出せるようです。
コードの確認
ヘッダの確認
以下は GameplayAbility.h
ですが、すでに確認した通り BlueprintCallable
で定義されています。
/** Finds all currently active tasks named InstanceName and confirms them. What this means depends on the individual task. By default, this does nothing other than ending if bEndTask is true. */ UFUNCTION(BlueprintCallable, Category = Ability) void ConfirmTaskByInstanceName(FName InstanceName, bool bEndTask); /** Add any task with this instance name to a list to be canceled (not ended) next frame. See also EndTaskByInstanceName. */ UFUNCTION(BlueprintCallable, Category = Ability) void CancelTaskByInstanceName(FName InstanceName);
ドキュメントの CustomMutli
にあった 「データ生成時に AbilityTask
を終了しないでください。」というのは、bool bEndTask
が関係してきそうです。
しかし InstanceName
とはなんでしょうか。
InstanceName
改めて WaitTargetData
ノードを見てみると、これまで触れてきませんでしたがそのままの名前の入力ピンがあります。おそらくこれが一致するということが重要なのではないかと想像しつつ先に進みます。
ソースの確認
ConfirmTaskByInstanceName
まずは ConfirmTaskByInstanceName
です。
void UGameplayAbility::ConfirmTaskByInstanceName(FName InstanceName, bool bEndTask) { TArray<UGameplayTask*, TInlineAllocator<8> > NamedTasks; for (UGameplayTask* Task : ActiveTasks) { if (Task && Task->GetInstanceName() == InstanceName) { NamedTasks.Add(Task); } } for (int32 i = NamedTasks.Num() - 1; i >= 0; --i) { UGameplayTask* CurrentTask = NamedTasks[i]; if (IsValid(CurrentTask)) { CurrentTask->ExternalConfirm(bEndTask); } } }
ActiveTasks
この配列は OnGameplayTaskActivated
で Add
されており詳細は追いませんが名前からは (なんらか) Activate 時に追加されていると考えて良さそうです。なので、Ability 内にある GameplayTask
のうち、Active な状態のものが入っていると考えて先に進みます。(当然ながら UAbilityTask_WaitTargetData
も GameplayTask
です。AbilityTask
の基底クラスが GameplayTask
なので)
そこからは予想通り引数の InstanceName
と Task の InstanceName
の一致を確認し、ExternalConfirm
を呼び出しています。
では、GameplayTask
の GetInstanceName
と ExternalConfirm
を確認します。
// ヘッダ FORCEINLINE FName GetInstanceName() const { return InstanceName; } /** Called when the task is asked to confirm from an outside node. What this means depends on the individual task. By default, this does nothing other than ending if bEndTask is true. */ virtual void ExternalConfirm(bool bEndTask); // ソース void UGameplayTask::ExternalConfirm(bool bEndTask) { UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Verbose , TEXT("%s ExternalConfirm called, bEndTask = %s, State : %s") , *GetName(), bEndTask ? TEXT("TRUE") : TEXT("FALSE"), *GetTaskStateName()); if (bEndTask) { EndTask(); } }
特筆すべき点はなさそうです。
続いて、WaitTargetData
を含む AbilityTask
がどうやって InstanceName
を設定しているかですが、初期化時に呼び出される AbilityTask
の static 関数である NewAbilityTask
内で設定されています。
// AbilityTask.h /** Helper function for instantiating and initializing a new task */ template <class T> static T* NewAbilityTask(UGameplayAbility* ThisAbility, FName InstanceName = FName()) { check(ThisAbility); T* MyObj = NewObject<T>(); MyObj->InitTask(*ThisAbility, ThisAbility->GetGameplayTaskDefaultPriority()); MyObj->InstanceName = InstanceName; return MyObj; }
そして、GameplayTask
では ExternalConfirm
も GetInstanceName
も override は無いため、続いて WaitTargetData
の方に移ります。
/** Called when the ability is asked to confirm from an outside node. What this means depends on the individual task. By default, this does nothing other than ending if bEndTask is true. */ void UAbilityTask_WaitTargetData::ExternalConfirm(bool bEndTask) { check(AbilitySystemComponent); if (TargetActor) { if (TargetActor->ShouldProduceTargetData()) { TargetActor->ConfirmTargetingAndContinue(); } } Super::ExternalConfirm(bEndTask); }
ここまで来ると前回の記事でも見た ConfirmTargetingAndContinue
関数が出てきます。
TargetActor が Confirm された状態になり、その後の処理が行われます。
異なっている点は、前回見た User Confirmed
の場合は TargetActor がすぐに Destroy されたのに対して、ここでは、Super::ExternalConfirm(bEndTask);
が呼び出されるだけで、GameplayTask
のライフサイクルに任せているという点です。(GameplayTask が Destroy されれば、TargetActor も Destroy されるように OnDestroy
が組まれています)
これはおそらく、GameplayTask
が bEndTask
で終了しないフラグを渡される可能性があり、その場合にはそこで利用する TargetActor も当然 Destroy してはならないということだろうと思います。(どこかにちゃんと書いて有りそうですが)
CancelTaskByInstanceName
void UGameplayAbility::CancelTaskByInstanceName(FName InstanceName) { //Avoid race condition by delaying for one frame CancelTaskInstanceNames.AddUnique(InstanceName); GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UGameplayAbility::EndOrCancelTasksByInstanceName); } void UGameplayAbility::EndOrCancelTasksByInstanceName() { // Static array for avoiding memory allocations TArray<UGameplayTask*, TInlineAllocator<8> > NamedTasks; // Call Endtask on everything in EndTaskInstanceNames list for (int32 j = 0; j < EndTaskInstanceNames.Num(); ++j) { FName InstanceName = EndTaskInstanceNames[j]; NamedTasks.Reset(); // Find every current task that needs to end before ending any for (UGameplayTask* Task : ActiveTasks) { if (Task && Task->GetInstanceName() == InstanceName) { NamedTasks.Add(Task); } } // End each one individually. Not ending a task may do "anything" including killing other tasks or the ability itself for (int32 i = NamedTasks.Num() - 1; i >= 0; --i) { UGameplayTask* CurrentTask = NamedTasks[i]; if (IsValid(CurrentTask)) { CurrentTask->EndTask(); } } } EndTaskInstanceNames.Empty(); // Call ExternalCancel on everything in CancelTaskInstanceNames list for (int32 j = 0; j < CancelTaskInstanceNames.Num(); ++j) { FName InstanceName = CancelTaskInstanceNames[j]; NamedTasks.Reset(); // Find every current task that needs to cancel before cancelling any for (UGameplayTask* Task : ActiveTasks) { if (Task && Task->GetInstanceName() == InstanceName) { NamedTasks.Add(Task); } } // Cancel each one individually. Not canceling a task may do "anything" including killing other tasks or the ability itself for (int32 i = NamedTasks.Num() - 1; i >= 0; --i) { UGameplayTask* CurrentTask = NamedTasks[i]; if (IsValid(CurrentTask)) { CurrentTask->ExternalCancel(); } } } CancelTaskInstanceNames.Empty(); }
NextTick に流すために実質の処理は EndOrCancelTasksByInstanceName
が担っています。ただ、ややこしいのですが、この関数は EndTaskByInstanceName
でも同様に利用されています。
void UGameplayAbility::EndTaskByInstanceName(FName InstanceName) { //Avoid race condition by delaying for one frame EndTaskInstanceNames.AddUnique(InstanceName); GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UGameplayAbility::EndOrCancelTasksByInstanceName); }
ここまでで分かる通り EndOrCancelTasksByInstanceName
では Cancel されたタスクと End されたタスクの2つを同時に処理しており、それぞれが CancelTaskInstanceNames
と EndTaskInstanceNames
の2つの配列で扱われています。
ここまで分かると、内容もそれ程面倒ではなく、Confirm と同じく InstanceName で一致させ、Cancel の場合は GameplayTask の ExternalCancel
を呼び出しているだけです。
// ヘッダ /** Called when the task is asked to cancel from an outside node. What this means depends on the individual task. By default, this does nothing other than ending the task. */ virtual void ExternalCancel(); // ソース void UGameplayTask::ExternalCancel() { UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Verbose , TEXT("%s ExternalCancel called, current State: %s") , *GetName(), *GetTaskStateName()); EndTask(); }
こちらも同じく、実際の処理は子クラスの方ですね。Confirm と同じく AbilityTask の方には実装は無いので、直接 WaitTargetData を見ます。
/** Called when the ability is asked to confirm from an outside node. What this means depends on the individual task. By default, this does nothing other than ending if bEndTask is true. */ void UAbilityTask_WaitTargetData::ExternalCancel() { check(AbilitySystemComponent); if (ShouldBroadcastAbilityTaskDelegates()) { Cancelled.Broadcast(FGameplayAbilityTargetDataHandle()); } Super::ExternalCancel(); }
こちらはわかりやすく Cancelled
の delegate に直接流しています。特にデータの生成も必要ありませんし、たしかにです。
Custom と CustomMulti の所在
さて、ここまでドキュメントを起点に処理を追ってきましたが、肝心の Custom
や CustomMulti
に関しては一切出てきていません。
これらは、あえて指定してというよりは、Instant
や User Confirmed
以外として扱われています。以下でその箇所を列挙していきます。
void UAbilityTask_WaitTargetData::FinalizeTargetActor(AGameplayAbilityTargetActor* SpawnedActor) const { check(SpawnedActor); check(Ability); // User ability activation is inhibited while this is active AbilitySystemComponent->SpawnedTargetActors.Push(SpawnedActor); SpawnedActor->StartTargeting(Ability); if (SpawnedActor->ShouldProduceTargetData()) { // If instant confirm, then stop targeting immediately. // Note this is kind of bad: we should be able to just call a static func on the CDO to do this. // But then we wouldn't get to set ExposeOnSpawnParameters. if (ConfirmationType == EGameplayTargetingConfirmation::Instant) { SpawnedActor->ConfirmTargeting(); } else if (ConfirmationType == EGameplayTargetingConfirmation::UserConfirmed) { // Bind to the Cancel/Confirm Delegates (called from local confirm or from repped confirm) SpawnedActor->BindToConfirmCancelInputs(); } } }
まず FinalizeTargetActor
のでは、Instant
は即 Confirm 、UserConfirmed
の場合は Generic な Confirm/Cancel の入力にバインドとなっており、Cusom/CustomMuti では何のバインドもありません。
よって、Custom/CustomMulti は前回見たような入力マッピングからの Confirm/Cancel のInputを受け付けません。
/** Valid TargetData was replicated to use (we are server, was sent from client) */ void UAbilityTask_WaitTargetData::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& Data, FGameplayTag ActivationTag) { check(AbilitySystemComponent); FGameplayAbilityTargetDataHandle MutableData = Data; AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey()); /** * Call into the TargetActor to sanitize/verify the data. If this returns false, we are rejecting * the replicated target data and will treat this as a cancel. * * This can also be used for bandwidth optimizations. OnReplicatedTargetDataReceived could do an actual * trace/check/whatever server side and use that data. So rather than having the client send that data * explicitly, the client is basically just sending a 'confirm' and the server is now going to do the work * in OnReplicatedTargetDataReceived. */ if (TargetActor && !TargetActor->OnReplicatedTargetDataReceived(MutableData)) { if (ShouldBroadcastAbilityTaskDelegates()) { Cancelled.Broadcast(MutableData); } } else { if (ShouldBroadcastAbilityTaskDelegates()) { ValidData.Broadcast(MutableData); } } if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti) { EndTask(); } } /** The TargetActor we spawned locally has called back with valid target data */ void UAbilityTask_WaitTargetData::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data) { check(AbilitySystemComponent); if (!Ability) { return; } FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, ShouldReplicateDataToServer()); const FGameplayAbilityActorInfo* Info = Ability->GetCurrentActorInfo(); if (IsPredictingClient()) { if (!TargetActor->ShouldProduceTargetDataOnServer) { FGameplayTag ApplicationTag; // Fixme: where would this be useful? AbilitySystemComponent->CallServerSetReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey(), Data, ApplicationTag, AbilitySystemComponent->ScopedPredictionKey); } else if (ConfirmationType == EGameplayTargetingConfirmation::UserConfirmed) { // We aren't going to send the target data, but we will send a generic confirmed message. AbilitySystemComponent->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::GenericConfirm, GetAbilitySpecHandle(), GetActivationPredictionKey(), AbilitySystemComponent->ScopedPredictionKey); } } if (ShouldBroadcastAbilityTaskDelegates()) { ValidData.Broadcast(Data); } if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti) { EndTask(); } }
レプリケーションや予測の部分の説明はできないのですが、ひとまずここでは、いずれの関数も最後にある CustomMulti
でない場合のみ EndTask
するということだけ注目します。
最初に触れたドキュメントにも 「データ生成時に AbilityTask
を終了しないでください」とありましたが、ここにその実装があるということです。特に後者の関数は Wait したデータの準備ができた場合の関数なのでレプリケーションや ConfirmationType
に依らず呼び出されます。
よって、Custom/CustomMulti
は UserConfirmed
でバインドされる入力には対応せず、Confirm/CancelTaskByInstanceName
からのみ Confirm or Cancel され、かつ、CustomMuti
はデータの準備ができたとしても EndTask
されず、Confirm/CancelTaskByInstanceName
の bEndTask
フラグが true の場合に終了されるというもののようです。
使ってみる
GitHub - dany1468/GASDocumentation_PlayGround at blog/confirmation_custom_custommulti
WaitTargetData
と Radius を試したサンプルからの継続ですが、さらに別の branch にしてあります。(C++ の方をビルドしないと、GA_Sample_SpawnActor
の Ability Input ID
と Ability ID
が不正になっていそうです。何かたりなさそうな気がします。)
上記にあるコードは動作のために少し変えてあるんですが、以下では余計な部分を省いたノードで示します。
Custom の場合
今回は、Custom の Confirm のためのトリガとして WaitInputPress
を利用しました。これは、単にこの Ability の Input、つまり今回は T
の press を待ちます。
ただ、当然最初の T
は Ability の Activate になるので、Ability が Activate している状態、つまり WaitInputPress
が待ち状態になっている場合の T
の press を検出します。(要は2回 T を押します)
InstanceName
は WaitRadiusSample
で合わせてあります。
Custom の場合はそのまま Radius も終了するため継続はできませんので End Task
にもチェックを付けて WaitTargetData
も終了させておきます。
CustomMulti の場合
多少ややこしいのですが、Custom Multi の場合は一度 Confirm されても Radius は終了しないため複数回待機ができるということを試しています。
そのため、Confirm Task By Instance Name
の End Task
も false
になっています。
実行
今回は「Left Mouse Button to ~」の UI メッセージは関係無いのですが、Ability の Activate 状態に応じて表示されて都合がいいのでそのままにしています。
Custom の方は T
で SpawnActor が実行されると UI のメッセージも非表示になるため、Ability 自体が終了していることが分かります。
一方で、Custom Multi は SpawnActor されても UI のメッセージは表示されたままで、連続して T
キーを待ち受け、Cube が Spawn されていることが分かります。(いつ T
を押している化が動画からは分からないのが微妙なんですが。。)
まとめ
今回は Confirmation Type の Custom/CustomMulti
を眺めました。
User Confirmed
とは異なり、他の Wait 系の Task と組み合わせて Confirm/Cancel ができるので用途は広がりそうです。パッとは思いつきませんが 😅