Skip to Content

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); }
Last updated on