--- tags: - UE - Puerts - EasyEditorPlugin - UnrealMCP - MCP - TypeScript - V8 - 架构分析 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`): 1. 创建 `v8::Isolate`,分配默认 ArrayBuffer 分配器 2. 创建 `v8::Context` 存入 `DefaultContext` 3. 将 `this` 指针存为 Isolate 数据:`Isolate->SetData(0, static_cast(this))` ### 2. C++ → JS 桥接注册 构造函数注册 C++ 回调为全局 JavaScript 函数: ```cpp 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 模块(按顺序执行) ```cpp 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 系统: ```javascript // 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`) 搜索算法: 1. 在 `RequiredDir/RequiredModule` 查找(尝试扩展名:`.js`, `.mjs`, `.cjs`, `.mbc`, `.cbc`, `package.json`, `index.js`) 2. 在 `RequiredDir/node_modules/RequiredModule` 查找 3. 向上遍历目录树,搜索每个父目录的 `node_modules` 4. 回退到 `Content/ScriptRoot/RequiredModule`(默认 `Content/JavaScript/`) 5. 如果 ScriptRoot 不是 `"JavaScript"`,也回退到 `Content/JavaScript/` **注意:搜索范围不包括插件 Content 目录。** ## JsEnv 数据流 ### `puerts/uelazyload.js` — UE 类型懒加载 这是最关键的模块之一: 1. 创建全局 `UE` 对象作为 **Proxy**,访问属性时懒加载 UClass 类型 2. 创建全局 `CPP` 对象用于注册的 C++ 类型 3. 将 `UE` 注册为内置模块:`require('ue')` 返回它 4. 暴露:`NewObject`, `NewStruct`, `FNameLiteral`, `NewArray/Set/Map`, `makeUClass`, `mixin`, `blueprint.tojs/load/unload` 5. 定义 UE 反射常量:`FunctionFlags`, `ClassFlags`, `PropertyFlags` 6. 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()` ```cpp 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`,回调 `FJsEnvImpl` - `FunctionToRedirect` — 需要从 C++ 重定向到 JS 的函数名集合 - `HasConstructor` — TS 类是否有 `Constructor` 方法 - `StaticConstructor` — 替换原生 C++ 构造函数 ### `UJSGeneratedFunction` 和 Magic Bytecode `UJSGeneratedFunction` 是动态创建的 `UFunction`,通过 **magic bytecode trick** 重定向到 JS: 1. 在 UFunction 的 `Script` bytecode 中存储自身指针(3 字节 magic marker + 8 字节指针) 2. `execCallJS` 执行时恢复指针 3. 调用 `DynamicInvoker->InvokeJsMethod()` → `FJsEnvImpl::InvokeJsMethod()` 4. `FunctionTranslator->CallJs()` 将所有参数编组到 V8,调用 JS 函数 ### `MakeUClass` / `Mixin` — 运行时类创建 - **`MakeUClass`** (`puerts.makeUClass(ctor)`):接收 JS 构造函数、原型、类名、方法对象、父 UClass,创建 `UJSGeneratedClass`,遍历父类 `FUNC_BlueprintEvent`,为每个覆盖创建 `UJSGeneratedFunction` - **`Mixin`** (`puerts.blueprint.mixin(to, mixinMethods)`):接收现有 UClass,添加方法覆盖 ## C++ 调用 TS 的方式 ### 方式 1:`FJsObject`(直接 JS 函数包装器) ```cpp // FJsObject 是 USTRUCT,持有持久化 V8 引用 FJsObject MyFunc; MyFunc.Action(42, TEXT("hello")); // 无返回值 int32 Result = MyFunc.Func(42, TEXT("hello")); // 有返回值 int32 Val = MyObj.Get("someProperty"); // 读取属性 MyObj.Set("someProperty", 42); // 设置属性 ``` 自动使用 `puerts::v8_impl::Converter` 进行 C++ ↔ V8 类型转换。线程安全(`THREAD_SAFE` 模式下使用 `v8::Locker`)。 ### 方式 2:UFUNCTION / BlueprintEvent 覆盖 标准 UE 模式: ```cpp // C++ 中定义 UFUNCTION(BlueprintImplementableEvent) void OnSomethingHappened(int32 Param); // TypeScript 中实现 class MyActor extends UE.Actor { OnSomethingHappened(Param: number): void { // 被 C++ 调用时执行 } } ``` ### 方式 3:委托 ```typescript 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 模块: ```cpp // 在任何 C++ 文件中 PUERTS_MODULE(MyPlugin, [](v8::Local Context, v8::Local Exports) { // 注册类、函数等 Exports->Set(...); }); ``` 使用静态自动注册: ```cpp #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` | 聚合所有声明(通过 `/// `) | `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 定义 ```json { "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`) ```cpp class FEasyEditorPluginModule : public IModuleInterface { public: virtual void StartupModule() override; virtual void ShutdownModule() override; std::function OnJsEnvPreReload; std::function Eval; TSparseArray> TsConsoleCommands; FSimpleMulticastDelegate OnJsEnvCleanup; private: TSharedPtr JsEnv; // ← 私有!外部无法访问 TSharedPtr SourceFileWatcher; TUniquePtr 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()` 详细分析 ```cpp void FEasyEditorPluginModule::InitJsEnv() { // 1. 创建文件监听器(用于热重载) SourceFileWatcher = MakeShared( [this](const FString& InPath) { if (JsEnv.IsValid()) { TArray 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( std::make_shared(TEXT("EasyEditorScripts")), std::make_shared(), -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` | 重启前执行 | | `Eval` | `std::function` | 执行任意 JS 字符串 | 其他插件的集成方式(以 EasyEditor_ImGui 为例): ```cpp // 注册清理回调 FModuleManager::LoadModuleChecked("EasyEditorPlugin") .OnJsEnvCleanup.AddLambda([]() { // 清理状态 }); ``` ## 脚本组织 - **脚本目录**: `Content/EasyEditorScripts/`(项目 Content 下) - **入口文件**: `Main.js` 或 `Main.ts` - **语言**: TypeScript(主要)/ JavaScript - **热重载**: ✅ 支持 ## 当前缺少的能力 1. **JsEnv 不公开** — 外部 C++ 无法获取 JS 函数引用(`FJsObject`) 2. **ModuleLoader 不灵活** — 只支持项目 Content 目录,不支持插件 Content 目录 3. **Editor-Only** — 无法用于游戏运行时业务逻辑 4. **无返回值调用** — `Eval` 只能执行字符串,无法获取返回值 5. **无结构化的 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`,LoadingPhase `Default` - **基类**: `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` | 运行时工具启用/禁用 |