Gameplay Ability System コードリーディング ⑥ WaitTargetData - Confirmation Type

上記の続きになります。

前回の記事で、WaitTargetData を実際に TargetActor Radius を指定して使ってみました。
その中で、ひとまず GASDocumentation の Confirm の利用方法をそのまま流用しましたが、内容は分からず利用していたのでここではそれを見ていきたいと思います。

一つは上記の WaitTargetData ノードの Confirmation Type に関して、もう一つは Show Ability Confirm Cancel Text ノードに関してです。
後者は GASDocumentation の実装も関わってくるので、引き続き GASDocumentation のコードの参照も行います。

まずは、Confirm と Cancel の入力を受け付けるところから

分かりやすいところで、Confirm のメッセージが出た時に、左マウスクリックで Confirm が成立する部分から見ていきます。

Input

GASDocumentation/GASDocumentation.h at fca71af6ffa99a2d106419a15bbf63901c474986 · tranek/GASDocumentation · GitHub

UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
    // 0 None
    None            UMETA(DisplayName = "None"),
    // 1 Confirm
    Confirm         UMETA(DisplayName = "Confirm"),
    // 2 Cancel
    Cancel          UMETA(DisplayName = "Cancel"),

EGDAbilityInputID は前回の記事で Sample の input を定義した場所でもあります。

続いて、この EGDAbilityInputID::ConfirmEGDAbilityInputID::Cancel の利用箇所を見ていきます。一箇所しかありません。

GASDocumentation/GDHeroCharacter.cpp at cc222c6a21c2615c1840e262efe76c766d6c9c74 · tranek/GASDocumentation · GitHub

void AGDHeroCharacter::BindASCInput()
{
    if (!ASCInputBound && AbilitySystemComponent.IsValid() && IsValid(InputComponent))
    {
        AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
            FString("CancelTarget"), FString("EGDAbilityInputID"), static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));

        ASCInputBound = true;
    }
}

この BindASCInput は以下の 2 箇所から呼び出されています。キャラクターが利用できるようになるときには呼び出されていると考えてよさそうです。

AbilitySystemComponent#BindAbilityActivationToInputComponent を見ていきますが、おそらく名前的にも Input の Bind をまるっと行っていそうです。

まず、引数になっている FGameplayAbilityInputBinds です。こちらは Engine の Plugin なのでコードだけ貼ります。

/** Structure that tells AbilitySystemComponent what to bind to an InputComponent (see BindAbilityActivationToInputComponent) */
struct FGameplayAbilityInputBinds
{
    FGameplayAbilityInputBinds(FString InConfirmTargetCommand, FString InCancelTargetCommand, FString InEnumName, int32 InConfirmTargetInputID = INDEX_NONE, int32 InCancelTargetInputID = INDEX_NONE)
        : ConfirmTargetCommand(InConfirmTargetCommand)
        , CancelTargetCommand(InCancelTargetCommand)
        , EnumName(InEnumName)
        , ConfirmTargetInputID(InConfirmTargetInputID)
        , CancelTargetInputID(InCancelTargetInputID)
    { }

    /** Defines command string that will be bound to Confirm Targeting */
    FString ConfirmTargetCommand;

    /** Defines command string that will be bound to Cancel Targeting */
    FString CancelTargetCommand;

    /** Returns enum to use for ability binds. E.g., "Ability1"-"Ability9" input commands will be bound to ability activations inside the AbiltiySystemComponent */
    FString EnumName;

    /** If >=0, Confirm is bound to an entry in the enum */
    int32 ConfirmTargetInputID;

    /** If >=0, Cancel is bound to an entry in the enum */
    int32 CancelTargetInputID;

    UEnum* GetBindEnum() { return FindObject<UEnum>(ANY_PACKAGE, *EnumName); }
};

Confirm / Cancel に関するものと、 EnumName という他の Ability に関するものが分けられています。

EnumName には FString("EGDAbilityInputID") が入れられていますが、これは最初に見た Confirm, Cancel, Sample などの Enum のことです。この指定は GASDocumentation 独自のものなので、この Enum は各プロジェクトで自由にできるようですね。

