Gameplay Ability System コードリーディング ① WaitOverlap

環境

UE 5.0.3

なぜ WaitOverlap

「Gameplay Ability System さっぱりわからん」となった状態で AbilityTask.h に以下のような記述を見つけたからです。

We have code in K2Node_LatentAbilityCall to make using these in blueprints streamlined. The best way to become familiar with AbilityTasks is to look at existing tasks like UAbilityTask_WaitOverlap (very simple) and UAbilityTask_WaitTargetData (much more complex).

AbilityTask

UAbilityTask | Unreal Engine Documentation

上記で多くの子クラスの一覧ができます。

また AbilityTask.h の冒頭にも説明があり、ここでは以下を抜粋します。

These are the basic requirements for using an ability task:

1) Define dynamic multicast, BlueprintAssignable delegates in your AbilityTask. These are the OUTPUTs of your task. When these delegates fire, execution resumes in the calling blueprints.

2) Your inputs are defined by a static factory function which will instantiate an instance of your task. The parameters of this function define the INPUTs into your task. All the factory function should do is instantiate your task and possibly set starting parameters. It should NOT invoke any of the callback delegates!

3) Implement a Activate() function (defined here in base class). This function should actually start/execute your task logic. It is safe to invoke callback delegates here.

This is all you need for basic AbilityTasks.

CheckList:
- Override ::OnDestroy() and unregister any callbacks that the task registered. Call Super::EndTask too!
- Implemented an Activate function which truly 'starts' the task. Do not 'start' the task in your static factory function!

まずは上記を頭に入れつつ WaitOverlap を見ていきます。

WaitOverlap

UAbilityTask_WaitOverlap | Unreal Engine Documentation

Fixme: this is still incomplete and probablyh not what most games want for melee systems.
-Only actually activates on Blocking hits
-Uses first PrimitiveComponent instead of being able to specify arbitrary component.

最初にヒットした PrimitiveComponent を待つということのようです。

ヘッダファイル

大きくないので全体貼り付けます。

class GAMEPLAYABILITIES_API UAbilityTask_WaitOverlap : public UAbilityTask
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(BlueprintAssignable)
    FWaitOverlapDelegate    OnOverlap;

    UFUNCTION()
    void OnHitCallback(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

    virtual void Activate() override;

    /** Wait until an overlap occurs. This will need to be better fleshed out so we can specify game specific collision requirements */
    UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
    static UAbilityTask_WaitOverlap* WaitForOverlap(UGameplayAbility* OwningAbility);

private:

    virtual void OnDestroy(bool AbilityEnded) override;

    UPrimitiveComponent* GetComponent();
    
};

AbilityTask.h にあった内容の通りで、 BlueprintAssignabledelegate として OnOverlap があり、static ファクトリ、そして Activate も用意されています。

ソースファイル

ファクトリ

UAbilityTask_WaitOverlap* UAbilityTask_WaitOverlap::WaitForOverlap(UGameplayAbility* OwningAbility)
{
    UAbilityTask_WaitOverlap* MyObj = NewAbilityTask<UAbilityTask_WaitOverlap>(OwningAbility);
    return MyObj;
}

NewAbilityTask は継承元の AbliityTask で定義されているヘルパー関数です。小さいので貼っておきます。

