Gameplay Ability System コードリーディング ③ WaitTargetData

上記の続きになります。
WaitOverlap は、待機状態になる AbilityTask、SpawnActor は spawn を伴う AbilityTask としてのサンプルになっていました。

今回は WaitTargetData を見ていきます。

WaitTargetData

UAbilityTask_WaitTargetData | Unreal Engine Documentation

名前の通り、データの提供を待つタスクのようです。

また、Class を渡すか、TargetActor を渡すかの違いはありますが、両方のノードにおいて提供されるデータを出力する指示が必要です。そのために、順序は前後しますがまずはファクトリを見ます。

ファクトリ

上記で貼ったノードが2つとうことで分かるように、static ファクトリも2つ存在しています。

/** Spawns target actor and waits for it to return valid data or to be canceled. */
UFUNCTION(BlueprintCallable, meta=(HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true", HideSpawnParms="Instigator"), Category="Ability|Tasks")
static UAbilityTask_WaitTargetData* WaitTargetData(UGameplayAbility* OwningAbility, FName TaskInstanceName, TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType, TSubclassOf<AGameplayAbilityTargetActor> Class);

/** Uses specified target actor and waits for it to return valid data or to be canceled. */
UFUNCTION(BlueprintCallable, meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true", HideSpawnParms = "Instigator"), Category = "Ability|Tasks")
static UAbilityTask_WaitTargetData* WaitTargetDataUsingActor(UGameplayAbility* OwningAbility, FName TaskInstanceName, TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType, AGameplayAbilityTargetActor* TargetActor);

そして、以下の提供データ用の引数を持ちます。

WaitTargetData WaitTargetDataUsingActor
TSubclassOf<AGameplayAbilityTargetActor> Class AGameplayAbilityTargetActor* TargetActor

両方に共通する AGameplayAbilityTargetActor は以下にドキュメントがあります。

AGameplayAbilityTargetActor | Unreal Engine Documentation

これには以下のような子クラスが存在します。提供を待つデータの種類別に分かれているようです。

ファクトリのソースは以下のようになっています。

UAbilityTask_WaitTargetData* UAbilityTask_WaitTargetData::WaitTargetData(UGameplayAbility* OwningAbility, FName TaskInstanceName, TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType, TSubclassOf<AGameplayAbilityTargetActor> InTargetClass)
{
    UAbilityTask_WaitTargetData* MyObj = NewAbilityTask<UAbilityTask_WaitTargetData>(OwningAbility, TaskInstanceName);        //Register for task list here, providing a given FName as a key
    MyObj->TargetClass = InTargetClass;
    MyObj->TargetActor = nullptr;
    MyObj->ConfirmationType = ConfirmationType;
    return MyObj;
}

UAbilityTask_WaitTargetData* UAbilityTask_WaitTargetData::WaitTargetDataUsingActor(UGameplayAbility* OwningAbility, FName TaskInstanceName, TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType, AGameplayAbilityTargetActor* InTargetActor)
{
    UAbilityTask_WaitTargetData* MyObj = NewAbilityTask<UAbilityTask_WaitTargetData>(OwningAbility, TaskInstanceName);        //Register for task list here, providing a given FName as a key
    MyObj->TargetClass = nullptr;
    MyObj->TargetActor = InTargetActor;
    MyObj->ConfirmationType = ConfirmationType;
    return MyObj;
}

TargetClassTargetActor のそれぞれのプロパティがあり、それぞれに設定しているだけのようです。

delegate

UPROPERTY(BlueprintAssignable)
FWaitTargetDataDelegate ValidData;

UPROPERTY(BlueprintAssignable)
FWaitTargetDataDelegate Cancelled;

順序が前後しましたが BlueprintAssignabledelegate の存在を確認しました。ノードの状態とも一致しますね。

Activate / BeginSpawningActor / FinishSpawningActor

virtual void Activate() override;

UFUNCTION(BlueprintCallable, meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true"), Category = "Abilities")
bool BeginSpawningActor(UGameplayAbility* OwningAbility, TSubclassOf<AGameplayAbilityTargetActor> Class, AGameplayAbilityTargetActor*& SpawnedActor);

UFUNCTION(BlueprintCallable, meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true"), Category = "Abilities")
void FinishSpawningActor(UGameplayAbility* OwningAbility, AGameplayAbilityTargetActor* SpawnedActor);

ヘッダファイルを見てわかる通り、このクラスにはここまでの WaitOverlap (Activate) と SpawnActor (BeginSpawningActor, FinishSpawningActor) で登場した 3 つの関数がすべて揃っています。

改めて AbilityTask.h の説明文を貼ります。

We have additional support for AbilityTasks that want to spawn actors. Though this could be accomplished in an Activate() function, it would not be possible to pass in dynamic "ExposeOnSpawn" actor properties. This is a powerful feature of blueprints, in order to support this, you need to implement a different step 3:

Instead of an Activate() function, you should implement a BeginSpawningActor() and FinishSpawningActor() function.

Activate 関数では not be possible to pass in dynamic "ExposeOnSpawn" actor properties となっているだけで、Activate を実装するべきではないということではないようです。

では実装を見ていきます。

Activate

void UAbilityTask_WaitTargetData::Activate()
{
    // Need to handle case where target actor was passed into task
    if (Ability && (TargetClass == nullptr))
    {
        if (TargetActor)
        {
            AGameplayAbilityTargetActor* SpawnedActor = TargetActor;
            TargetClass = SpawnedActor->GetClass();

            RegisterTargetDataCallbacks();


            if (!IsValid(this))
            {
                return;
            }

            if (ShouldSpawnTargetActor())
            {
                InitializeTargetActor(SpawnedActor);
                FinalizeTargetActor(SpawnedActor);

                // Note that the call to FinalizeTargetActor, this task could finish and our owning ability may be ended.
            }
            else
            {
                TargetActor = nullptr;

                // We may need a better solution here.  We don't know the target actor isn't needed till after it's already been spawned.
                SpawnedActor->Destroy();
                SpawnedActor = nullptr;
            }
        }
        else
        {
            EndTask(); // そもそも TargetActor も null なら不正な状態なので即終了
        }
    }
}

冒頭に 「Need to handle case where target actor was passed into task」とあるように、WaitTargetDataUsingActor の方を利用された場合の処理のようです。

つまり、TargetActor が渡されているということは Spawn の必要が無いため、ここで完結できる ということです。

TargetClass = SpawnedActor->GetClass(); の部分で、WaitTargetDataUsingActor の場合には設定されない TargetClass を補完していることが分かります。

ざっくりとこの時点で注目したい部分だけ見ますが、

if (ShouldSpawnTargetActor())
{
    InitializeTargetActor(SpawnedActor);
    FinalizeTargetActor(SpawnedActor);

    // Note that the call to FinalizeTargetActor, this task could finish and our owning ability may be ended.
}

ShouldSpawnTargetActor が真の場合に InitializeFinalize を渡された Actor に対して行っていることが分かります。(この SpawnedActor は元々入力された TargetActor です)

BeginSpawningActor / FinishSpawningActor

bool UAbilityTask_WaitTargetData::BeginSpawningActor(UGameplayAbility* OwningAbility, TSubclassOf<AGameplayAbilityTargetActor> InTargetClass, AGameplayAbilityTargetActor*& SpawnedActor)
{
    SpawnedActor = nullptr;

    if (Ability)
    {
        if (ShouldSpawnTargetActor())
        {
            UClass* Class = *InTargetClass;
            if (Class != nullptr)
            {
                if (UWorld* World = GEngine->GetWorldFromContextObject(OwningAbility, EGetWorldErrorMode::LogAndReturnNull))
                {
                    SpawnedActor = World->SpawnActorDeferred<AGameplayAbilityTargetActor>(Class, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
                }
            }

            if (SpawnedActor)
            {
                TargetActor = SpawnedActor;
                InitializeTargetActor(SpawnedActor);
            }
        }

        RegisterTargetDataCallbacks();
    }

    return (SpawnedActor != nullptr);
}

void UAbilityTask_WaitTargetData::FinishSpawningActor(UGameplayAbility* OwningAbility, AGameplayAbilityTargetActor* SpawnedActor)
{
    if (IsValid(SpawnedActor))
    {
        check(TargetActor == SpawnedActor);

        const FTransform SpawnTransform = AbilitySystemComponent->GetOwner()->GetTransform();

        SpawnedActor->FinishSpawning(SpawnTransform);

        FinalizeTargetActor(SpawnedActor);
    }
}

こちらも SpawnActor の AbilityTask と同様の作りになっています。
InTargetClass は入力ピンで指定された TargetClass です。

BeginSpawningActor では、Activate の方と同じく ShouldSpawnTargetActor の真をチェックしています。
その後は、SpawnActor と同じように SpawnActorDeferred を使って Spawn 処理を書いています。

SpawnedActor を得た後は、また Activate の方と同じように InitializeTargetActor を呼び出しています。

FinishSpawningActor では、SpawnActor と同じように FinishSpawning で、UCS の呼び出しを行い、最後に Activate でも呼び出していた FinalizeTargetActor を呼び出しています。

ここまで見てわかる通り、BeginSpawningActorFinishSpawningActor で Spawn 処理の手続きはあるものの、やっていることは Activate で直接 Actor に対して行った処理を同じことを行っているということです。

さて、ここまでで WaitTargetData の序盤的な箇所を見てきました。ただ、ここまででは肝心の WaitTargetData な部分が出てきていません。

TargetClass/TargetActor として渡された AGameplayAbilityTargetActor が、どのように扱われるのかを見ていきます。

InitializeTargetActor / FinalizeTargetActor

ヘッダファイルにはコメントも無いので省きます。

void UAbilityTask_WaitTargetData::InitializeTargetActor(AGameplayAbilityTargetActor* SpawnedActor) const
{
    check(SpawnedActor);
    check(Ability);

    SpawnedActor->MasterPC = Ability->GetCurrentActorInfo()->PlayerController.Get();

    // If we spawned the target actor, always register the callbacks for when the data is ready.
    SpawnedActor->TargetDataReadyDelegate.AddUObject(const_cast<UAbilityTask_WaitTargetData*>(this), &UAbilityTask_WaitTargetData::OnTargetDataReadyCallback);
    SpawnedActor->CanceledDelegate.AddUObject(const_cast<UAbilityTask_WaitTargetData*>(this), &UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback);
}

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();
        }
    }
}

詳細は避けながらざっくりと処理を追ってみます。

  • InitializeTargetActor
    • SpawnedActorMasterPC に、現在の Ability の PlayerController を設定
    • SpawnedActorTargetDataReadyDelegateUAbilityTask_WaitTargetData::OnTargetDataReadyCallback を紐づけ
    • SpawnedActorCanceledDelegateUAbilityTask_WaitTargetData::OnTargetDataCancelledCallback を紐づけ
  • FinalizeTargetActor
    • AbilitySystemComponentSpawnedTargetActorsSpawnedActor を追加
    • SpawnedActorStartTargeting を呼び出し
    • SpawnedActorShouldProduceTargetData が真の場合
      • ConfirmationType に応じて、即 Confirm するか、Confirm か Cancel かの入力待ちをバインド

SpawnedActor 、つまりは AGameplayAbilityTargetActor の関連クラスの詳細を無視すれば、以下のようなことが行われているようです。

  1. SpawnedActor に対して TargetData の Ready or Cancel に関するコールバックを付ける
  2. ASC の SpawnedTargetActors に SpawnedActor を追加
  3. SpawnedActorStartTargeting を呼び出し(待ちの開始?)
  4. WaitTargetDataConfirmationType に応じて、SpawnedActor の Confirm 処理の切り替え

ノードの利用側としては、SpawnedActorTargetData を発見すればコールバック (ValidData or Cancelled) が呼び出され、それが ConfirmationType に依存しているという感じでしょうか。

では、コールバックで delegate が呼び出されているかの確認をしておきたいと思います。

OnTargetDataReadyCallback / OnTargetDataCancelledCallback

/** The TargetActor we spawned locally has called back with valid target data */
void UAbilityTask_WaitTargetData::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& Data)
{
    // Prediction に関する処理につき省略

    if (ShouldBroadcastAbilityTaskDelegates())
    {
        ValidData.Broadcast(Data);
    }

    if (ConfirmationType != EGameplayTargetingConfirmation::CustomMulti)
    {
        EndTask();
    }
}

/** The TargetActor we spawned locally has called back with a cancel event (they still include the 'last/best' targetdata but the consumer of this may want to discard it) */
void UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback(const FGameplayAbilityTargetDataHandle& Data)
{
    // Prediction に関する処理につき省略

    Cancelled.Broadcast(Data);
    EndTask();
}

現状 Prediction に関して全く追いつけていないので処理を一部省略しています。

コードを見てわかる通り、ValidData, Cancelleddelegate はそれぞれのコールバックで呼び出されていることが確認できました。

ConfirmationType の CustomMulti というのは、まだ全くわかりませんがこの時点で EndTask にしないというのは気になります。

まとめ

WaitTargetData の表層だけではありますが、ここまでの WaitOverlapSpawnActor の AbilityTask と比較しつつ見ていきました。

一方で、 AGameplayAbilityTargetActor でのデータを探す処理が面白い部分だと思うので引き続き見ていきたいと思います。

加えて、Predition やネットワークのレプリケーション部分も大きく省いて見たので、その辺も見ていければと思います。