続いて AbilitySystemComponent#BindAbilityActivationToInputComponent です。

void UAbilitySystemComponent::BindAbilityActivationToInputComponent(UInputComponent* InputComponent, FGameplayAbilityInputBinds BindInfo)
{
    UEnum* EnumBinds = BindInfo.GetBindEnum();

    SetBlockAbilityBindingsArray(BindInfo);

    // EGDAbilityInputID Enum の列挙に対してバインド設定
    for(int32 idx=0; idx < EnumBinds->NumEnums(); ++idx)
    {
        const FString FullStr = EnumBinds->GetNameStringByIndex(idx);
        
        // Pressed event
        {
            FInputActionBinding AB(FName(*FullStr), IE_Pressed);
            AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::AbilityLocalInputPressed, idx);
            InputComponent->AddActionBinding(AB);
        }

        // Released event
        {
            FInputActionBinding AB(FName(*FullStr), IE_Released);
            AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::AbilityLocalInputReleased, idx);
            InputComponent->AddActionBinding(AB);
        }
    }

    // 以降は Confirm と Cancel のみの設定

    // Bind Confirm/Cancel. Note: these have to come last!
    if (BindInfo.ConfirmTargetCommand.IsEmpty() == false)
    {
        FInputActionBinding AB(FName(*BindInfo.ConfirmTargetCommand), IE_Pressed);
        AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::LocalInputConfirm);
        InputComponent->AddActionBinding(AB);
    }
    
    if (BindInfo.CancelTargetCommand.IsEmpty() == false)
    {
        FInputActionBinding AB(FName(*BindInfo.CancelTargetCommand), IE_Pressed);
        AB.ActionDelegate.GetDelegateForManualSet().BindUObject(this, &UAbilitySystemComponent::LocalInputCancel);
        InputComponent->AddActionBinding(AB);
    }

    if (BindInfo.CancelTargetInputID >= 0)
    {
        GenericCancelInputID = BindInfo.CancelTargetInputID;
    }
    if (BindInfo.ConfirmTargetInputID >= 0)
    {
        GenericConfirmInputID = BindInfo.ConfirmTargetInputID;
    }
}

コード内にコメントを足しましたが、EGDAbilityInputIDEnum を列挙し全てに Press/Release のバインドを行い、その後 Confirm / Cancel のバインド設定をおこなっているようです。

どういうアクションをバインドしている?

FInputActionBinding を利用していることや、Action Mapping と対応したキー名になっていることからもなんらかの Action にバインドしていることは間違いありません。

まずは今回の主題でもある Confirm / Cancel にバインドされている UAbilitySystemComponent::LocalInputConfirm / LocalInputCancel を見てみます。

void UAbilitySystemComponent::LocalInputConfirm()
{
    FAbilityConfirmOrCancel Temp = GenericLocalConfirmCallbacks;
    GenericLocalConfirmCallbacks.Clear();
    Temp.Broadcast();
}

void UAbilitySystemComponent::LocalInputCancel()
{   
    FAbilityConfirmOrCancel Temp = GenericLocalCancelCallbacks;
    GenericLocalCancelCallbacks.Clear();
    Temp.Broadcast();
}

ここでは GenericLocalConfirmCallbacks / GenericLocalCancelCallbacksdelegate に関しては素通りしますが、簡単にはこれらを Broadcast し Clear しています。

続いて、各 Ability にバインドされている UAbilitySystemComponent::AbilityLocalInputReleased を見てみます。

void UAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
    // Consume the input if this InputID is overloaded with GenericConfirm/Cancel and the GenericConfim/Cancel callback is bound
    if (IsGenericConfirmInputBound(InputID))
    {
        LocalInputConfirm();
        return;
    }

    if (IsGenericCancelInputBound(InputID))
    {
        LocalInputCancel();
        return;
    }

    // ---------------------------------------------------------

    ABILITYLIST_SCOPE_LOCK();
    for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
    {
        if (Spec.InputID == InputID)
        {
            if (Spec.Ability)
            {
                Spec.InputPressed = true;
                if (Spec.IsActive())
                {
                    if (Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false)
                    {
                        ServerSetInputPressed(Spec.Handle);
                    }

                    AbilitySpecInputPressed(Spec);

                    // Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
                    InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());                   
                }
                else
                {
                    // Ability is not active, so try to activate it
                    TryActivateAbility(Spec.Handle);
                }
            }
        }
    }
}

