Gameplay Ability System コードリーディング ④ WaitTargetData - GameplayAbilityTargetActor (及び Radius)

上記の続きになります。

WaitTargetData の表層部分だけを前回ざっと眺めました。

WaitTargetData は GameplayAbilityTargetActor で挙動が変わる

上記は左が AGameplayAbilityTargetActor_Radius を指定し、右が AGameplayAbilityTargetActor_SingleLineTrace を指定したものですが、指定した GameplayAbilityTargetActor によって入力ピンも大きく変化しているのが分かります。

SpawnActor のところでも貼りましたが 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.

つまり、WaitTargetData のノードにそこで指定した GameplayAbilityTargetActor のプロパティが出ている (ExposeOnSpawn) のは、これに則って実装しているからということだと分かります。

よって、例えば自身で spawn する状況があるとすれば以下の AGameplayAbilityTargetActor_Radius の例のようになり、プロパティは同じことが分かります。 (Transform に関しては、SpawnActorDeferred で指定されているため、ノードには出ていません)

GASDocumentation の説明

4.11.2 Target Actors | GASDocumentation/README.jp.md at lang-ja · sentyaanko/GASDocumentation

恣意的にはなってしまいますが、読み進めるヒントになりそうな分かりやすい部分だけ引用します。

GameplayAbilities は、 WaitTargetData AbilityTask を伴って TargetActors を視覚化のためにスポーンし、ワールドからのターゲット情報をキャプチャします。

~略

TargetActorsAActor に基づいており、ターゲットとする 場所 と 方法 を示すために、あらゆる種類の可視コンポーネントを持つことができます(スタティックメッシュやデカールのような)。

~略

これらは、「基本のトレースまたはオーバーラップを使いターゲットの情報をキャプチャ」し、「 TargetActor の実装に基づいて、結果を FHitResults または AActor の配列として TargetData に変換」します。

TargetActors

AGameplayAbilityTargetActor をスポーンしているのは、すでに WaitTargetData を読み進めた部分で確認できています。

以下は GameplayAbilityTargetActor.h の class 定義部分のみですが、たしかに AActor に基づいています。

UCLASS(Blueprintable, abstract, notplaceable)
class GAMEPLAYABILITIES_API AGameplayAbilityTargetActor : public AActor

AGameplayAbilityTargetActor は abstract なので、ここからは一つ TargetActor を選んで見ていきたいと思います。

AGameplayAbilityTargetActor_Radius

Built-in の GameplayAbilityTargetActor のうち、Radius は特に他との継承関係もないため読みやすいだろうと判断してまずはこれを見てみます。

ヘッダファイル

UCLASS(Blueprintable, notplaceable)
class GAMEPLAYABILITIES_API AGameplayAbilityTargetActor_Radius : public AGameplayAbilityTargetActor
{
    GENERATED_UCLASS_BODY()

public:

    virtual void StartTargeting(UGameplayAbility* Ability) override;
    
    virtual void ConfirmTargetingAndContinue() override;

    /** Radius of target acquisition around the ability's start location. */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Radius)
    float Radius;

protected:

    TArray<TWeakObjectPtr<AActor> > PerformOverlap(const FVector& Origin);

    FGameplayAbilityTargetDataHandle MakeTargetData(const TArray<TWeakObjectPtr<AActor>>& Actors, const FVector& Origin) const;
};

まず目に入るのが Radius プロパティです。 ExposeOnSpawn = true となっており、実際にノードにも表出しています。

一方で、それ以外には ExposeOnSpawn = true のプロパティはありません、つまり他は基底クラスである AGameplayAbilityTargetActor にあるということです。

AGameplayAbilityTargetActor のヘッダファイル

class GAMEPLAYABILITIES_API AGameplayAbilityTargetActor : public AActor
{
public:

    /** Describes where the targeting action starts, usually the player character or a socket on the player character. */
    //UPROPERTY(BlueprintReadOnly, meta=(ExposeOnSpawn=true), Category=Targeting)
    UPROPERTY(BlueprintReadOnly, meta = (ExposeOnSpawn = true), Replicated, Category = Targeting)
    FGameplayAbilityTargetingLocationInfo StartLocation;

    /** Parameters for world reticle. Usage of these parameters is dependent on the reticle. */
    UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
    FWorldReticleParameters ReticleParams;

