Files
BlueRoseNote/07-Other/AI/AI Agent/UnrealEngine/Puerts/Puerts 架构深度分析.md

714 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<IObjectMapper*>(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使用 PESAPIPortable 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<ITsDynamicInvoker>`,回调 `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<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`)。
### 方式 2UFUNCTION / 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<v8::Context> Context, v8::Local<v8::Object> 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` | 聚合所有声明(通过 `/// <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 定义
```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<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热重载
│ └── 创建 FJsEnvModuleLoader 指向 "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<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 为例):
```cpp
// 注册清理回调
FModuleManager::LoadModuleChecked<FEasyEditorPluginModule>("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 架构分析
## 概述
UnrealMCPnpm 包: `unreal-engine-mcp-server`, v0.5.21)是由 ChiR24 开发的开源 MCPModel 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` | 运行时工具启用/禁用 |