Unreal C++ · 插件开发 · Native ABI

在 C++ 中复用完整插件能力

蓝图层建立在导出的 UObject 类之上,并不是另一套实现。玩法 C++ 可以创建相同对话、绑定相同事件、使用记忆/工具/MCP;只有确实需要更低层控制时,才显式进入完整 Native SDK。

始终选择能够满足需求的最高层接口

代码需要什么使用原因
自动设置的一段聊天ULiteRtLmQuickChat最小所有权与事件接口。
多身份、记忆与工具ULiteRtLmAgent完整的产品级对话对象。
模型状态或 Agent 工厂重载ULiteRtLmSubsystem共享 Engine Subsystem 与 C++ 便利接口。
MCP 路由与 Schema 生命周期ULiteRtLmMcpGateway负责路由、Pending Call 与历史回放。
直接使用上游句柄/函数LiteRtLmNativeSdk.h完整官方 C API + 仅头文件 C++ 所有权/重载层。
不要因为某项功能也暴露给蓝图,就在 C++ 中重新实现。Quick Chat、Agent 记忆、工具、MCP、诊断与生命周期都是公开 C++ API。只有上层确实无法表达需求时才下沉。

1. 添加 Unreal 模块依赖

在玩法模块或你的插件模块 .Build.cs 中:

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

如果你自己的 Public 头文件直接暴露 LiteRT-LM 类型,则改放到 PublicDependencyModuleNames。该模块会发布场景层头文件与 Native SDK Include Path。

2. 在 Actor 中创建一段单对话

在 Actor 头文件中声明被持有的 Quick Chat 与动态事件处理器:

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

UPROPERTY(Transient)
TObjectPtr<ULiteRtLmQuickChat> QuickChat;

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

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

通常在 BeginPlay 只创建一次,Ask 之前先绑定事件,之后一直复用同一个对象:

#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);
}

单参数 AskOnceAskStreaming 重载使用默认请求选项。某一次请求需要自定义 FLiteRtLmAskOptions 时,使用双参数重载。

3. 创建多个独立 Agent

取得共享 Engine Subsystem,并使用它的 C++ 重载:

#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 提供“显示名”“显示名 + System Prompt”“显示名 + System Prompt + Tool Declaration JSON”三组便利重载。AskAskMessagesJsonSubmitToolResult 同样提供默认 Options 重载。

所有 Agent 都应存入由对局或玩法系统持有的 UPROPERTY 数组/Map。公共事件把真实事件追加到所有相关 Agent 的记忆;私密事件只调用指定收件人。

所有权与异步规则

  • 反射持有 UObject:使用 UPROPERTY/TObjectPtr 字段;临时局部裸指针不是完整的所有权方案。
  • Ask 前绑定:完成和错误都通过异步事件送达。
  • 使用 Request ID:Ask 立即返回身份,事件稍后带回最终结果。
  • 同一个 Agent 不重叠请求:先检查 IsBusy(),或正确处理拒绝错误。
  • 一份模型,多段对话:Agent 共享 Runtime 和串行推理队列;只有你的代码复制消息时,它们才会共享记忆。
  • 玩法边界显式 Close:尤其是带着未完成请求替换整局/整场景时。

确实需要时使用完整 Native SDK

包含统一入口头文件:

#include "LiteRtLmNativeSdk.h"

它会包含来自 c/engine.h 的完整上游 C 声明;在 C++ 中还会包含无异常、仅头文件的 litert_lm_native.hpp 所有权和重载层。每个 Owner 都暴露 get()release(),因此上游新增函数仍可直接使用,不必等待便利包装再增加同名方法。

Native SDK 链接是显式选择。场景模块本身不链接 LiteRtLm_Native.lib。Win64 消费模块直接调用 Native 符号时,必须添加 Source/ThirdParty/LiteRtLm/Binaries/Win64 下的 Import Library,并部署旁边的官方 Runtime Bundle。

以下示例适用于 ModuleDirectory 位于 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"));
}

如果消费模块位于另一个插件,需要调整路径发现方式。除非需求就是直接 Native 控制,否则 Unreal 生命周期、委托、记忆、MCP 与打包仍应优先使用上层 UObject API。

发布到 Win11 与 Android

如果你只想先运行完整 Demo,不需要自己编译 UE5 项目。打开 LiteRTDemo v5.1.0 Release,下载本页所示的重组脚本;脚本会自动下载全部分卷、检查每个分卷及最终文件的 SHA-256,再生成可运行文件。

1. 下载并重组发布包

Assemble-LiteRTDemo-v5.1.0.ps1 放进一个空目录,在该目录打开 PowerShell,然后二选一运行:

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

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

Android 包只面向 ARM64。Windows 包解压后运行 LiteRTDemo.exe。Release 同时提供清单和 SHA256SUMS.txt,因此不需要手工拼接或猜测文件是否完整。

2. 在手机上先读取真实可用内存

v5.1.0 会在创建 Native 模型之前查询设备的总物理内存、当前可用内存和本进程已用内存,再把项目配置的上下文上限保守地限制到 4K、8K、16K 或 32K 档位。多个 Agent 仍共享同一份 Runtime 和模型;创建多段对话不会加载多份模型。

#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 默认开启;对应配置是 bAutoTuneAndroidContext=True。只有你已经在目标机型上做过压力测试时才建议关闭。内存规划会减少上下文缓存压力,但无法掩盖驱动、GPU Delegate 或 Native ABI 崩溃,仍需查看系统崩溃日志。

3. Android 推理闪退时导出日志

Release 中的 Collect-LiteRTDemoAndroidCrash.ps1 会通过 ADB 收集应用信息、设备内存、GPU/驱动信息、Logcat 和 Android 崩溃退出原因。连接手机并允许 USB 调试后运行:

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

复现一次闪退后按脚本提示结束采集,把生成目录压缩后提供即可。日志可能包含设备型号、Android 版本、进程信息和应用输出;分享前可以先检查或删除不希望公开的字段。

当前 GitHub Android 包是用于 Demo 与故障定位的 ARM64 v2 签名包。它使用 Android debug 证书,不是商店 Distribution/AAB 成品。本次发布已完成 UE Shipping 构建、Native 依赖闭包和签名结构检查,但发布时没有连接物理 ARM64 手机,因此真机 GPU 推理结果必须以你的设备日志为准。

4. 自己打包插件项目

  • 消费模块已经依赖 LiteRTLMUnreal
  • UObject 引用经过反射持有,不会被垃圾回收。
  • 动态事件处理器是签名完全匹配的 UFUNCTION
  • Win64/Android 目标二进制和模型都存在于 Staged Build。
  • 直接 Native 调用者在受支持目标上显式链接 Import Library。
  • 打包烟雾测试至少创建一段对话、收到一次完成事件,再通过同一个对象发送第二条消息。