まず、前半で Confirm / Cancel の入力に対する処理が行われています。Enum として指定した EGDAbilityInputID には Confirm / Cancel も含まれていますし、Action Mapping にも設定されているため、こちらにも流れてくることへの対応かもしれません。

その後は、ActivatableAbilities を for で回しながら、Ability (コード内では Spec) がすでに Active か否かで処理を行っています。ここで Ability の Active 化も行っていたのですね。

ここまでで、入力設定から Confirm と Cancel がどのように入力を受付、そのイベントを流しているのかを確認しました。

続いて、先程すっ飛ばした、Confirm と Cancel にバインドされていた delegate を追っていきます。

AbilitySystemComponent GenericLocalConfirmCallbacks / GenericLocalCancelCallbacks

/** Generic local callback for generic ConfirmEvent that any ability can listen to */
FAbilityConfirmOrCancel GenericLocalConfirmCallbacks;

/** Generic local callback for generic CancelEvent that any ability can listen to */
FAbilityConfirmOrCancel GenericLocalCancelCallbacks;

上記が定義になります。

WaitTargetData に戻る

void UAbilityTask_WaitTargetData::FinalizeTargetActor(AGameplayAbilityTargetActor* SpawnedActor) const
{
    // 略

    if (SpawnedActor->ShouldProduceTargetData())
    {
        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();
        }
    }
}

上記は AbilityTask_WaitTargetData の FinalizeTargetActor です。SpawnedActor は前回見た Radius のような GameplayAbilityTargetActor です。

ここで、ノードでいうところの Confirmation TypeInstantUser Confirmed かの分岐があります。

今回は User Confirmed の方を見ていきます。

void AGameplayAbilityTargetActor::BindToConfirmCancelInputs()
{
    check(OwningAbility);

    const FGameplayAbilityActorInfo* const Info = OwningAbility->GetCurrentActorInfo();

    // AbilitySystemComponent を取得
    UAbilitySystemComponent* const ASC = Info->AbilitySystemComponent.Get();

    if (ASC)
    {
        if (Info->IsLocallyControlled())
        {
            // We have to wait for the callback from the AbilitySystemComponent. Which will always be instigated locally
            ASC->GenericLocalConfirmCallbacks.AddDynamic(this, &AGameplayAbilityTargetActor::ConfirmTargeting); // Tell me if the confirm input is pressed
            ASC->GenericLocalCancelCallbacks.AddDynamic(this, &AGameplayAbilityTargetActor::CancelTargeting);   // Tell me if the cancel input is pressed

            // Save off which ASC we bound so that we can error check that we're removing them later
            GenericDelegateBoundASC = ASC;
        }
        else
        {   
            // 略
        }
    }
}

見てわかる通り、ここで前述の GenericLocalConfirmCallbacks / GenericLocalCancelCallbacks に対してバインドされています。

AGameplayAbilityTargetActor::ConfirmTargeting

まずは Confirm の場合です。

void AGameplayAbilityTargetActor::ConfirmTargeting()
{
    // 略

    if (IsConfirmTargetingAllowed())
    {
        ConfirmTargetingAndContinue();
        if (bDestroyOnConfirmation)
        {
            Destroy();
        }
    }
}

void AGameplayAbilityTargetActor::ConfirmTargetingAndContinue()
{
    check(ShouldProduceTargetData());
    if (IsConfirmTargetingAllowed())
    {
        TargetDataReadyDelegate.Broadcast(FGameplayAbilityTargetDataHandle());
    }
}

ConfirmTargetingAndContinue は以前にも触れましたが、 ConfirmTargeting では、ConfirmTargetingAndContinue を呼び出し Destroy をする流れになっています。

