6
4

More than 1 year has passed since last update.

[UE] AbilityTask のつくりかた

Posted at

はじめに

前回の記事ではプラグイン内であらかじめ用意されている AbilityTask について紹介しました。しかしゲームを作っているとアレコレ欲しくなってくるもの。今回は AbilityTask の作成についてまとめます。

AbilityTask を使用するための基本的な要件は次の通りです

AbilityTask.h にタスクを作成するための手引きが書いてあります。
GA_Document-EventGraph_63.png
ドキュメントになぞって Montage から MontageNotify を受け取るタスク WaitMontageNotify を実装してみましょう。

MontageNotify とは

Montage に埋め込むことができる AnimNotify です。
3d28aaa6ceaa22bddcb2347aaee45ab1.png
MontageNotify(上)と MontageNotifyWindow(下)があり、NotifyName で通知されます。Notify 自体に中身の実装は何もなく、ただ通知するだけの AnimNotify。
GA_Document-EventGraph_56.png
PlayMontage ノード(PlayMontageCallbackProxy)でも実装されてますね。アニメーション内のタイミング通知だけ受け取って処理は BP で書く、みたいな実装に便利です。AnimNotify では Begin のみ、AnimNotifyState では Begin/End にそれぞれ通知されます。

受け取るイベントを定義する

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.

AbilityTask で動的マルチキャスト(BlueprintAssignable デリゲート)を定義します。これらはタスクの出力です。これらのデリゲートが起動すると、呼び出し元のブループリントで実行が再開されます。

AbilityTask_WaitMontageNotify.h
UCLASS()
class MYPROJECT_API UAbilityTask_WaitMontageNotify : public UAbilityTask
{
    GENERATED_BODY()

    // 通知名、発生時間、NotifyState(Begin-End)の区間時間
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
        FNotifyDelegate, FName, NotifyName, float, TriggerTime, float, Duration );

    UPROPERTY( BlueprintAssignable )
    FNotifyDelegate OnNotifyBegin;

    UPROPERTY( BlueprintAssignable )
    FNotifyDelegate OnNotifyEnd;

ファクトリ静的関数を定義する

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!

入力は、タスクをインスタンス化するファクトリ静的関数によって定義されます。この関数のパラメーターはタスクへの入力を定義します。ファクトリ関数が実行するのはタスクをインスタンス化し、場合によっては開始パラメータを設定することだけです。コールバックデリゲートを呼び出さないでください!

