9.0 KiB
9.0 KiB
tags, created
| tags | created | |||||
|---|---|---|---|---|---|---|
|
2026-05-31 |
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", // ← 仅在 Unreal Editor 中加载
"LoadingPhase": "Default"
}],
"Plugins": [{
"Name": "Puerts",
"Enabled": true // ← 硬依赖 Puerts
}]
}
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; // JS 环境重启前回调
std::function<void(const FString&)> Eval; // 执行 JS 字符串
TSparseArray<TUniquePtr<FAutoConsoleCommand>> TsConsoleCommands; // TS 注册的控制台命令
FSimpleMulticastDelegate OnJsEnvCleanup; // JS 环境清理广播
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 |