そして ConfirmTargetingAndContinue で TargetData の待機完了の delegate に流しているということですね。(このまま WaitTargetData の ValidData に流れます)

以前見た Radius もそうですが、多くの場合はこの関数は各 TargetActor の方で独自の TargetDataHandle を作成するために override されています。次に見る Cancel も同様です。

続いて Cancel の場合です。

/** Outside code is saying 'stop everything and just forget about it' */
void AGameplayAbilityTargetActor::CancelTargeting()
{
    // 略

    CanceledDelegate.Broadcast(FGameplayAbilityTargetDataHandle());
    Destroy();
}

こちらも Cancel の delegate に流して Destroy というだけです。WaitTargetData の Cancelled に流れます。

さて、ここまでで、Confirm / Cancel の入力と、それが WaitTargetData の Confirmation に続く流れを確認できました。

最後は、UI とどのように関連しているかを確認します。

Show Ability Confirm Cancel Text で UI が表示される部分の確認

UI 部分は当然 GASDocumentation 側です。

GASDocumentation/GDPlayerState.cpp at 44a197049d3343b4b448755fb998d531acebf26e · tranek/GASDocumentation · GitHub

void AGDPlayerState::ShowAbilityConfirmCancelText(bool ShowText)
{
    AGDPlayerController* PC = Cast<AGDPlayerController>(GetOwner());
    if (PC)
    {
        UGDHUDWidget* HUD = PC->GetHUD();
        if (HUD)
        {
            HUD->ShowAbilityConfirmCancelText(ShowText);
        }
    }
}

PC->GetHUD(); を辿っていくと、以下の設定値にたどり着きます。値は BP から設定されています。

GASDocumentation/GDPlayerController.h at 44a197049d3343b4b448755fb998d531acebf26e · tranek/GASDocumentation · GitHub

以下の UI_HUD を利用しているようです。
GASDocumentation/UI_HUD.uasset at 44a197049d3343b4b448755fb998d531acebf26e · tranek/GASDocumentation · GitHub

Designer から見ても間違いありません。

コードに戻りますが、ShowAbilityConfirmCancelText が呼び出されています。

GASDocumentation/GDHUDWidget.h at 44a197049d3343b4b448755fb998d531acebf26e · tranek/GASDocumentation · GitHub

Blueprint の方で実装されているようです。

単純に表示の切り替えをしているだけのようです。

Confirm / Cancel に関しては UI は特に関係無い

上記で分かる通り、単に UI は表示されているだけで、GAS の Confirm には何ら関係はありません。実際、UI の表示に関するノードを削除したとしても問題無く動作します。

Confirmation Type を Instant にした場合どうなるか

再び UAbilityTask_WaitTargetData::FinalizeTargetActor に戻りますが、Instant の場合は SpawnedActor->ConfirmTargeting(); が呼び出されています。

この関数ですが、実はすでに確認していて、Confirm 時の Callback で呼び出される関数です。

なので、Instant とはその名の通りで、「即 Confirm されたことにする」という挙動になっています。

まとめ

ざっと Confirmation Type について見てきました。ただ、InstantUser Confirmed を見ただけで CustomCustom Multi に関してはまだ見れていません。

余談ですが、見てきた通りで Confirm に紐付けられる Callback は一気に broadcast されるため、例えば WaitTargetData を2つ用意し、両方とも User Confirmed にした場合は (片方で End Ability しなければ) 両方が Confirm された扱いになります。

User Confirmed を使う場合においては、対応する入力キーは一つに固定されるように見えるため、Ability によって confirm のキーを変更したい場合には工夫が必要になりそうです。(またはそういう仕組が用意されていそうな気もします)

以下のドキュメントに「4.6.2.1 Binding to Input without Activating Abilities (Abilities を有効化せずに入力をバインド)」がありますが、それがヒントになるかもしれません(が試してません。。)。

4.6.2 Binding Input to the ASC (ASC に入力をバインド) | GASDocumentation/README.jp.md at lang-ja · sentyaanko/GASDocumentation