264 lines
9.0 KiB
Markdown
264 lines
9.0 KiB
Markdown
|
|
---
|
|||
|
|
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(热重载)
|
|||
|
|
│ └── 创建 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<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 |
|