Examples

Custom executor

Build a UMoveToLocationExecutor from empty file to working. The most common 'how do I extend the framework?' walkthrough.

[ Example ]

Custom executor — write a MoveTo action

The bundled executors (Wait, Move, PlayMontage, Notify…) cover the common cases. The next most-common question is: "how do I add my own action that does X?" This page walks through writing a UMoveToLocationExecutor from empty file to plugged-into-an-archetype. The same pattern applies to any custom executor: attack, fire weapon, talk to NPC, custom animation, Gameplay Event.

DIFFICULTY ▮▮▯ INTERMEDIATESHIPS AS WALKTHROUGH (C++)BUILD ~20 MIN

The contract

A UIntentActionExecutor subclass implements three lifecycle methods. The executor reports completion by calling FinishAction(Result) where Result ∈ {Succeeded, Failed, Aborted}.

MethodCalled byWhen
EnterAction(OwnerActor, Params)DispatcherOnce, when action starts
TickAction(DeltaTime)DispatcherEvery frame while running
ExitAction(Status)DispatcherOnce, on completion

The header (UMoveToLocationExecutor.h)

#pragma once
#include "CoreMinimal.h"
#include "Action/IntentActionExecutor.h"
#include "UMoveToLocationExecutor.generated.h"

USTRUCT(BlueprintType)
struct FMoveToLocationParams
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite,
        meta=(ToolTip="World location to move toward."))
    FVector TargetLocation = FVector::ZeroVector;

    UPROPERTY(EditAnywhere, BlueprintReadWrite,
        meta=(ClampMin="1.0", ToolTip="How close is close enough (units)."))
    float AcceptanceRadius = 50.f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite,
        meta=(ClampMin="0.0", ToolTip="Optional time-out before failing."))
    float TimeoutSeconds = 0.f;
};

UCLASS(BlueprintType)
class MYPROJECT_API UMoveToLocationExecutor : public UIntentActionExecutor
{
    GENERATED_BODY()
public:
    virtual void EnterAction(AActor* OwnerActor, const FInstancedStruct& Params) override;
    virtual void TickAction(float DeltaTime) override;
    virtual void ExitAction(EIntentActionResult Status) override;

private:
    FVector Target = FVector::ZeroVector;
    float AcceptanceRadius = 50.f;
    float Timeout = 0.f;
    float Elapsed = 0.f;
    TWeakObjectPtr<AAIController> AIController;
};

The implementation (UMoveToLocationExecutor.cpp)

#include "UMoveToLocationExecutor.h"
#include "AIController.h"
#include "GameFramework/Pawn.h"
#include "Navigation/PathFollowingComponent.h"

void UMoveToLocationExecutor::EnterAction(AActor* OwnerActor, const FInstancedStruct& Params)
{
    const FMoveToLocationParams& P = Params.Get<FMoveToLocationParams>();
    Target = P.TargetLocation;
    AcceptanceRadius = P.AcceptanceRadius;
    Timeout = P.TimeoutSeconds;
    Elapsed = 0.f;

    if (APawn* Pawn = Cast<APawn>(OwnerActor))
    {
        AIController = Cast<AAIController>(Pawn->GetController());
        if (AIController.IsValid())
        {
            AIController->MoveToLocation(Target, AcceptanceRadius);
        }
    }
}

void UMoveToLocationExecutor::TickAction(float DeltaTime)
{
    Elapsed += DeltaTime;

    if (!AIController.IsValid())
    {
        FinishAction(EIntentActionResult::Failed);
        return;
    }

    if (Timeout > 0.f && Elapsed >= Timeout)
    {
        AIController->StopMovement();
        FinishAction(EIntentActionResult::Failed);
        return;
    }

    const EPathFollowingStatus::Type Status = AIController->GetMoveStatus();
    if (Status == EPathFollowingStatus::Idle)
    {
        const FVector OwnerLoc = AIController->GetPawn()->GetActorLocation();
        if (FVector::Dist(OwnerLoc, Target) <= AcceptanceRadius)
        {
            FinishAction(EIntentActionResult::Succeeded);
        }
        else
        {
            FinishAction(EIntentActionResult::Failed);
        }
    }
}

void UMoveToLocationExecutor::ExitAction(EIntentActionResult Status)
{
    if (AIController.IsValid())
    {
        AIController->StopMovement();
    }
}

That's the entire executor. ~80 lines of C++.

Wiring it to an action asset

Create an IntentAction data asset (e.g. DA_Action_MoveToFlank).
Set Executor Class to UMoveToLocationExecutor in the class picker.
Set Default Params type to FMoveToLocationParams.
Fill in TargetLocation (probably set at runtime by goal-spawning logic; the default is a placeholder).
Set the action's Effects to whatever the move accomplishes (e.g. OnFlank = true).
Set Cost Strategy as appropriate.

The action is now usable by any archetype that includes it.

Common variations

◢ T+

Acceptance test

  1. Spawn

    Pawn with archetype using DA_Action_MoveToFlank dropped in a level.

  2. Plan

    A goal whose plan includes MoveToFlank becomes active.

  3. Move

    Pawn pathfinds to TargetLocation using its AIController.

  4. Arrived

    Within AcceptanceRadius the Live Inspector shows the action completing with Succeeded and the next action (if any) starting.

  5. Timeout

    If Timeout elapses, the action completes with Failed and the planner re-evaluates.

◢ Next

Try this next

On this page