AbilityTask_WaitMontageNotify.h
    UFUNCTION( BlueprintCallable, Category = "Ability|Tasks",
        meta = ( HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true" ) )
    static UAbilityTask_WaitMontageNotify* WaitMontageNotify( UGameplayAbility* OwningAbility, UAnimMontage* MontageToWait );
AbilityTask_WaitMontageNotify.cpp
UAbilityTask_WaitMontageNotify* UAbilityTask_WaitMontageNotify::WaitMontageNotify(
    UGameplayAbility* OwningAbility, UAnimMontage* MontageToWait )
{
    auto* MyObj = NewAbilityTask<ThisClass>( OwningAbility ); // ThisClassはクラス名のエイリアス
    MyObj->MontageToWait = MontageToWait;
    return MyObj;
}

そのままノードのインターフェースとなる関数。インスタンスを作ってパラメータのセットするだけ、それ以上のことは次の Activate に記述する。

Activate 関数を実装する

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.

Activate 関数を実装します(基本クラスで定義されています)。この関数は、実際にタスクロジックを開始/実行する必要があります。ここでコールバックデリゲートを呼び出すのは安全です。

AbilityTask_WaitMontageNotify.h
protected:
    virtual void Activate() override;
    virtual void OnDestroy( bool AbilityEnded ) override;

private:
    class UAnimInstance* GetAnimInstance() const;

    bool IsNotifyValid( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload ) const;

    UFUNCTION()
    void OnNotifyBeginReceived( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload );

    UFUNCTION()
    void OnNotifyEndReceived( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload );

private:
    int32 MontageInstanceID = INDEX_NONE;

    UPROPERTY()
    UAnimMontage* MontageToWait;
};
AbilityTask_WaitMontageNotify.cpp
void UAbilityTask_WaitMontageNotify::Activate()
{
    // 既に再生済みのMontageに対して通知の登録を行う
    UAnimInstance* AnimInstance = GetAnimInstance();
    if ( FAnimMontageInstance* MontageInstance =
             AnimInstance ? AnimInstance->GetActiveInstanceForMontage( MontageToWait ) : nullptr )
    {
        // AnimInstanceのデリゲートでは全てのMontage通知が行われるため、識別するためにIDを取得
        MontageInstanceID = MontageInstance->GetInstanceID();

        AnimInstance->OnPlayMontageNotifyBegin.AddDynamic( this, &ThisClass::OnNotifyBeginReceived );
        AnimInstance->OnPlayMontageNotifyEnd.AddDynamic( this, &ThisClass::OnNotifyEndReceived );
    }
    else
    {
        // Montageが再生中ではないときタスクを終了する
        // 補足:EndAbilityでも親Abilityが発行した全ての有効タスクに対して終了処理が行われる
        EndTask();
    }
}

void UAbilityTask_WaitMontageNotify::OnDestroy( bool AbilityEnded )
{
    UAnimInstance* AnimInstance = GetAnimInstance();
    if ( AnimInstance )
    {
        // Activateでバインドしたデリゲートを解除する
        AnimInstance->OnPlayMontageNotifyBegin.RemoveDynamic( this, &ThisClass::OnNotifyBeginReceived );
        AnimInstance->OnPlayMontageNotifyEnd.RemoveDynamic( this, &ThisClass::OnNotifyEndReceived );
    }

    Super::OnDestroy( AbilityEnded );
}

UAnimInstance* UAbilityTask_WaitMontageNotify::GetAnimInstance() const
{
    if ( Ability )
    {
        if ( const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo() )
        {
            return ActorInfo->GetAnimInstance();
        }
    }
    return nullptr;
}

bool UAbilityTask_WaitMontageNotify::IsNotifyValid( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload ) const
{
    return MontageInstanceID != INDEX_NONE && BPNPayload.MontageInstanceID == MontageInstanceID;
}

void UAbilityTask_WaitMontageNotify::OnNotifyBeginReceived( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload )
{
    if ( IsNotifyValid( NotifyName, BPNPayload ) && ShouldBroadcastAbilityTaskDelegates() )
    {
        float TriggerTime = BPNPayload.NotifyEvent ? BPNPayload.NotifyEvent->GetTriggerTime() : 0.f;
        float Duration = BPNPayload.NotifyEvent ? BPNPayload.NotifyEvent->GetDuration() : 0.f;
        OnNotifyBegin.Broadcast( NotifyName, TriggerTime, Duration );
    }
}

void UAbilityTask_WaitMontageNotify::OnNotifyEndReceived( FName NotifyName, const FBranchingPointNotifyPayload& BPNPayload )
{
    if ( IsNotifyValid( NotifyName, BPNPayload ) && ShouldBroadcastAbilityTaskDelegates() )
    {
        float TriggerTime = BPNPayload.NotifyEvent ? BPNPayload.NotifyEvent->GetTriggerTime() : 0.f;
        float Duration = BPNPayload.NotifyEvent ? BPNPayload.NotifyEvent->GetDuration() : 0.f;
        OnNotifyEnd.Broadcast( NotifyName, TriggerTime, Duration );
    }
}

カスタムパラメータでアクターをスポーンする

GA_Document-EventGraph_64.png
Actor クラスを指定すると自動的にピンが変化するノードを作成することができます。
これも AbilityTask.h のコメントから。

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

Activate 関数の代わりに、BeginSpawningActor および FinishSpawningActor 関数を実装する必要があります。

BeginSpawningActor() must take in a TSubclassOf parameters named 'Class'. It must also have a out reference parameters of type YourActorClassToSpawn*& named SpawnedActor. This function is allowed to decide whether it wants to spawn the actor or not (useful if wishing to predicate actor spawning on network authority).

BeginSpawningActor は、TSubclassOf 型で「Class」という名前の引数を定義する必要があります。また、[YourActorClassToSpawn]*& 型の SpawnedActor という名前で外部参照(出力用)パラメータが必要です。この関数は、アクターをスポーンするかどうかを決定できます(ネットワーク権限でのアクターのスポーンを予測する場合に便利です)。

BeginSpawningActor() can instantiate an actor with SpawnActorDeferred. This is important, otherwise the UCS will run before spawn parameters are set.
BeginSpawningActor() should also set the SpawnedActor parameter to the actor it spawned.

BeginSpawningActor は、SpawnActorDeferred を使用してアクターをインスタンス化できます。これは重要で、そうしないとスポーンパラメータが設定される前に UCS(UserConstructionScript)が実行されます。
BeginSpawningActor は、スポーンしたアクターを SpawnedActor に代入する必要もあります。

[Next, the generated byte code will set the expose on spawn parameters to whatever the user has set]

次に、生成されたバイトコードは、「ExposeOnSpawn」でマークされたパラメータをユーザー任意に設定します。

If you spawned something, FinishSpawningActor() will be called and pass in the same actor that was just spawned. You MUST call ExecuteConstruction + PostActorConstruction on this actor!

何かをスポーンした場合、それを引数にとり FinishSpawningActor が呼び出されます。このアクターは ExecuteConstruction + PostActorConstruction を呼び出す必要があります!

This is a lot of steps but in general, AbilityTask_SpawnActor() gives a clear, minimal example.

これは多くの手順となりましたが、AbilityTask_SpawnActor は明確で最小限な汎用例となります。

ExecuteConstruction + PostActorConstruction とあるが、AActor::FinishSpawning のことである

なぜ静的関数と Activate の定義だけで成立するのか

コールスタックを見ると、AbilityTask の起動エントリは UGameplayTask::ReadyForActivation となっており、Blueprint から呼び出されているようです。以下ファイルで Blueprint のコンパイルが実装されています。
Engine\Source\Editor\GameplayTasksEditor\Private\K2Node_LatentGameplayTaskCall.cpp

BeginSpawningActor だけでなく BeginSpawningActorArray(複数のスポーン)もサポートしているっぽい

クラス内の BlueprintCallable 関数

GA_Document-EventGraph_59.png
UCLASS の meta に埋め込まれた ExposedAsyncProxy(GameplayTask.h を参照)により、インスタンスが AsyncTask として取得できます。 BlueprintCallable 関数を実装することでタスクの関数を作成、呼び出すことが可能です。UGameplayTask::EndTask は外部からタスクを終了させることができます。

Tick 処理を作成する

bTickingTask を true にすることで、UAbilityTask::TickTask が走るようになります。基本的にはデリゲートコールバックによる処理となりますが、Tick の方が都合よい時に使いましょう。

GameplayAbility で Tick を処理する

GameplayAbility 自体は Tick を持ちませんが、AbilityTask により Tick 動作が可能です。
GA_Document-EventGraph_61.png

AbilityTask_Tick.cpp
void UAbilityTask_Tick::TickTask( float DeltaTime )
{
    if ( ShouldBroadcastAbilityTaskDelegates() )
    {
        Tick.Broadcast( DeltaTime );
    }
}
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4