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++ 所有权/重载层。 |
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);
}
单参数 AskOnce 与 AskStreaming 重载使用默认请求选项。某一次请求需要自定义 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”三组便利重载。Ask、AskMessagesJson 和 SubmitToolResult 同样提供默认 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(),因此上游新增函数仍可直接使用,不必等待便利包装再增加同名方法。
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 版本、进程信息和应用输出;分享前可以先检查或删除不希望公开的字段。
4. 自己打包插件项目
- 消费模块已经依赖
LiteRTLMUnreal。 - UObject 引用经过反射持有,不会被垃圾回收。
- 动态事件处理器是签名完全匹配的
UFUNCTION。 - Win64/Android 目标二进制和模型都存在于 Staged Build。
- 直接 Native 调用者在受支持目标上显式链接 Import Library。
- 打包烟雾测试至少创建一段对话、收到一次完成事件,再通过同一个对象发送第二条消息。