Gameplay Ability System コードリーディング ⑦ WaitTargetData - Confirmation Type (Custom / CustomMulti)

上記の続きになります。

前回で Confirmation Type の InstantUser Confirmed の 2 つの確認を行いました。

今回は残った CustomCustomMulti を確認していきます。

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() を呼び出すことで、ターゲティングデータが準備できたかどうかを判断します。 TargetActorUGameplayAbility::CancelTaskByInstanceName() に応答することで、ターゲティングのキャンセルもします。
CustomMulti GameplayTargeting アビリティは、 UGameplayAbility::ConfirmTaskByInstanceName() を呼び出すことで、ターゲティングデータが準備できたかどうかを判断します。 TargetActorUGameplayAbility::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 この配列は OnGameplayTaskActivatedAdd されており詳細は追いませんが名前からは (なんらか) Activate 時に追加されていると考えて良さそうです。なので、Ability 内にある GameplayTask のうち、Active な状態のものが入っていると考えて先に進みます。(当然ながら UAbilityTask_WaitTargetDataGameplayTask です。AbilityTask の基底クラスが GameplayTask なので)

そこからは予想通り引数の InstanceName と Task の InstanceName の一致を確認し、ExternalConfirm を呼び出しています。

では、GameplayTaskGetInstanceNameExternalConfirm を確認します。

// ヘッダ
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 では ExternalConfirmGetInstanceName も 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 が組まれています)

これはおそらく、GameplayTaskbEndTask で終了しないフラグを渡される可能性があり、その場合にはそこで利用する 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つを同時に処理しており、それぞれが CancelTaskInstanceNamesEndTaskInstanceNames の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();
}

こちらはわかりやすく Cancelleddelegate に直接流しています。特にデータの生成も必要ありませんし、たしかにです。

Custom と CustomMulti の所在

さて、ここまでドキュメントを起点に処理を追ってきましたが、肝心の CustomCustomMulti に関しては一切出てきていません。

これらは、あえて指定してというよりは、InstantUser 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/CustomMultiUserConfirmed でバインドされる入力には対応せず、Confirm/CancelTaskByInstanceName からのみ Confirm or Cancel され、かつ、CustomMuti はデータの準備ができたとしても EndTask されず、Confirm/CancelTaskByInstanceNamebEndTask フラグが true の場合に終了されるというもののようです。

使ってみる

GitHub - dany1468/GASDocumentation_PlayGround at blog/confirmation_custom_custommulti

WaitTargetDataRadius を試したサンプルからの継続ですが、さらに別の branch にしてあります。(C++ の方をビルドしないと、GA_Sample_SpawnActorAbility Input IDAbility ID が不正になっていそうです。何かたりなさそうな気がします。)

上記にあるコードは動作のために少し変えてあるんですが、以下では余計な部分を省いたノードで示します。

Custom の場合

今回は、Custom の Confirm のためのトリガとして WaitInputPress を利用しました。これは、単にこの Ability の Input、つまり今回は T の press を待ちます。
ただ、当然最初の T は Ability の Activate になるので、Ability が Activate している状態、つまり WaitInputPress が待ち状態になっている場合の T の press を検出します。(要は2回 T を押します)

InstanceNameWaitRadiusSample で合わせてあります。

Custom の場合はそのまま Radius も終了するため継続はできませんので End Task にもチェックを付けて WaitTargetData も終了させておきます。

CustomMulti の場合

多少ややこしいのですが、Custom Multi の場合は一度 Confirm されても Radius は終了しないため複数回待機ができるということを試しています。

そのため、Confirm Task By Instance NameEnd Taskfalse になっています。

実行

youtu.be

今回は「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 ができるので用途は広がりそうです。パッとは思いつきませんが 😅