/** 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;
}

InitTask やらは GameplayTask のものです。

Activate

このタスクの実行を行うものです。

void UAbilityTask_WaitOverlap::Activate()
{
    SetWaitingOnAvatar();

    UPrimitiveComponent* PrimComponent = GetComponent();
    if (PrimComponent)
    {
        PrimComponent->OnComponentHit.AddDynamic(this, &UAbilityTask_WaitOverlap::OnHitCallback);
    }
}

まず SetWaitingOnAvatar ですが、これは Avator (Character や Pawn や Actor) の挙動に対する待ち状態 にするというものです。一応この実装だけ貼っておきます。

// AbilityTask.cpp
void UAbilityTask::SetWaitingOnAvatar()
{
    if (IsValid(Ability) && AbilitySystemComponent)
    {
        WaitStateBitMask |= (uint8)EAbilityTaskWaitState::WaitingOnAvatar;
        Ability->NotifyAbilityTaskWaitingOnAvatar(this);
    }
}

Ability はこのタスクを生成した GameplayAbility の参照です。このタスクを生成した時にセットされています。

続いて PrimitiveComponent の取得部分である GetComponent を見ていきます。

UPrimitiveComponent* UAbilityTask_WaitOverlap::GetComponent()
{
    // TEMP - we are just using root component's collision. A real system will need more data to specify which component to use
    UPrimitiveComponent * PrimComponent = nullptr;
    AActor* ActorOwner = GetAvatarActor();
    if (ActorOwner)
    {
        PrimComponent = Cast<UPrimitiveComponent>(ActorOwner->GetRootComponent());
        if (!PrimComponent)
        {
            PrimComponent = ActorOwner->FindComponentByClass<UPrimitiveComponent>();
        }
    }

    return PrimComponent;
}

GetAvatarActor の詳細は省きますが、仮に Character に Ability を付与したのであれば、その Character がが ActorOwner として取得できます。

その場合 RootComponent はおそらく Capsule Component のようなものが取得できているはずです。

最後に

PrimComponent->OnComponentHit.AddDynamic(this, &UAbilityTask_WaitOverlap::OnHitCallback);

でこの AbilityTask の OnHitCallbackOnCompoentHit に渡して Activate は終了です。たしかに WaitOverlap としての起動は十分のように見えます。

「AbilityTask としての待ち状態に移行」し、「PrimitiveComponent の OnHit を待つ」。

おそらく、OnHit が発生したところで、用意された delegate が呼び出されると想像しつつ見ていきます。

OnHitCallback

void UAbilityTask_WaitOverlap::OnHitCallback(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
    if(OtherActor)
    {
        // Construct TargetData
        FGameplayAbilityTargetData_SingleTargetHit * TargetData = new FGameplayAbilityTargetData_SingleTargetHit(Hit);

        // Give it a handle and return
        FGameplayAbilityTargetDataHandle    Handle;
        Handle.Data.Add(TSharedPtr<FGameplayAbilityTargetData>(TargetData));
        if (ShouldBroadcastAbilityTaskDelegates())
        {
            OnOverlap.Broadcast(Handle);
        }

        // We are done. Kill us so we don't keep getting broadcast messages
        EndTask();
    }
}

FGameplayAbilityTargetData_SingleTargetHit 構造体は HitResult の wrap をしているようです。wrap された結果として一つのヒット対象のみ取得されるようになっています。

ShouldBroadcastAbilityTaskDelegates は、この AbilityTask の生成元の Ability がこの時点でも Active 状態かを確認しています。
そして OnOverlap.Broadcast(Handle); でようやく、delegate が利用されていることを確認できました 🎉

最後に EndTask ですが、これは Checklist の Override ::OnDestroy() and unregister any callbacks that the task registered. Call Super::EndTask too! の記述にあったものです。継承元の GameplayTask に定義されています。

void UGameplayTask::EndTask()
{
    UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Verbose
        , TEXT("%s EndTask called, current State: %s")
        , *GetName(), *GetTaskStateName());

    if (TaskState != EGameplayTaskState::Finished)
    {
        if (IsValid(this))
        {
            OnDestroy(false);
        }
        else
        {
            // mark as finished, just to be on the safe side 
            TaskState = EGameplayTaskState::Finished;
        }
    }
}

OnDestroy が呼び出されているので、ここで UAbilityTask_WaitOverlapOnDestroy が呼び出されます。ではこの流れで OnDestroy を見ていきます。

OnDestroy

void UAbilityTask_WaitOverlap::OnDestroy(bool AbilityEnded)
{
    UPrimitiveComponent* PrimComponent = GetComponent();
    if (PrimComponent)
    {
        PrimComponent->OnComponentHit.RemoveDynamic(this, &UAbilityTask_WaitOverlap::OnHitCallback);
    }

    Super::OnDestroy(AbilityEnded);
}

Checklist にあったように、callback 登録の削除が行われています。

ここまでで一通り AbilityTask_WaitOverlap の実装を眺めることができたとともに、AbilityTask.h の冒頭にあった「These are the basic requirements for using an ability task」が満たされていることが確認できました。

次回

UAbilityTask_WaitOverlap が very simple に対して、much more complex となっていた UAbilityTask_WaitOverlap を見ていこうと思います。