Simple Navigation Example
Let’s create a simple navigation function that allows an AI agent to move the player to specific coordinates. This example shows how to implement basic movement with progress tracking using the UAIBlueprintHelperLibrary build in functions.
UCLASS()
class USimpleMovement : public UFlayerFunctionLibrary
{
GENERATED_BODY()
public:
USimpleMovement()
{
REGISTER_FLAYER_FUNCTION_SHORT(
MoveToCoords,
"move_to_coords",
"Moves to the specified coordinates with a maximum duration in milliseconds"
);
}
// Structure to hold our movement state
struct FMovementContext
{
APlayerController* PlayerController = nullptr;
FVector TargetLocation = FVector::ZeroVector;
float TimeElapsed = 0.0f;
float DurationSeconds = 0.0f;
bool IsActive = false;
};
UFUNCTION()
FFlayerPromise MoveToCoords(float X, float Y, float Z, int DurationMs, UPacketLogger* Logger)
{
// Convert duration to seconds
const float DurationSeconds = UFlayerFunctionUtils::MillisecondsToSeconds(DurationMs);
const FVector TargetLocation = FVector(X, Y, Z);
// Get player controller
APlayerController* PC = UFlayerFunctionUtils::GetPlayerController(GetWorld());
if (!PC || !PC->GetPawn())
{
Logger->LogToAI(TEXT("Failed to get player references"), ELogLevel::Error);
return FFlayerPromise::CreateCompleted(false);
}
// Log the starting position
FVector StartLocation = PC->GetPawn()->GetActorLocation();
Logger->LogToAI(
FString::Printf(TEXT("Starting movement from: %s to target: %s"),
*UFlayerFunctionUtils::VectorToString(StartLocation),
*UFlayerFunctionUtils::VectorToString(TargetLocation)
)
);
// Create movement context using shared pointer
TSharedPtr<FMovementContext> Context = MakeShared<FMovementContext>();
Context->PlayerController = PC;
Context->TargetLocation = TargetLocation;
Context->DurationSeconds = DurationSeconds;
Context->IsActive = true;
// Start the movement
UAIBlueprintHelperLibrary::SimpleMoveToLocation(PC, TargetLocation);
// Create our promise
FFlayerPromise Promise = UFlayerPromiseUtils::TickingPromise(
GetWorld(),
FStandardPromiseTickDelegate::CreateLambda(
[Context](float DeltaTime, UPacketLogger* Logger) -> bool
{
// Update elapsed time
Context->TimeElapsed += DeltaTime;
// Basic validation
if (!Context->IsActive ||
!Context->PlayerController ||
!Context->PlayerController->GetPawn())
{
Logger->LogToAI(TEXT("Movement context invalid"), ELogLevel::Warning);
return true; // End movement
}
// Get current position
APawn* PlayerPawn = Context->PlayerController->GetPawn();
FVector CurrentLocation = PlayerPawn->GetActorLocation();
float DistanceToTarget = FVector::Dist(
CurrentLocation,
Context->TargetLocation
);
// Point player in movement direction
FVector MovementDirection = (
Context->TargetLocation - CurrentLocation
).GetSafeNormal();
if (!MovementDirection.IsNearlyZero())
{
FRotator NewRotation = MovementDirection.Rotation();
Context->PlayerController->SetControlRotation(NewRotation);
}
// Check if we should end movement
const float ArrivingDistance = 100.0f; // Units
bool ReachedTarget = DistanceToTarget < ArrivingDistance;
if (ReachedTarget)
{
// Stop movement
UAIBlueprintHelperLibrary::SimpleMoveToLocation(
Context->PlayerController,
PlayerPawn->GetActorLocation()
);
// Log completion
Logger->LogToAI(
FString::Printf(
TEXT("Reached target after %.1f seconds"),
Context->TimeElapsed
),
ELogLevel::Info
);
return true; // End movement with success
}
return false; // Continue movement
}
),
Logger,
FDynamicParams(), // Default result
DurationSeconds, // Maximum time to allow
0.1f // Check progress every 0.1 seconds
);
// Handle successful completion
Promise.OnCompleted().AddLambda([Context, Logger](const FDynamicParams& Result)
{
// Mark as inactive
Context->IsActive = false;
});
// Handle cancellation (timeout)
Promise.OnCancelled().AddLambda([Context, Logger]()
{
// Log timeout
Logger->LogToAI(
FString::Printf(
TEXT("Movement timed out after %.1f seconds"),
Context->TimeElapsed
),
ELogLevel::Warning
);
// Stop movement
if (Context->PlayerController && Context->PlayerController->IsValidLowLevel())
{
APawn* PlayerPawn = Context->PlayerController->GetPawn();
if (PlayerPawn)
{
UAIBlueprintHelperLibrary::SimpleMoveToLocation(
Context->PlayerController,
PlayerPawn->GetActorLocation()
);
}
}
// Mark as inactive
Context->IsActive = false;
});
return Promise;
}
};Code Explanation
Check out the sections below for a breakdown of the code:
The FMovementContext structure holds the state of our movement, including the player controller, target location, elapsed time, and whether the movement is active.
We use it to pass data between ticks so we can track the progress of the movement and check for completion.
struct FMovementContext
{
APlayerController* PlayerController; // Who we're moving
FVector TargetLocation; // Where we're going
float TimeElapsed; // How long we've been moving
float DurationSeconds; // Maximum time allowed
bool IsActive; // Is movement in progress
};We initialize the movement using the SimpleMoveToLocation function from UAIBlueprintHelperLibrary, which handles the actual movement logic. The UpdateMovement function is called every tick to check the progress of the movement.
bool UpdateMovement(float DeltaTime, UPacketLogger* Logger)
{
// Update time
TimeElapsed += DeltaTime;
// Check distance to target
float Distance = FVector::Dist(CurrentLocation, TargetLocation);
// Point in movement direction
FRotator Rotation = MovementDirection.Rotation();
PlayerController->SetControlRotation(Rotation);
// End when we arrive or timeout
return Distance < ArrivingDistance || TimeElapsed > MaxTime;
}We periodically check and log the distance to the target so we can complete the promise when we get close enough.
We added error handling to ensure that the player controller and pawn are valid before starting the movement. If they are not, we log an error message and return a completed promise.
This good practice to ensure that the function doesn’t crash if the player controller or pawn is not found.
if (!PlayerController || !PlayerPawn)
{
Logger->LogToAI(TEXT("Invalid player state"), ELogLevel::Error);
return FFlayerPromise::CreateCompleted(false);
}