    /** Reticle that will appear on top of acquired targets. Reticles will be spawned/despawned as targets are acquired/lost. */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (ExposeOnSpawn = true), Category = Targeting)
    TSubclassOf<AGameplayAbilityWorldReticle> ReticleClass;       //Using a special class for replication purposes.

    UPROPERTY(BlueprintReadWrite, Replicated, meta = (ExposeOnSpawn = true), Category = Targeting)
    FGameplayTargetDataFilterHandle Filter;

    /** Draw the debug information (if applicable) for this targeting actor. */
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Replicated, meta = (ExposeOnSpawn = true), Category = Targeting)
    bool bDebug;

上記に ExposeOnSpawn = true のものだけ貼りましたが、これでノードに出ているプロパティは全て揃っています。

ここでは個々の詳細には触れませんが、GASDocumentation に解説があります。

ソースファイル

初期化

AGameplayAbilityTargetActor_Radius::AGameplayAbilityTargetActor_Radius(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickGroup = TG_PrePhysics;
    ShouldProduceTargetDataOnServer = true;
}

初期化部分です。アクターとして普通に spawn され Tick も有効にされるようです。

ShouldProduceTargetDataOnServer に関してですが、基底クラスの方に定義があります。

/** The TargetData this class produces can be entirely generated on the server. We don't require the client to send us full or partial TargetData (possibly just a 'confirm') */
UPROPERTY(EditAnywhere, Category=Advanced)
bool ShouldProduceTargetDataOnServer;

少し長いですが GASDocumentation の方に解説があるので引用します。

TargetActors はデフォルトではレプリケーションされません : しかしながら、ローカルプレイヤーがどこをターゲティングしているかを他のプレイヤーが見えるのがゲームとして理にかなっているのであれば、レプリケーションを作るようにすることもできます。 それらには WaitTargetData AbilityTask 上の RPCs を介してサーバーと通信するためのデフォルトの機能が含まれています。 TargetActorShouldProduceTargetDataOnServer プロパティが false に設定されると、クライアントは UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()CallServerSetReplicatedTargetData() を介した確認時に TargetData をサーバーに RPC します。 ShouldProduceTargetDataOnServertrue ならば、クライアントは汎用確認イベント EAbilityGenericReplicatedEvent::GenericConfirm を送り、 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback() でサーバーに RPC し、サーバーは受信した RPC の上で、サーバー上でデータを生成するためにトレースまたはオーバーラップのチェックを行います。 クライアントがターゲティングをキャンセルした場合、汎用的なキャンセルイベント EAbilityGenericReplicatedEvent::GenericCancel を送り、 UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback でサーバーに RPC します。

雑な解釈になってしまいますが ShouldProduceTargetDataOnServer は「サーバー側で TargetData を生成するべきか」というフラグであり、 true ならばサーバーには「サーバー側で改めてトレースなりして生成しろ」、false の場合は、「クライアントで生成した TargetData を送るね」ということになるのだと思います。

(利用している callback が異なっているようなので、また後ほど詳しく見てみます 🙏)

そして、ここでは true が設定されているということです。

AGameplayAbilityTargetActor::AGameplayAbilityTargetActor(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    ShouldProduceTargetDataOnServer = false;
    bDebug = false;
    bDestroyOnConfirmation = true;
}

ちなみに、基底クラス側では false が指定されているためデフォルトでは false のようです。

StartTargeting

AbilityTaskWaitTargetData の方で、この TargetActor の Finalize 処理で呼び出されていた関数です。

void AGameplayAbilityTargetActor_Radius::StartTargeting(UGameplayAbility* InAbility)
{
    Super::StartTargeting(InAbility);
    SourceActor = InAbility->GetCurrentActorInfo()->AvatarActor.Get();
}

// 基底クラス側
void AGameplayAbilityTargetActor::StartTargeting(UGameplayAbility* Ability)
{
    OwningAbility = Ability;
}

特に特筆する点はありません。以降で使われる変数に値を設定しています。

ConfirmTargetingAndContinue

この AGameplayAbilityTargetActor_Radius はこの ConfirmTargetingAndContinue が実質の処理になります。

初期化で Tick は有効になっていますが、Tick の実装はなく、この ConfirmTargetingAndContinue が実行された時点で終了します。

