Unreal C++ · Plugin development · Native ABI

Reuse the full plugin from C++

The Blueprint layer is built on exported UObject classes, not a separate implementation. C++ gameplay code can create the same conversations, bind the same events, use memory/tools/MCP, and opt into the complete native SDK when it truly needs the lower layer.

Choose the highest useful surface

Your code needsUseWhy
One chat with automatic setupULiteRtLmQuickChatSmallest ownership and event surface.
Multiple identities, memory, toolsULiteRtLmAgentFull product-facing conversation object.
Model status or Agent factory overloadsULiteRtLmSubsystemShared Engine Subsystem and C++ conveniences.
MCP routes and schema lifecycleULiteRtLmMcpGatewayOwns routing, pending calls, and history replay.
Direct upstream handles/functionsLiteRtLmNativeSdk.hComplete official C API plus header-only C++ ownership/overloads.
Do not reimplement a feature just because Blueprint also exposes it. Quick Chat, Agent memory, tools, MCP, diagnostics, and lifecycle are public C++ APIs. Move down a layer only when the higher layer cannot express your requirement.

1. Add the Unreal module dependency

In your gameplay module or plugin module .Build.cs:

PrivateDependencyModuleNames.AddRange(new[]
{
    "Core",
    "CoreUObject",
    "Engine",
    "LiteRTLMUnreal"
});

Use PublicDependencyModuleNames instead if your own public headers expose LiteRT-LM types. The module publishes the scenario headers and native SDK include path.

2. Create one conversation in an Actor

Declare an owned Quick Chat and dynamic handlers in your Actor header:

#include "LiteRtLmQuickChat.h"
#include "LiteRtLmUnrealApi.h"

UPROPERTY(Transient)
TObjectPtr<ULiteRtLmQuickChat> QuickChat;

UFUNCTION()
void HandleAnswer(const FLiteRtLmResult& Result);

UFUNCTION()
void HandleChatError(const FLiteRtLmError& Error);

Create once—typically in BeginPlay—bind events before Ask, then keep using the same object:

#include "LiteRtLmBlueprintLibrary.h"

void ALocalChatActor::BeginPlay()
{
    Super::BeginPlay();

    QuickChat = ULiteRtLmBlueprintLibrary::CreateQuickChat(
        this,
        TEXT("Answer concisely in the user's language."));

    if (!IsValid(QuickChat))
    {
        return;
    }

    QuickChat->OnAnswer.AddDynamic(this, &ALocalChatActor::HandleAnswer);
    QuickChat->OnError.AddDynamic(this, &ALocalChatActor::HandleChatError);
    QuickChat->AskOnce(TEXT("Hello from Unreal C++."));
}

void ALocalChatActor::HandleAnswer(const FLiteRtLmResult& Result)
{
    UE_LOG(LogTemp, Display, TEXT("AI: %s"), *Result.Text);
}

The single-argument AskOnce and AskStreaming overloads use default request options. Use the two-argument overload when a particular request needs custom FLiteRtLmAskOptions.

3. Create multiple independent Agents

Get the shared Engine Subsystem and use its C++ overloads:

#include "LiteRtLmAgent.h"
#include "LiteRtLmSubsystem.h"

ULiteRtLmSubsystem* Runtime =
    GEngine->GetEngineSubsystem<ULiteRtLmSubsystem>();

ULiteRtLmAgent* Judge = Runtime
    ? Runtime->CreateAgent(
        TEXT("Judge"),
        TEXT("Apply the game rules and return structured decisions."))
    : nullptr;

if (IsValid(Judge))
{
    Judge->OnCompleted.AddDynamic(this, &AMatchDirector::HandleJudgeResult);
    Judge->Ask(TEXT("Evaluate the current round."));
}

CreateAgent has C++ overloads for display name; display name + system prompt; and display name + system prompt + tool declaration JSON. Ask, AskMessagesJson, and SubmitToolResult also have default-options overloads.

Store every Agent in a UPROPERTY array/map owned by the match or gameplay system. For public events, append the actual event to each relevant Agent’s memory; for private events, call only the intended recipients.

Ownership and asynchronous rules

  • Keep UObjects referenced: use UPROPERTY/TObjectPtr fields. A temporary local pointer is not an ownership plan.
  • Bind before Ask: completion and error delivery is asynchronous.
  • Use Request IDs: Ask returns identity immediately; the event carries the terminal result later.
  • Do not overlap requests on one Agent: check IsBusy() or handle the rejection error.
  • One model, many conversations: Agents share the runtime and serial inference queue, but never share canonical memory unless your code copies messages.
  • Close explicitly at a gameplay boundary: especially when replacing a match/scene with outstanding requests.

Use the complete native SDK when required

Include the umbrella header:

#include "LiteRtLmNativeSdk.h"

