27 KiB
tags, created
| tags | created | ||||||||
|---|---|---|---|---|---|---|---|---|---|
|
2026-05-31 |
Puerts 架构深度分析
概述
Puerts(发音 "pu-erts")是腾讯开源的 Unreal Engine TypeScript/JavaScript 运行时。它将 V8 JavaScript 引擎直接嵌入 UE 的模块系统,实现 C++ 和 TypeScript 的双向互操作。
- 仓库:
D:\MatrixTA\puerts - UE 端源码:
puerts\unreal\Puerts\ - 许可证: BSD 3-Clause
插件结构
Puerts.uplugin 定义了 6 个模块,有严格的初始化顺序:
| 模块 | 类型 | LoadingPhase | 用途 |
|---|---|---|---|
| WasmCore | Runtime | PreDefault | WebAssembly (wasm3) 支持 |
| JsEnv | Runtime | PreDefault | 核心 V8 嵌入引擎 |
| ParamDefaultValueMetas | Program | PostConfigInit | UHT 插件,参数默认值 |
| Puerts | Runtime | PostEngineInit | 高层模块:设置、JsEnv 生命周期、PIE 钩子 |
| DeclarationGenerator | Editor | Default | 从 UE 反射生成 *.d.ts |
| PuertsEditor | Editor | PostEngineInit | 编辑器集成、TS 编译监听 |
依赖链
WasmCore → Core, CoreUObject, Engine
JsEnv → Core, CoreUObject, Engine, UMG, ParamDefaultValueMetas, WasmCore, Json
Puerts → Core, CoreUObject, Engine, Serialization, OpenSSL, UMG, JsEnv
PuertsEditor → Puerts, JsEnv, UnrealEd, LevelEditor, DirectoryWatcher, AssetRegistry
DeclarationGenerator → Puerts, JsEnv, UnrealEd
JsEnv 链接 V8 静态库(wee8.lib / v8_11.8.172),也可切换为 QuickJS 或 Node.js 后端。
启动流程
1. V8 Isolate 初始化
FJsEnvImpl 构造函数(JsEnvImpl.cpp:347-646):
- 创建
v8::Isolate,分配默认 ArrayBuffer 分配器 - 创建
v8::Context存入DefaultContext - 将
this指针存为 Isolate 数据:Isolate->SetData(0, static_cast<IObjectMapper*>(this))
2. C++ → JS 桥接注册
构造函数注册 C++ 回调为全局 JavaScript 函数:
MethodBindingHelper<&FJsEnvImpl::EvalScript>::Bind(..., "__tgjsEvalScript", ...);
MethodBindingHelper<&FJsEnvImpl::SearchModule>::Bind(..., "__tgjsSearchModule", ...);
MethodBindingHelper<&FJsEnvImpl::LoadModule>::Bind(..., "__tgjsLoadModule", ...);
MethodBindingHelper<&FJsEnvImpl::FindModule>::Bind(..., "__tgjsFindModule", ...);
MethodBindingHelper<&FJsEnvImpl::LoadUEType>::Bind(..., puertsObj, "loadUEType", ...);
MethodBindingHelper<&FJsEnvImpl::LoadCppType>::Bind(..., puertsObj, "loadCPPType", ...);
MethodBindingHelper<&FJsEnvImpl::NewObjectByClass>::Bind(..., "__tgjsNewObject", ...);
MethodBindingHelper<&FJsEnvImpl::MakeUClass>::Bind(..., "__tgjsMakeUClass", ...);
MethodBindingHelper<&FJsEnvImpl::Mixin>::Bind(..., "__tgjsMixin", ...);
// + setTimeout, setInterval, dumpStatisticsLog, FNameToArrayBuffer 等
3. Bootstrap JS 模块(按顺序执行)
ExecuteModule("puerts/first_run.js"); // Inspector 存根(可选)
ExecuteModule("puerts/polyfill.js"); // JS polyfills(非 NodeJS 模式)
ExecuteModule("puerts/log.js"); // console.log → UE_LOG
ExecuteModule("puerts/modular.js"); // CommonJS/ESM 模块系统
ExecuteModule("puerts/uelazyload.js"); // UE 类型懒加载 (Proxy)
ExecuteModule("puerts/events.js"); // EventEmitter
ExecuteModule("puerts/promises.js"); // Promise 支持
ExecuteModule("puerts/argv.js"); // 命令行参数
ExecuteModule("puerts/jit_stub.js"); // JIT 模块存根
ExecuteModule("puerts/hot_reload.js"); // V8 Inspector 热重载
ExecuteModule("puerts/pesaddon.js"); // PESAPI 原生 addon 加载
4. Start() 入口
FJsEnvImpl::Start() 调用 require(ModuleName) 执行用户入口模块。
模块解析系统
核心文件
puerts/modular.js— JS 端 CommonJS/ESM 模块系统JSModuleLoader.h— C++IJSModuleLoader接口DefaultJSModuleLoader.cpp— 文件系统解析实现
genRequire() 工厂
modular.js 实现 Node.js 兼容的 CommonJS 系统:
// require() 解析流程
1. 尝试 org_require (Node.js 原生,如果使用 Node.js 后端)
2. 检查 buildinModule 缓存 ("ue", "puerts", "cpp")
3. 调用 __tgjsFindModule(moduleName) — 搜索 PUERTS_MODULE 注册的 C++ addon
4. 调用 __tgjsSearchModule(moduleName, requiringDir) — 文件系统搜索(见下)
5. 调用 __tgjsLoadModule(fullPath) — 加载文件
6. CommonJS wrapper 执行: (function(exports, require, module, __filename, __dirname) { ... })
7. ES 模块 (.mjs/.mbc): 使用 V8 原生 v8::Module API
文件系统搜索(DefaultJSModuleLoader::Search)
搜索算法:
- 在
RequiredDir/RequiredModule查找(尝试扩展名:.js,.mjs,.cjs,.mbc,.cbc,package.json,index.js) - 在
RequiredDir/node_modules/RequiredModule查找 - 向上遍历目录树,搜索每个父目录的
node_modules - 回退到
Content/ScriptRoot/RequiredModule(默认Content/JavaScript/) - 如果 ScriptRoot 不是
"JavaScript",也回退到Content/JavaScript/
注意:搜索范围不包括插件 Content 目录。
JsEnv 数据流
puerts/uelazyload.js — UE 类型懒加载
这是最关键的模块之一:
- 创建全局
UE对象作为 Proxy,访问属性时懒加载 UClass 类型 - 创建全局
CPP对象用于注册的 C++ 类型 - 将
UE注册为内置模块:require('ue')返回它 - 暴露:
NewObject,NewStruct,FNameLiteral,NewArray/Set/Map,makeUClass,mixin,blueprint.tojs/load/unload - 定义 UE 反射常量:
FunctionFlags,ClassFlags,PropertyFlags - Decorator 存根:
uclass,ufunction,uproperty,uparam等
puerts/callable.js(旧版/替代模式)
使用 sendRequestSync(同步 C++ 桥接)通过 JSON 序列化和 Proxy 包装调用 UE 方法。这是旧的 callable 模式。
puerts/hot_reload.js
使用 V8 Inspector 协议 (Debugger.setScriptSource) 在运行时热重载 JS 模块,无需重启游戏。触发 'HMR.prepare' 和 'HMR.finish' 事件。
puerts/pesaddon.js
包装 puerts.load() 加载原生 .dll/.dylib/.so addon,使用 PESAPI(Portable Embedded Scripting API)。
Puerts 模块(FPuertsModule)
关键文件
PuertsModule.h—IPuertsModule接口PuertsModule.cpp— 实现PuertsSetting.h— 设置 UObject
StartupModule()
void FPuertsModule::StartupModule() {
RegisterSettings(); // 从 DefaultPuerts.ini 加载设置
// Editor: 绑定 PIE PreBeginPIE/EndPIE 委托
// 绑定热重载委托
if (Settings.AutoModeEnable) {
Enable(); // 创建 JsEnv,注册对象监听器
}
}
MakeSharedJsEnv() — JsEnv 工厂
根据 UPuertsSetting 创建:
- 单一 JsEnv (
FJsEnv) — 普通模式 - JsEnvGroup (
FJsEnvGroup) — 多隔离模式(2-9 个实例)
关键设置:
RootPath— JS 源码根目录(默认"JavaScript",相对于Content/)AutoModeEnable— 模块加载时自动启动DebugEnable/DebugPort— V8 Inspector 调试NumberOfJsEnv— 多隔离模式数量
UObject 创建监听
FPuertsModule 同时实现 FUObjectCreateListener,每个新创建的 UObject 触发 NotifyUObjectCreated() → JsEnv->TryBindJs(InObject)。这就是 Puerts 自动将 TypeScript 行为注入 Blueprint 生成对象的机制。
TS → C++ 绑定架构
UTypeScriptGeneratedClass 和 UTypeScriptBlueprint
UTypeScriptBlueprint— TypeScript 类的 Blueprint 资产类型UTypeScriptGeneratedClass— TS 生成的运行时 UClass:DynamicInvoker—TWeakPtr<ITsDynamicInvoker>,回调FJsEnvImplFunctionToRedirect— 需要从 C++ 重定向到 JS 的函数名集合HasConstructor— TS 类是否有Constructor方法StaticConstructor— 替换原生 C++ 构造函数
UJSGeneratedFunction 和 Magic Bytecode
UJSGeneratedFunction 是动态创建的 UFunction,通过 magic bytecode trick 重定向到 JS:
- 在 UFunction 的
Scriptbytecode 中存储自身指针(3 字节 magic marker + 8 字节指针) execCallJS执行时恢复指针- 调用
DynamicInvoker->InvokeJsMethod()→FJsEnvImpl::InvokeJsMethod() FunctionTranslator->CallJs()将所有参数编组到 V8,调用 JS 函数
MakeUClass / Mixin — 运行时类创建
MakeUClass(puerts.makeUClass(ctor)):接收 JS 构造函数、原型、类名、方法对象、父 UClass,创建UJSGeneratedClass,遍历父类FUNC_BlueprintEvent,为每个覆盖创建UJSGeneratedFunctionMixin(puerts.blueprint.mixin(to, mixinMethods)):接收现有 UClass,添加方法覆盖
C++ 调用 TS 的方式
方式 1:FJsObject(直接 JS 函数包装器)
// FJsObject 是 USTRUCT,持有持久化 V8 引用
FJsObject MyFunc;
MyFunc.Action<int32, FString>(42, TEXT("hello")); // 无返回值
int32 Result = MyFunc.Func<int32, int32, FString>(42, TEXT("hello")); // 有返回值
int32 Val = MyObj.Get<int32>("someProperty"); // 读取属性
MyObj.Set("someProperty", 42); // 设置属性
自动使用 puerts::v8_impl::Converter<T> 进行 C++ ↔ V8 类型转换。线程安全(THREAD_SAFE 模式下使用 v8::Locker)。
方式 2:UFUNCTION / BlueprintEvent 覆盖
标准 UE 模式:
// C++ 中定义
UFUNCTION(BlueprintImplementableEvent)
void OnSomethingHappened(int32 Param);
// TypeScript 中实现
class MyActor extends UE.Actor {
OnSomethingHappened(Param: number): void {
// 被 C++ 调用时执行
}
}
方式 3:委托
import { toDelegate, toManualReleaseDelegate } from "puerts";
let d = toDelegate(myObject, myCallback); // 绑定到对象
let d2 = toManualReleaseDelegate(myCallback); // 独立委托
C++ 侧,FJsEnvImpl 维护 DelegateMap 将委托指针映射到 JS 回调函数。
方式 4:ITsDynamicInvoker 接口
IDynamicInvoker::InvokeJsMethod— 从UJSGeneratedFunction::execCallJS调用IDynamicInvoker::JsConstruct— 运行 JS 构造函数ITsDynamicInvoker::TsConstruct— TS 特定构造路径
FJsEnvImpl 通过内部类 DynamicInvokerImpl 和 TsDynamicInvokerImpl 实现这些接口。
方式 5:PUERTS_MODULE 宏(Addon 模块系统)
C++ 代码注册为 addon 模块:
// 在任何 C++ 文件中
PUERTS_MODULE(MyPlugin, [](v8::Local<v8::Context> Context, v8::Local<v8::Object> Exports) {
// 注册类、函数等
Exports->Set(...);
});
使用静态自动注册:
#define PUERTS_MODULE(Name, RegFunc) \
static struct FAutoRegisterFor##Name { \
FAutoRegisterFor##Name() { \
PUERTS_NAMESPACE::RegisterAddon(#Name, (RegFunc)); \
} \
} _AutoRegisterFor##Name
JS 端:require('MyPlugin') → FindModule → 执行注册函数 → 获取 exports。
类型声明文件
位于 puerts\unreal\Puerts\Typing\:
| 文件 | 内容 |
|---|---|
puerts/index.d.ts |
核心 Puerts API:$Ref, blueprint, toDelegate, on/off/emit, merge, load |
ue/puerts.d.ts |
UE 绑定类型:$Delegate, TArray, TSet, TMap, NewObject, NewStruct |
ue/puerts_decorators.d.ts |
装饰器:rpc.flags(), edit_on_instance(), uclass, ufunction |
ue/index.d.ts |
聚合所有声明(通过 /// <reference path="..." />) |
ue.d.ts 和 ue_bp.d.ts 由 DeclarationGenerator 模块使用 UE 反射系统自动生成。
架构图
[Game/Editor 启动]
│
▼
FPuertsModule::StartupModule()
├── RegisterSettings() → 读取 DefaultPuerts.ini
└── Enable()
├── GUObjectArray::AddUObjectCreateListener(this) // 拦截所有 UObject 创建
└── MakeSharedJsEnv()
└── FJsEnv(ScriptRoot) 包装器
└── FJsEnvImpl 构造
├── v8::Isolate::New() + v8::Context::New()
├── 注册全局 C++ 回调 (__tgjs* + puerts.*)
├── ExecuteModule() ×13 (bootstrap JS 文件)
└── 捕获 global.require, global.__reload
└── Start(ModuleName) → require(ModuleName) → 用户代码执行
│
▼
[运行时: UObject 创建(如 Blueprint 实例化)]
│
▼
FPuertsModule::NotifyUObjectCreated()
└── JsEnv->TryBindJs(InObject)
├── TypeScriptGeneratedClass? → MakeSureInject (绑定 JS 原型)
└── JSGeneratedClass? → JsConstruct (调用 JS 构造函数)
[C++ 调用 TS]
UFUNCTION(BlueprintEvent) _Implementation → UE VM → execCallJS
→ DynamicInvoker->InvokeJsMethod()
→ FJsEnvImpl::InvokeJsMethod()
→ FunctionTranslator->CallJs() [参数编组]
→ JS 函数被调用
[TS 调用 C++]
require('ue') → Proxy → loadUEType → UETypeToJsClass → DataTransfer::FindOrAddObject
new MyActor() → NewObjectByClass → UObject 创建并调用构造函数
actor.K2_SomeMethod() → UFUNCTION → 原生 C++ 代码执行
关键文件索引
| 文件 | 用途 |
|---|---|
unreal/Puerts/Puerts.uplugin |
插件定义(模块列表、加载阶段) |
Source/Puerts/Public/PuertsModule.h |
IPuertsModule 接口 |
Source/Puerts/Private/PuertsModule.cpp |
模块启动/关闭,JsEnv 生命周期,PIE 钩子 |
Source/JsEnv/Public/JsEnv.h |
FJsEnv 公开包装器 |
Source/JsEnv/Private/JsEnvImpl.h |
FJsEnvImpl 完整定义 |
Source/JsEnv/Private/JsEnvImpl.cpp |
V8 初始化、bootstrap、模块执行、绑定 |
Source/JsEnv/Public/JSModuleLoader.h |
IJSModuleLoader 接口 + DefaultJSModuleLoader |
Source/JsEnv/Private/DefaultJSModuleLoader.cpp |
文件系统搜索/加载实现 |
Source/JsEnv/Public/JsObject.h |
FJsObject — JS 函数/对象的 C++ 包装器 |
Source/JsEnv/Public/DataTransfer.h |
DataTransfer — C++/JS 类型转换 |
Source/JsEnv/Public/DynamicInvoker.h |
IDynamicInvoker 接口 |
Source/JsEnv/Public/TsDynamicInvoker.h |
ITsDynamicInvoker 接口 |
Source/JsEnv/Public/TypeScriptGeneratedClass.h |
UTypeScriptGeneratedClass |
Source/JsEnv/Private/JSGeneratedClass.h |
UJSGeneratedClass |
Source/JsEnv/Private/JSGeneratedFunction.h |
UJSGeneratedFunction (magic bytecode) |
Source/JsEnv/Private/FunctionTranslator.h |
FFunctionTranslator — C++/JS 参数编组 |
Source/JsEnv/Public/JSClassRegister.h |
JSClassDefinition, PUERTS_MODULE 宏 |
Source/JsEnv/Public/JsEnvGroup.h |
FJsEnvGroup — 多隔离模式 |
Content/JavaScript/puerts/modular.js |
JS 端 CommonJS/ESM 模块系统 |
Content/JavaScript/puerts/uelazyload.js |
UE 类型懒加载, makeUClass, mixin |
Content/JavaScript/puerts/callable.js |
替代 Proxy callable 模式 |
Content/JavaScript/puerts/hot_reload.js |
V8 Inspector 热重载 |
Content/JavaScript/puerts/pesaddon.js |
PESAPI 原生 addon 加载器 |
EasyEditorPlugin 架构分析
概述
EasyEditorPlugin 是 johnche(腾讯,也是 Puerts 的作者)开发的 Unreal Engine 编辑器快速扩展插件。它将 V8 JavaScript 运行时(通过 Puerts 的 FJsEnv)嵌入 Unreal Editor,让开发者可以用 TypeScript/JavaScript 编写编辑器扩展,而非 C++。
- 仓库:
D:\MatrixTA\EasyEditorPlugin - 许可证: MIT
- 引擎兼容: UE 5.3+
- 核心价值: 几行 TypeScript 完成编辑器菜单、工具栏、控制台命令、Detail 面板定制,支持热重载
目录结构
EasyEditorPlugin/
├── .gitignore
├── EasyEditorPlugin.uplugin # UE 插件描述符
├── LICENSE # MIT
├── README.md # 使用文档(英文)
├── Resources/
│ └── Icon128.png # 插件图标
├── Doc/
│ ├── extension.md # 第三方 C++ 库扩展指南
│ └── Pic/
│ └── easyeditor.gif # 演示动画
└── Source/
└── EasyEditorPlugin/
├── EasyEditorPlugin.Build.cs # UE 构建规则
├── Public/
│ └── EasyEditorPlugin.h # 模块头文件(公开 API)
└── Private/
└── EasyEditorPlugin.cpp # 完整实现(~320 行)
单模块 Editor 插件,无 Runtime/Gameplay 模块。
.uplugin 定义
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "EasyEditorPlugin",
"CanContainContent": true,
"Modules": [{
"Name": "EasyEditorPlugin",
"Type": "Editor",
"LoadingPhase": "Default"
}],
"Plugins": [{
"Name": "Puerts",
"Enabled": true
}]
}
Build.cs 模块依赖
| 依赖 | 类型 | 用途 |
|---|---|---|
Core |
Public | 基础 UE 类型(FString, TSharedPtr 等) |
CoreUObject |
Private | UObject 系统 |
Engine |
Private | Engine 层类型 |
Slate |
Private | Slate UI 框架 |
SlateCore |
Private | 核心 Slate 类型(FSlateIcon) |
ToolMenus |
Private | UE5 可扩展菜单系统 |
UnrealEd |
Private | 编辑器 API(地图变更通知等) |
ContentBrowserData |
Private | Content Browser 上下文菜单 |
JsEnv |
Private | Puerts JavaScript 环境 |
公开 API(EasyEditorPlugin.h)
class FEasyEditorPluginModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
std::function<void()> OnJsEnvPreReload;
std::function<void(const FString&)> Eval;
TSparseArray<TUniquePtr<FAutoConsoleCommand>> TsConsoleCommands;
FSimpleMulticastDelegate OnJsEnvCleanup;
private:
TSharedPtr<puerts::FJsEnv> JsEnv; // ← 私有!外部无法访问
TSharedPtr<puerts::FSourceFileWatcher> SourceFileWatcher;
TUniquePtr<FAutoConsoleCommand> ConsoleCommand;
bool StartupScriptCalled = false;
void OnPostEngineInit();
void InitJsEnv();
void UnInitJsEnv();
bool Tick(float);
void HandleMapChanged(UWorld* InWorld, EMapChangeType InMapChangeType);
};
关键点:JsEnv 是私有成员,外部 C++ 代码无法直接获取 JS 函数/对象。
Puerts 绑定(AutoRegisterForEEP)
静态初始化器在模块加载前(StartupModule 之前)自动注册以下 C++ 类型到 V8:
| C++ 类型 | 暴露的功能 |
|---|---|
FSlateIcon |
三个构造函数重载 |
FToolMenuEntry |
InitMenuEntry, InitToolBarButton, InitComboButton |
EasyEditorPlugin |
SetOnJsEnvPreReload, SetEval, AddConsoleCommand, RemoveConsoleCommand |
FContentBrowserItem |
IsFolder, IsFile, GetItemName, GetItemPhysicalPath |
生命周期
Module Load → AutoRegisterForEEP 构造(注册 Puerts 绑定)
│
▼
StartupModule()
├── 设置 V8 --expose-gc 标志(允许显式 GC)
├── 注册 OnPostEngineInit 回调
└── 注册 OnMapChanged 处理器(地图卸载时 GC)
│
▼
OnPostEngineInit()
├── InitJsEnv()
│ ├── 创建 FSourceFileWatcher(热重载)
│ └── 创建 FJsEnv,ModuleLoader 指向 "EasyEditorScripts"
├── 注册每帧 Tick
├── 注册 "EasyEditor.Restart" 控制台命令
└── 注册 "TypeScript" UToolMenus 字符串命令处理器
│
▼
First Tick()
└── JsEnv->Start("Main") // 执行 Main.ts
│
▼
[热重载循环]
SourceFileWatcher 检测文件变更
→ 读取文件
→ JsEnv->ReloadSource(InPath, Content)
│
▼
ShutdownModule()
└── UnInitJsEnv()
├── Broadcast OnJsEnvCleanup
├── 清除 Eval / OnJsEnvPreReload
├── 清空 TsConsoleCommands
└── 重置 JsEnv 和 SourceFileWatcher
InitJsEnv() 详细分析
void FEasyEditorPluginModule::InitJsEnv()
{
// 1. 创建文件监听器(用于热重载)
SourceFileWatcher = MakeShared<puerts::FSourceFileWatcher>(
[this](const FString& InPath) {
if (JsEnv.IsValid()) {
TArray<uint8> Source;
if (FFileHelper::LoadFileToArray(Source, *InPath)) {
JsEnv->ReloadSource(InPath, puerts::PString(
(const char*)Source.GetData(), Source.Num()));
}
}
});
// 2. 创建 JsEnv
// 关键: DefaultJSModuleLoader("EasyEditorScripts")
// → 从项目 Content/EasyEditorScripts/ 加载 TS 文件
JsEnv = MakeShared<puerts::FJsEnv>(
std::make_shared<puerts::DefaultJSModuleLoader>(TEXT("EasyEditorScripts")),
std::make_shared<puerts::FDefaultLogger>(), -1,
[this](const FString& InPath) {
if (SourceFileWatcher.IsValid()) {
SourceFileWatcher->OnSourceLoaded(InPath);
}
});
}
当前限制:
DefaultJSModuleLoader("EasyEditorScripts")只在项目Content/目录下搜索- 不搜索插件 Content 目录
- 不能直接从插件的
Resources/或Content/加载 TS
编辑器集成能力
菜单扩展(UToolMenus)
FToolMenuEntry.InitMenuEntry— 添加普通菜单项FToolMenuEntry.InitToolBarButton— 添加工具栏按钮FToolMenuEntry.InitComboButton— 添加下拉工具栏按钮
Content Browser 上下文菜单
FContentBrowserItem.IsFolder(),IsFile(),GetItemName(),GetItemPhysicalPath()
控制台命令
EasyEditorPlugin.AddConsoleCommand(name, help, callback)— 注册控制台命令EasyEditorPlugin.RemoveConsoleCommand(index)— 注销
String Command Handler
UToolMenus::RegisterStringCommandHandler("TypeScript", ...) 让菜单项可以触发任意 TypeScript 代码。
重启命令
内置 EasyEditor.Restart 控制台命令:销毁 JS 环境 → 重新创建 → 重新加载 Main 脚本。
扩展点(给其他插件使用)
| 扩展点 | 类型 | 用途 |
|---|---|---|
OnJsEnvCleanup |
FSimpleMulticastDelegate |
JS 环境销毁前,其他插件清理状态 |
OnJsEnvPreReload |
std::function<void()> |
重启前执行 |
Eval |
std::function<void(const FString&)> |
执行任意 JS 字符串 |
其他插件的集成方式(以 EasyEditor_ImGui 为例):
// 注册清理回调
FModuleManager::LoadModuleChecked<FEasyEditorPluginModule>("EasyEditorPlugin")
.OnJsEnvCleanup.AddLambda([]() {
// 清理状态
});
脚本组织
- 脚本目录:
Content/EasyEditorScripts/(项目 Content 下) - 入口文件:
Main.js或Main.ts - 语言: TypeScript(主要)/ JavaScript
- 热重载: ✅ 支持
当前缺少的能力
- JsEnv 不公开 — 外部 C++ 无法获取 JS 函数引用(
FJsObject) - ModuleLoader 不灵活 — 只支持项目 Content 目录,不支持插件 Content 目录
- Editor-Only — 无法用于游戏运行时业务逻辑
- 无返回值调用 —
Eval只能执行字符串,无法获取返回值 - 无结构化的 C++ → TS 调用 API — 只能通过字符串 Eval 或 UE 反射间接调用
总结
| 方面 | 详情 |
|---|---|
| 设计目标 | 编辑器快速扩展 |
| 插件类型 | Editor-only |
| 运行时引擎 | V8(通过 puerts::FJsEnv) |
| 脚本语言 | TypeScript / JavaScript |
| 脚本入口 | Main.ts,从 EasyEditorScripts/ 加载 |
| C++ 代码量 | ~320 行(极简) |
| 热重载 | ✅ |
| 可扩展性 | 其他插件可通过 OnJsEnvCleanup 回调集成 |
| 核心限制 | JsEnv 私有、脚本路径固定、Editor-Only |
UnrealMCP 架构分析
概述
UnrealMCP(npm 包: unreal-engine-mcp-server, v0.5.21)是由 ChiR24 开发的开源 MCP(Model Context Protocol)服务器,让 AI 助手(Claude Code、Cursor、Gemini 等)能够远程实时控制和自动化 Unreal Engine 5.0-5.7。
- 仓库:
D:\AI\MCP\UnrealEngine\ChiR24_Unreal_mcp - 许可证: MIT
- GitHub:
github.com/ChiR24/Unreal_mcp
双组件架构(TypeScript Server + C++ 插件)
| 组件 | 语言 | 角色 |
|---|---|---|
TypeScript MCP Server (src/) |
TypeScript (Node.js 18+) | MCP 协议端点、schema 验证、请求路由、连接管理 |
McpAutomationBridge 插件 (plugins/McpAutomationBridge/) |
C++ (Unreal Build Tool) | 原生 UE API 执行、WebSocket 服务器、属性反射、资产管理 |
两者通过 WebSocket(传统模式)或 C++ 插件直接服务 AI 客户端(Native HTTP/SSE,推荐模式)通信。
通信流
Native MCP 模式(推荐)
AI Client (Claude Code, Cursor)
│ stdio / HTTP Streamable Transport
▼ http://localhost:3000/mcp
FMcpNativeTransport (FRunnable, 原始 socket HTTP 服务器)
│ 解析 JSON-RPC 2.0,管理 SSE 流
▼
UMcpAutomationBridgeSubsystem (UEditorSubsystem)
│ O(1) handler map 查找
▼
McpTool_*.cpp → UE Editor APIs
TypeScript Bridge 模式(传统)
AI Client → TypeScript MCP Server (Node.js) → WebSocket ws://127.0.0.1:8091 → UE Plugin → UE APIs
插件详情
- 类型: Editor-only (
"Type": "Editor"),不是 Runtime - 模块:
McpAutomationBridge,LoadingPhaseDefault - 基类:
UEditorSubsystem - 设置:
UDeveloperSettings子类,在 Project Settings > Plugins > MCP Automation Bridge 中配置 - 依赖: EditorScriptingUtilities, Niagara
- 无 Puerts 依赖:100% 原生 C++
安全特性
- 双向传输的能力令牌认证
- 默认仅 loopback 绑定(可选 LAN)
- 模式匹配的命令安全校验
- 每 IP/socket 速率限制
- TLS/SSL 支持
- 请求边界验证(Zod + AJV schema)
核心 MCP 工具
TypeScript 服务器暴露 22 个合并的 MCP 工具,每个是多 action 的父容器:
| 工具 | 域 |
|---|---|
manage_asset |
资产 CRUD、材质、依赖 |
manage_blueprint |
蓝图、SCS 组件、图表编辑、UMG |
control_actor |
Actor 生成、物理、组件 |
control_editor |
PIE、相机、视口、截图 |
manage_level |
关卡加载/保存、World Partition |
system_control |
UBT、测试、CVars、控制台命令 |
inspect |
对象内省、属性 get/set |
animation_physics |
动画蓝图、Control Rig、物理 |
manage_effect |
Niagara、粒子、调试形状 |
manage_gas |
Gameplay Ability System |
manage_character |
角色创建、移动 |
manage_combat |
武器、弹丸、伤害 |
manage_ai |
AI 控制器、行为树、EQS、感知 |
manage_sequence |
Sequencer、过场动画、关键帧 |
manage_audio |
音效、音频组件 |
manage_networking |
复制、RPC、会话 |
build_environment |
地形、植被 |
manage_level_structure |
子关卡、体积 |
manage_geometry |
程序化网格 |
manage_inventory |
物品、装备、战利品 |
manage_interaction |
可交互对象、触发器 |
manage_tools |
运行时工具启用/禁用 |