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

27 KiB
Raw Blame History

tags, created
tags created
UE
Puerts
EasyEditorPlugin
UnrealMCP
MCP
TypeScript
V8
架构分析
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 函数:

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

搜索算法:

  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.hIPuertsModule 接口
  • 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++ 绑定架构

UTypeScriptGeneratedClassUTypeScriptBlueprint

  • UTypeScriptBlueprint — TypeScript 类的 Blueprint 资产类型
  • UTypeScriptGeneratedClass — TS 生成的运行时 UClass
    • DynamicInvokerTWeakPtr<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 的方式

方式 1FJsObject(直接 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)。

方式 2UFUNCTION / 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 回调函数。

方式 4ITsDynamicInvoker 接口

  • IDynamicInvoker::InvokeJsMethod — 从 UJSGeneratedFunction::execCallJS 调用
  • IDynamicInvoker::JsConstruct — 运行 JS 构造函数
  • ITsDynamicInvoker::TsConstruct — TS 特定构造路径

FJsEnvImpl 通过内部类 DynamicInvokerImplTsDynamicInvokerImpl 实现这些接口。

方式 5PUERTS_MODULEAddon 模块系统)

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.tsue_bp.d.tsDeclarationGenerator 模块使用 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 环境

公开 APIEasyEditorPlugin.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热重载
    │     └── 创建 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() 详细分析

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.jsMain.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
  • 模块: McpAutomationBridgeLoadingPhase 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 运行时工具启用/禁用