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

264 lines
9.0 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
- EasyEditorPlugin
- Puerts
- Editor
- TypeScript
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 定义
```json
{
"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`
```cpp
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热重载
│ └── 创建 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 |