Nunu SDK Utility Functions
The Nunu SDK provides three powerful utility classes to simplify writing Flayer functions. These classes are designed to enhance the functionality of your Flayer functions and make them more efficient.
All of them are available for both C++ and Blueprints.
Let’s take a closer look at each of them:
Flayer Promise Utils
These utility functions are designed to simplify the process of creating and managing promises in your Flayer functions. They provide a convenient way to handle asynchronous operations and ensure that your Flayer functions can return results in a clean and efficient manner.
For more information on what Flayer promises are, and when to use them, check out the Flayer Promises section.
Delayed Promise
The simplest form of promise - it resolves after a specified time delay. Perfect for operations that just need to wait.
FFlayerPromise DelayedPromise(
UObject* WorldContextObject,
float DelaySeconds,
const FDynamicParams& Result = FDynamicParams()
);this example demonstrates how to create a delayed promise for reloading a weapon. The function takes a reload time and a logger as arguments, and returns a promise that resolves after the specified reload time.
UFUNCTION()
FFlayerPromise ReloadWeapon(float ReloadTime, UPacketLogger* Logger)
{
// Start the reload animation
if (APlayerCharacter* Character = GetPlayerCharacter())
{
Character->PlayReloadAnimation();
Logger->LogToAI(TEXT("Starting weapon reload..."), ELogLevel::Info);
}
// Create result for when reload completes
FDynamicParams ReloadResult;
ReloadResult.Add(FDynamicParam(true)); // Success flag
// Return promise that completes after reload time
return UFlayerPromiseUtils::DelayedPromise(
GetWorld(),
ReloadTime,
ReloadResult
);
}Conditional Promise
Waits for a specific condition to be met. Useful when you need to monitor state changes but don’t need to perform actions each tick.
FFlayerPromise ConditionalPromise(
UObject* WorldContextObject,
FStandardPromiseConditionDelegate Condition,
UPacketLogger* Logger,
const FDynamicParams& Result = FDynamicParams(),
float MaxWaitTime = 0.0f,
float TickInterval = 0.1f
);Here’s an example waiting for a door to finish opening:
UFUNCTION()
FFlayerPromise WaitForDoorToOpen(ADoor* Door, UPacketLogger* Logger)
{
if (!Door)
{
Logger->LogToAI(TEXT("Invalid door reference"), ELogLevel::Error);
return FFlayerPromise::CreateCompleted(false);
}
// Start opening the door
Door->Open();
// Create condition delegate
FStandardPromiseConditionDelegate Condition;
Condition.BindLambda([Door, Logger](UPacketLogger* CondLogger) -> bool
{
if (!Door->IsValid())
{
Logger->LogToAI(TEXT("Door was destroyed while waiting"), ELogLevel::Warning);
return true; // End the promise if door is destroyed
}
float OpenAmount = Door->GetOpenAmount();
// Log progress occasionally
static float LastLoggedAmount = 0.0f;
if (FMath::Abs(OpenAmount - LastLoggedAmount) > 0.25f)
{
Logger->LogToAI(
FString::Printf(TEXT("Door is %.0f%% open"), OpenAmount * 100.0f),
ELogLevel::Info
);
LastLoggedAmount = OpenAmount;
}
return Door->IsFullyOpen();
});
// Wait for door to finish opening
return UFlayerPromiseUtils::ConditionalPromise(
GetWorld(),
Condition,
Logger,
FDynamicParams(), // Default result
5.0f, // Maximum 5 seconds wait
0.1f // Check every 0.1 seconds
);
}
Ticking Promise
The most flexible promise type. Executes code every tick until completion. Perfect for operations that need continuous updates or complex state management.
FFlayerPromise TickingPromise(
UObject* WorldContextObject,
FStandardPromiseTickDelegate OnTick,
UPacketLogger* Logger,
const FDynamicParams& Result = FDynamicParams(),
float MaxTickTime = 0.0f,
float TickInterval = 0.1f
);Here’s a practical example of following another actor while maintaining distance:
UFUNCTION()
FFlayerPromise FollowActor(AActor* TargetActor, float DesiredDistance, UPacketLogger* Logger)
{
if (!TargetActor)
{
Logger->LogToAI(TEXT("No target actor specified"), ELogLevel::Error);
return FFlayerPromise::CreateCompleted(false);
}
// Create context to hold our operation state
struct FFollowContext
{
AActor* Target;
APlayerController* Controller;
float Distance;
float TimeElapsed = 0.0f;
FVector LastTargetLocation;
};
// Initialize context
FFollowContext* Context = new FFollowContext();
Context->Target = TargetActor;
Context->Controller = UGameplayStatics::GetPlayerController(GetWorld(), 0);
Context->Distance = DesiredDistance;
Context->LastTargetLocation = TargetActor->GetActorLocation();
// Create the ticking promise
return UFlayerPromiseUtils::TickingPromise(
GetWorld(),
FStandardPromiseTickDelegate::CreateLambda(
[Context](float DeltaTime, UPacketLogger* TickLogger) -> bool
{
Context->TimeElapsed += DeltaTime;
// Validate our references
if (!Context->Controller || !Context->Target ||
!Context->Controller->GetPawn())
{
TickLogger->LogToAI(TEXT("Lost required references"), ELogLevel::Error);
delete Context;
return true; // End promise
}
// Get current positions
FVector TargetLocation = Context->Target->GetActorLocation();
FVector PlayerLocation = Context->Controller->GetPawn()->GetActorLocation();
// Calculate movement needed
float CurrentDistance = FVector::Distance(PlayerLocation, TargetLocation);
FVector DirectionToTarget = (TargetLocation - PlayerLocation).GetSafeNormal();
// Determine if target has moved significantly
bool TargetMoved = !TargetLocation.Equals(
Context->LastTargetLocation,
1.0f // Tolerance
);
// Log status periodically or when target moves
static float TimeSinceLastLog = 0.0f;
TimeSinceLastLog += DeltaTime;
if (TimeSinceLastLog >= 1.0f || TargetMoved)
{
TickLogger->LogToAI(
FString::Printf(
TEXT("Following target. Current distance: %.1f, Desired: %.1f"),
CurrentDistance,
Context->Distance
),
ELogLevel::Info
);
TimeSinceLastLog = 0.0f;
}
// Update last known position
Context->LastTargetLocation = TargetLocation;
// Calculate desired position
FVector DesiredPosition = TargetLocation -
(DirectionToTarget * Context->Distance);
// Move towards desired position
UAIBlueprintHelperLibrary::SimpleMoveToLocation(
Context->Controller,
DesiredPosition
);
// Continue until operation is cancelled
return false;
}
),
Logger,
FDynamicParams(), // Default result
0.0f, // No timeout
0.0f // Tick every frame
);
}Promise with Dynamic Result
Perfect for operations that need to generate or modify their result over time.
FFlayerPromise TickingPromiseWithResult(
UObject* WorldContextObject,
FStandardPromiseTickWithLoggerAndResultDelegate OnTickWithResult,
UPacketLogger* Logger,
const FDynamicParams& DefaultResult = FDynamicParams(),
float MaxTickTime = 0.0f,
float TickInterval = 0.1f
);Example of scanning nearby actors and returning the closest one:
UFUNCTION()
FFlayerPromise ReloadWeapon(float ReloadTime, UPacketLogger* Logger)
{
// Start the reload animation
if (APlayerCharacter* Character = GetPlayerCharacter())
{
Character->PlayReloadAnimation();
Logger->LogToAI(TEXT("Starting weapon reload..."), ELogLevel::Info);
}
// Create result for when reload completes
FDynamicParams ReloadResult;
ReloadResult.Add(FDynamicParam(true)); // Success flag
// Return promise that completes after reload time
return UFlayerPromiseUtils::DelayedPromise(
GetWorld(),
ReloadTime,
ReloadResult
);
}Flayer Function Utils
The FlayerFunctionUtils library provides a collection of helper functions that simplify common operations needed when writing Flayer functions. From vector manipulation to player state queries, these utilities help speed up development and reduce boilerplate code.
Time Utilities
The AI agent prefers to have time in milliseconds and integers, while Unreal Engine mostly uses floats and seconds. The utilities provide clean conversions between milliseconds and seconds.
// Convert from milliseconds to seconds
float DelaySeconds = UFlayerFunctionUtils::MillisecondsToSeconds(1500); // Returns 1.5
// Convert from seconds to milliseconds
int32 DelayMs = UFlayerFunctionUtils::SecondsToMilliseconds(1.5f); // Returns 1500Vector Utilities
Since we can only send primitive types, vectors are sent as three separate floats. The utilities provide a clean way to convert and manipulate vectors.
// Create vectors from components
FVector PosFromFloats = UFlayerFunctionUtils::MakeVectorFromFloats(100.0f, 200.0f, 300.0f);
FVector PosFromInts = UFlayerFunctionUtils::MakeVectorFromInts(100, 200, 300);
// Break vector into components
float X, Y, Z;
UFlayerFunctionUtils::BreakVectorToFloats(PosFromFloats, X, Y, Z);
// Convert vector to string for logging or display
FString LocationStr = UFlayerFunctionUtils::VectorToString(PlayerLocation);
Logger->LogToAI(FString::Printf(TEXT("Player at: %s"), *LocationStr));The library provides structured logging functions that ensure consistent formatting:
// Log vector with context
UFlayerFunctionUtils::LogVector(Logger, TEXT("Player Position"), PlayerLocation);
// Log rotator with context
UFlayerFunctionUtils::LogRotator(Logger, TEXT("Camera Rotation"), CameraRotation);World Interaction
The library provides convenient ways to interact with the game world and its actors:
// Get actor's location
FVector Location = UFlayerFunctionUtils::GetActorLocation(SomeActor);
// Get distance between actors
float Distance = UFlayerFunctionUtils::GetDistanceBetweenActors(ActorA, ActorB);
// Get direction to target
FVector Direction = UFlayerFunctionUtils::GetDirectionToActor(SourceActor, TargetActor);String operations
String manipulation utilities help with formatting and parsing:
// Join array of strings
TArray<FString> Items = {"A", "B", "C"};
FString Combined = UFlayerFunctionUtils::JoinStrings(Items, TEXT(", ")); // "A, B, C"
// Split string into array
TArray<FString> Split = UFlayerFunctionUtils::SplitString(Combined, TEXT(", "));Dynamic Param Utils
Dynamic parameters form the foundation of data exchange between your game and AI agents. When working with primitive types, this conversion is automatically handled for you. However, when dealing with Flayer promises, you need to create dynamic parameters manually. The DynamicParamUtils library provides a set of functions to simplify this process.
The most common primitive types are supported, including:
// Single-value collections
FDynamicParams StringParams = UDynamicParamUtils::CreateStringDynamicParams("Message");
FDynamicParams BoolParams = UDynamicParamUtils::CreateBoolDynamicParams(true);
FDynamicParams IntParams = UDynamicParamUtils::CreateIntDynamicParams(42);
FDynamicParams FloatParams = UDynamicParamUtils::CreateFloatDynamicParams(3.14f);