この関数が呼び出されるタイミングはいくつかありますが、ここで触れると長くなるのでひとまず以下のドキュメントを貼るに留めます。EGameplayTargetingConfirmation::Type の説明の表で確認できます。

4.11.2 Target Actors | GASDocumentation/README.jp.md at lang-ja · sentyaanko/GASDocumentation

void AGameplayAbilityTargetActor_Radius::ConfirmTargetingAndContinue()
{
    check(ShouldProduceTargetData());
    if (SourceActor)
    {
        FVector Origin = StartLocation.GetTargetingTransform().GetLocation();
        FGameplayAbilityTargetDataHandle Handle = MakeTargetData(PerformOverlap(Origin), Origin);
        TargetDataReadyDelegate.Broadcast(Handle);
    }
}

まず、if (SourceActor) に関してですが、これは StartTargeting で設定済みのため、通常は真になると考えられます。

つまり、Confirm が行われれば、必ず if の内部に入るということです。

以降は、入力ピンになっている StartLocation の Location を Origin として、 TargetDataHandle を生成し、TargetDataReadyDelegate delegate に流しているようです。

TargetDataReadyDelegateAbilityTask_WaitTargetData#InitializeTargetActorAbilityTask_WaitTargetData#OnTargetDataReadyCallback に紐付けられているので、ここでの delegate はそのまま WaitTargetDataValidData (出力ピン) の delegate に繋がります。

よって、この Radius タスクは、「WaitTargetData の実行後、 Confirm を待機 (Wait) し、Confirm された時点で StartLocation に基づいてTargetData データを即生成し渡す」という形になります。

MakeTargetData / PerformOverlap

TargetDataHandle を生成している部分を見ていきます。

FGameplayAbilityTargetDataHandle AGameplayAbilityTargetActor_Radius::MakeTargetData(const TArray<TWeakObjectPtr<AActor>>& Actors, const FVector& Origin) const
{
    if (OwningAbility)
    {
        /** Use the source location instead of the literal origin */
        return StartLocation.MakeTargetDataHandleFromActors(Actors, false);
    }

    return FGameplayAbilityTargetDataHandle();
}

TArray<TWeakObjectPtr<AActor> > AGameplayAbilityTargetActor_Radius::PerformOverlap(const FVector& Origin)
{
    bool bTraceComplex = false;
    
    FCollisionQueryParams Params(SCENE_QUERY_STAT(RadiusTargetingOverlap), bTraceComplex);
    Params.bReturnPhysicalMaterial = false;

    TArray<FOverlapResult> Overlaps;

    SourceActor->GetWorld()->OverlapMultiByObjectType(Overlaps, Origin, FQuat::Identity, FCollisionObjectQueryParams(ECC_Pawn), FCollisionShape::MakeSphere(Radius), Params);

    TArray<TWeakObjectPtr<AActor>>  HitActors;

    for (int32 i = 0; i < Overlaps.Num(); ++i)
    {
        //Should this check to see if these pawns are in the AimTarget list?
        APawn* PawnActor = Overlaps[i].OverlapObjectHandle.FetchActor<APawn>();
        if (PawnActor && !HitActors.Contains(PawnActor) && Filter.FilterPassesForActor(PawnActor))
        {
            HitActors.Add(PawnActor);
        }
    }

    return HitActors;
}

まず PerformOverlap ですが、OverlapMultiByObjectTypeOrigin (実際には StartLocation ) を Center にして 入力値の Radius (半径) を指定した Sphere Collision で Pawn を取得しています。

その後は Filter に pass したもののみ HitActors に追加し、そのまま return しています。

なので、そのまま空の配列が return されることもありえるようです。

続いて MakeTargetData ですが、入力の StartLocation から TargetDataHandle を生成し、その引数に PerformOverlap で得た配列を渡しています。 (StartLocation から TargetDataHandle を生成しているのはパッとはピンとこないのですが、中を見ると SourceLocation として Handle の方に渡しているようです。)

まとめ

AGameplayAbilityTargetActorWaitTargetData の関係、そして簡単そうな例として AGameplayAbilityTargetActor_Radius をざっと眺めました。

どうやら Radius は Confirm を Wait するサンプルとして参考になりそうですが、肝心の Confirm 部分を飛ばした上に、実際の挙動が微妙に想像しにくいです。

次回は Confirm 部分を確認しつつ、実際にこの Radius を利用した BP を書いてみたいと思います。