It includes the complete upstream C declarations from c/engine.h. In C++, it also includes the exception-free header-only litert_lm_native.hpp ownership and overload layer. Every owner exposes get() and release(), so newly added upstream functions remain reachable without waiting for another wrapper method.

Native SDK linkage is an explicit opt-in. The scenario module does not link LiteRtLm_Native.lib. A Win64 consumer that directly calls native symbols must add the import library from Source/ThirdParty/LiteRtLm/Binaries/Win64 and ship its sibling runtime bundle.

Example for a project module whose ModuleDirectory is under Project/Source/YourModule:

if (Target.Platform == UnrealTargetPlatform.Win64)
{
    string ProjectRoot = Path.GetFullPath(
        Path.Combine(ModuleDirectory, "..", ".."));
    string LiteRtLmBin = Path.Combine(
        ProjectRoot,
        "Plugins", "LiteRT-LM-Unreal", "Source", "ThirdParty",
        "LiteRtLm", "Binaries", "Win64");

    PublicAdditionalLibraries.Add(
        Path.Combine(LiteRtLmBin, "LiteRtLm_Native.lib"));
}

Adjust discovery when the consuming module lives in another plugin. Keep the higher UObject layer for Unreal lifetime, delegates, memory, MCP, and packaging unless direct native control is the actual requirement.

Ship to Windows 11 and Android

You do not need to compile the UE5 project just to run the complete demo. Open the LiteRTDemo v5.1.0 release and download the assembly script. It downloads every part, verifies the SHA-256 of each part and of the final artifact, and only then produces the runnable file.

1. Download and assemble a package

Put Assemble-LiteRTDemo-v5.1.0.ps1 in an empty directory, open PowerShell there, and run one of these commands:

# Android ARM64: produces LiteRTDemo-Android-Shipping-arm64.apk
powershell -ExecutionPolicy Bypass -File .\Assemble-LiteRTDemo-v5.1.0.ps1 -Target Android -Download

# Windows 11: produces LiteRTDemo-Windows-v5.1.0.zip
powershell -ExecutionPolicy Bypass -File .\Assemble-LiteRTDemo-v5.1.0.ps1 -Target Windows -Download

The Android package targets ARM64 only. Extract the Windows package and run LiteRTDemo.exe. The release also includes manifests and SHA256SUMS.txt, so there is no manual joining or guesswork about file integrity.

2. Read real available memory on the phone

Before creating the native model, v5.1.0 queries total physical memory, currently available memory, and physical memory used by the process. It then conservatively caps the configured context limit to a 4K, 8K, 16K, or 32K tier. Multiple Agents still share one runtime and model; multiple conversations do not load multiple model copies.

#include "LiteRtLmSubsystem.h"

ULiteRtLmSubsystem* Runtime =
    GetGameInstance()->GetSubsystem<ULiteRtLmSubsystem>();
const FLiteRtLmMemoryPlan Plan = Runtime->GetMemoryPlan();

UE_LOG(LogTemp, Display,
    TEXT("LiteRT-LM RAM: total=%lld MB available=%lld MB process=%lld MB, context=%d/%d, %s"),
    Plan.TotalPhysicalMB,
    Plan.AvailablePhysicalMB,
    Plan.ProcessUsedPhysicalMB,
    Plan.EffectiveMaxContextTokens,
    Plan.ConfiguredMaxContextTokens,
    *Plan.PolicySummary);

Auto Tune Context for Device Memory is enabled by default in Project Settings; its config key is bAutoTuneAndroidContext=True. Disable it only after stress-testing the exact target devices. The memory plan reduces context-cache pressure, but it cannot hide a driver, GPU delegate, or native ABI crash; collect the system crash evidence as well.

3. Export evidence after an Android inference crash

The release includes Collect-LiteRTDemoAndroidCrash.ps1. It uses ADB to collect app metadata, device memory, GPU/driver information, Logcat, and Android process-exit reasons. Connect the phone, allow USB debugging, then run:

powershell -ExecutionPolicy Bypass -File .\Collect-LiteRTDemoAndroidCrash.ps1

Reproduce the crash once, finish collection as instructed, and share the resulting directory as an archive. The logs can contain the device model, Android version, process metadata, and application output; review or redact anything you do not want to share.

The current GitHub Android package is an ARM64 v2-signed demo and diagnostic build. It uses the Android debug certificate and is not a store-ready Distribution/AAB package. This release passed UE Shipping build, native dependency-closure, and signing-structure checks, but no physical ARM64 phone was connected at publication time. Treat your device log as authoritative for real GPU inference.

4. Package your own plugin project

  • The consuming module depends on LiteRTLMUnreal.
  • UObject references are reflected and survive garbage collection.
  • Dynamic handlers are UFUNCTIONs with exact delegate signatures.
  • Win64/Android target binaries and the model are present in the staged build.
  • Direct native callers explicitly link the import library on supported targets.
  • A packaged smoke test creates one conversation, receives one completion, then sends a second message through the same object.