vault backup: 2026-05-31 15:35:41
This commit is contained in:
693
07-Other/AI/AI Agent/UnrealEngine/Puerts/改造方案 - 将业务 TS 代码嵌入插件.md
Normal file
693
07-Other/AI/AI Agent/UnrealEngine/Puerts/改造方案 - 将业务 TS 代码嵌入插件.md
Normal file
@@ -0,0 +1,693 @@
|
||||
---
|
||||
tags:
|
||||
- UE
|
||||
- Puerts
|
||||
- EasyEditorPlugin
|
||||
- 改造方案
|
||||
- TypeScript
|
||||
- 架构
|
||||
created: 2026-05-31
|
||||
---
|
||||
|
||||
# 改造方案:将业务 TS 代码嵌入插件
|
||||
|
||||
## 需求背景
|
||||
|
||||
目前 EasyEditorPlugin 将业务 TypeScript 代码放在**项目的 `Content/EasyEditorScripts/` 目录**中,插件本身只是一个薄桥接层。我们希望能够:
|
||||
|
||||
1. **业务 TS 代码跟随插件分发**(写在插件的 Content/Resources 目录中)
|
||||
2. **项目 C++ 代码直接调用插件中的 TS 函数**(类型安全,有返回值)
|
||||
3. **项目代码只需最少量的调用**(LoadModule + 少量 API 调用)
|
||||
|
||||
## 可行性评估
|
||||
|
||||
| 需求 | 可行性 | 改造量 |
|
||||
|------|--------|--------|
|
||||
| TS 代码嵌入插件 Content | ✅ 可行,自定义 IJSModuleLoader | 小(~50 行 C++) |
|
||||
| C++ 调用插件 TS 函数 | ✅ 可行,暴露 FJsObject 获取 API | 中(~100 行 C++) |
|
||||
| TS 代码热重载 | ✅ 已有基础设施,仅需调整路径 | 无需改动 |
|
||||
| 运行时(非 Editor)支持 | ✅ 可行,但需创建新 Runtime 模块 | 大(架构变更) |
|
||||
|
||||
## 方案一:改造 EasyEditorPlugin(编辑器工具场景)
|
||||
|
||||
适用场景:**编辑器工具扩展**,业务代码是编辑器工具逻辑(如资产处理、关卡自动化等),不需要在打包后的游戏中运行。
|
||||
|
||||
### 改造点 1:自定义 IJSModuleLoader,从插件 Content 加载
|
||||
|
||||
#### 当前代码
|
||||
|
||||
```cpp
|
||||
// EasyEditorPlugin.cpp InitJsEnv()
|
||||
JsEnv = MakeShared<puerts::FJsEnv>(
|
||||
std::make_shared<puerts::DefaultJSModuleLoader>(TEXT("EasyEditorScripts")),
|
||||
...);
|
||||
```
|
||||
|
||||
**问题**:`DefaultJSModuleLoader` 只在项目 Content 目录搜索,不会搜索插件目录。
|
||||
|
||||
#### 改造后
|
||||
|
||||
新建 `PluginJSModuleLoader.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "JSModuleLoader.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
class FPluginJSModuleLoader : public PUERTS_NAMESPACE::IJSModuleLoader
|
||||
{
|
||||
public:
|
||||
explicit FPluginJSModuleLoader(const FString& InPluginContentSubDir)
|
||||
: ScriptRoot(InPluginContentSubDir)
|
||||
{
|
||||
// 获取插件 Content 目录
|
||||
// EasyEditorPlugin 中: FPaths::ProjectPluginsDir() / "EasyEditorPlugin/Content"
|
||||
PluginContentDir = DeterminePluginContentDir();
|
||||
}
|
||||
|
||||
virtual bool Search(const FString& RequiredDir, const FString& RequiredModule,
|
||||
FString& Path, FString& AbsolutePath) override
|
||||
{
|
||||
// 搜索顺序:
|
||||
// 1. 先在插件的 Content/ScriptRoot/ 下搜索
|
||||
// 2. 再走默认的 DefaultJSModuleLoader 逻辑(项目 Content、node_modules 等)
|
||||
|
||||
// 插件目录搜索
|
||||
FString PluginDir = PluginContentDir / ScriptRoot;
|
||||
if (SearchModuleInDir(PluginDir, RequiredModule, Path, AbsolutePath))
|
||||
return true;
|
||||
|
||||
// 回退到默认搜索逻辑
|
||||
return DefaultLoader.Search(RequiredDir, RequiredModule, Path, AbsolutePath);
|
||||
}
|
||||
|
||||
virtual bool Load(const FString& Path, TArray<uint8>& Content) override
|
||||
{
|
||||
return DefaultLoader.Load(Path, Content);
|
||||
}
|
||||
|
||||
virtual FString& GetScriptRoot() override
|
||||
{
|
||||
return ScriptRoot;
|
||||
}
|
||||
|
||||
static FString DeterminePluginContentDir()
|
||||
{
|
||||
// 获取当前插件自身的 Content 目录
|
||||
// 方案 A:通过 IPluginManager 查询
|
||||
TSharedPtr<IPlugin> Plugin = IPluginManager::Get()
|
||||
.FindPlugin(TEXT("EasyEditorPlugin"));
|
||||
if (Plugin.IsValid())
|
||||
{
|
||||
return Plugin->GetContentDir();
|
||||
}
|
||||
|
||||
// 方案 B:手动拼路径(兼容旧版本)
|
||||
return FPaths::ProjectPluginsDir() / TEXT("EasyEditorPlugin/Content");
|
||||
}
|
||||
|
||||
private:
|
||||
FString ScriptRoot;
|
||||
FString PluginContentDir;
|
||||
PUERTS_NAMESPACE::DefaultJSModuleLoader DefaultLoader;
|
||||
|
||||
bool SearchModuleInDir(const FString& Dir, const FString& RequiredModule,
|
||||
FString& Path, FString& AbsolutePath)
|
||||
{
|
||||
// 尝试扩展名: .js, .mjs, .cjs, .mbc, .cbc, /index.js, package.json
|
||||
static const TArray<FString> Extensions = {
|
||||
TEXT(".js"), TEXT(".mjs"), TEXT(".cjs"),
|
||||
TEXT(".mbc"), TEXT(".cbc")
|
||||
};
|
||||
|
||||
for (const auto& Ext : Extensions)
|
||||
{
|
||||
FString TestPath = Dir / RequiredModule + Ext;
|
||||
if (FPaths::FileExists(TestPath))
|
||||
{
|
||||
Path = RequiredModule + Ext;
|
||||
AbsolutePath = FPaths::ConvertRelativePathToFull(TestPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试目录 + index.js
|
||||
FString IndexPath = Dir / RequiredModule / TEXT("index.js");
|
||||
if (FPaths::FileExists(IndexPath))
|
||||
{
|
||||
Path = RequiredModule / TEXT("index.js");
|
||||
AbsolutePath = FPaths::ConvertRelativePathToFull(IndexPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 修改 InitJsEnv()
|
||||
|
||||
```cpp
|
||||
void FEasyEditorPluginModule::InitJsEnv()
|
||||
{
|
||||
SourceFileWatcher = MakeShared<puerts::FSourceFileWatcher>(
|
||||
[this](const FString& InPath) { /* 同上 */ });
|
||||
|
||||
// 使用自定义 Loader,从插件 Content/BusinessScripts/ 加载
|
||||
JsEnv = MakeShared<puerts::FJsEnv>(
|
||||
std::make_shared<FPluginJSModuleLoader>(TEXT("BusinessScripts")),
|
||||
std::make_shared<puerts::FDefaultLogger>(), -1,
|
||||
[this](const FString& InPath) { /* 同上 */ });
|
||||
}
|
||||
```
|
||||
|
||||
#### 插件目录结构变为
|
||||
|
||||
```
|
||||
EasyEditorPlugin/
|
||||
├── Content/
|
||||
│ └── BusinessScripts/ ← TS 业务代码(随插件分发)
|
||||
│ ├── Main.ts ← 入口
|
||||
│ ├── commands/
|
||||
│ │ ├── AssetCommands.ts
|
||||
│ │ └── LevelCommands.ts
|
||||
│ ├── ui/
|
||||
│ │ ├── MenuSetup.ts
|
||||
│ │ └── DetailPanel.ts
|
||||
│ └── utils/
|
||||
│ └── helpers.ts
|
||||
├── Source/
|
||||
│ └── ...
|
||||
└── EasyEditorPlugin.uplugin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 改造点 2:暴露 JsEnv,支持 C++ 类型安全调用 TS
|
||||
|
||||
#### 问题
|
||||
|
||||
```cpp
|
||||
class FEasyEditorPluginModule {
|
||||
private:
|
||||
TSharedPtr<puerts::FJsEnv> JsEnv; // ← 外部无法访问
|
||||
};
|
||||
```
|
||||
|
||||
外部 C++ 只能通过 `Eval` 执行 JS 字符串,无类型安全、无返回值。
|
||||
|
||||
#### 改造:公开 JsEnv 访问和 FJsObject 获取
|
||||
|
||||
在 `EasyEditorPlugin.h` 中新增:
|
||||
|
||||
```cpp
|
||||
class FEasyEditorPluginModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
// ===== 新增 API =====
|
||||
|
||||
/**
|
||||
* 从 JS 全局作用域获取一个函数/对象,返回 FJsObject 用于类型安全调用
|
||||
* 示例:
|
||||
* auto& Mod = FModuleManager::LoadModuleChecked<FEasyEditorPluginModule>("EasyEditorPlugin");
|
||||
* FJsObject MyFunc = Mod.GetJsExport("myBusinessFunction");
|
||||
* int32 Result = MyFunc.Func<int32, int32, FString>(42, TEXT("hello"));
|
||||
*/
|
||||
FJsObject GetJsExport(const FString& Name) const;
|
||||
|
||||
/**
|
||||
* 从 JS 模块(已 require 的)中获取一个导出
|
||||
* 示例:
|
||||
* FJsObject Handler = Mod.GetJsModuleExport("commands/AssetCommands", "handleAsset");
|
||||
* Handler.Action(TEXT("/Game/MyAsset"));
|
||||
*/
|
||||
FJsObject GetJsModuleExport(const FString& ModuleName, const FString& ExportName) const;
|
||||
|
||||
/**
|
||||
* 获取底层的 IJsEnv 接口(高级用法)
|
||||
*/
|
||||
TSharedPtr<puerts::FJsEnv> GetJsEnv() const { return JsEnv; }
|
||||
|
||||
// ===== 原有 API =====
|
||||
std::function<void()> OnJsEnvPreReload;
|
||||
std::function<void(const FString&)> Eval;
|
||||
FSimpleMulticastDelegate OnJsEnvCleanup;
|
||||
TSparseArray<TUniquePtr<FAutoConsoleCommand>> TsConsoleCommands;
|
||||
|
||||
private:
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
#### 实现(`EasyEditorPlugin.cpp`)
|
||||
|
||||
```cpp
|
||||
FJsObject FEasyEditorPluginModule::GetJsExport(const FString& Name) const
|
||||
{
|
||||
if (!JsEnv.IsValid())
|
||||
{
|
||||
UE_LOG(Puerts, Error, TEXT("JsEnv is not valid when getting export '%s'"), *Name);
|
||||
return {};
|
||||
}
|
||||
|
||||
// 通过 Eval 机制获取全局变量
|
||||
// 实际实现需要利用 Puerts 的内部 API
|
||||
// 方案见下方"实现细节"
|
||||
}
|
||||
|
||||
FJsObject FEasyEditorPluginModule::GetJsModuleExport(
|
||||
const FString& ModuleName, const FString& ExportName) const
|
||||
{
|
||||
if (!JsEnv.IsValid())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// 需要从模块缓存中查找
|
||||
// 方案见下方"实现细节"
|
||||
}
|
||||
```
|
||||
|
||||
#### 实现细节:如何从 JsEnv 中获取 JS 对象
|
||||
|
||||
**核心问题**:`FJsEnv` 的 `FJsEnvImpl` 成员是私有的,没有公开的 "通过名称获取 JS 变量" API。需要通过以下方式实现:
|
||||
|
||||
**方案 A:通过 `puerts::FJsEnv` 包装器暴露新方法**
|
||||
|
||||
需要在 Puerts 的 `FJsEnv` 中新增一个方法:
|
||||
|
||||
```cpp
|
||||
// 在 JsEnv.h 中新增
|
||||
class JSENV_API FJsEnv
|
||||
{
|
||||
public:
|
||||
// ... 现有 API ...
|
||||
|
||||
/**
|
||||
* 获取全局作用域中的一个值
|
||||
* @param Name 全局变量/函数名
|
||||
* @return 包装了该 JS 值的 FJsObject,如果不存在或类型不对则返回空
|
||||
*/
|
||||
FJsObject GetGlobalValue(const FString& Name);
|
||||
};
|
||||
```
|
||||
|
||||
实现(在 `JsEnvImpl.cpp` 中):
|
||||
|
||||
```cpp
|
||||
FJsObject FJsEnv::GetGlobalValue(const FString& Name)
|
||||
{
|
||||
if (!GameScript) return {};
|
||||
|
||||
auto Impl = static_cast<FJsEnvImpl*>(GameScript.get());
|
||||
auto Isolate = Impl->MainIsolate;
|
||||
auto Context = Impl->DefaultContext.Get(Isolate);
|
||||
|
||||
#ifdef THREAD_SAFE
|
||||
v8::Locker Locker(Isolate);
|
||||
#endif
|
||||
v8::Isolate::Scope IsolateScope(Isolate);
|
||||
v8::HandleScope HandleScope(Isolate);
|
||||
v8::Context::Scope ContextScope(Context);
|
||||
|
||||
auto Global = Context->Global();
|
||||
auto V8Name = FV8Utils::ToV8String(Isolate, Name);
|
||||
auto MaybeValue = Global->Get(Context, V8Name);
|
||||
|
||||
v8::Local<v8::Value> Val;
|
||||
if (MaybeValue.ToLocal(&Val) && Val->IsObject())
|
||||
{
|
||||
return FJsObject(Context, Val.As<v8::Object>());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
但这种方式需要 **修改 Puerts 源码**(在 `JsEnv.h` 和 `JsEnvImpl.cpp` 中增加代码),对于已经安装的 Puerts 插件不太方便。
|
||||
|
||||
**方案 B:在 EasyEditorPlugin 中注入 JS 辅助函数(推荐,无需修改 Puerts)**
|
||||
|
||||
思路:在 `InitJsEnv()` 时,向 JS 全局注入一个 `__eep_getExport` 函数,同时在 C++ 中存储引用。
|
||||
|
||||
```cpp
|
||||
// EasyEditorPlugin.cpp
|
||||
|
||||
// 存储注入的 JS 辅助函数
|
||||
static FJsObject G_GetExportFunc;
|
||||
|
||||
void FEasyEditorPluginModule::InitJsEnv()
|
||||
{
|
||||
// ... 创建 JsEnv ...
|
||||
|
||||
JsEnv->Start(TEXT("Main"));
|
||||
|
||||
// Main.ts 执行后,获取注入的辅助函数引用
|
||||
// Main.ts 中需要暴露:
|
||||
// global.__eep_getExport = (name) => { ... return eval(name); }
|
||||
// global.__eep_getModuleExport = (moduleName, exportName) => { ... }
|
||||
}
|
||||
```
|
||||
|
||||
但这形成了一个鸡生蛋蛋生鸡的问题 —— 在 Main.ts 执行前无法获取引用。
|
||||
|
||||
**方案 C:使用 Puerts 的 JIT stub 机制(最务实)**
|
||||
|
||||
在 EasyEditorPlugin 的绑定代码中注册可被 `require()` 调用的 C++ 函数:
|
||||
|
||||
```cpp
|
||||
// 在 EasyEditorPlugin.cpp 的 AutoRegisterForEEP 中新增绑定:
|
||||
|
||||
struct FEasyEditorBridge
|
||||
{
|
||||
static puerts::Function GetGlobalFunction;
|
||||
static FJsObject GetGlobalObject;
|
||||
|
||||
// 注册到 Puerts binding
|
||||
};
|
||||
```
|
||||
|
||||
然后在 Main.ts 中:
|
||||
|
||||
```typescript
|
||||
// 将需要暴露给 C++ 的函数注册到 global 对象
|
||||
import { MyBusinessLogic } from './business/BusinessLogic';
|
||||
|
||||
(global as any).__exports = {
|
||||
processAsset: MyBusinessLogic.processAsset,
|
||||
validateLevel: MyBusinessLogic.validateLevel,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
C++ 侧通过 Eval 获取:
|
||||
|
||||
```cpp
|
||||
FJsObject FEasyEditorPluginModule::GetJsExport(const FString& Name) const
|
||||
{
|
||||
// 利用 Eval 机制代理
|
||||
// 实际方案:在 V8 中执行 "__exports[Name]",捕获结果
|
||||
// 但当前 Eval 是 void 返回的...
|
||||
|
||||
// 需要改造 Eval 为有返回值的版本,见下文
|
||||
}
|
||||
```
|
||||
|
||||
**方案 D:改造 Eval 为有返回值的版本(最小侵入)**
|
||||
|
||||
```cpp
|
||||
// 在 EasyEditorPlugin.h 中
|
||||
class FEasyEditorPluginModule
|
||||
{
|
||||
public:
|
||||
// 替代原来的 void Eval,使用可返回值的版本
|
||||
std::function<FJsObject(const FString&)> EvalWithReturn;
|
||||
};
|
||||
```
|
||||
|
||||
在 `InitJsEnv()` 中注册对应的 C++ 绑定:
|
||||
|
||||
```cpp
|
||||
// 利用 Puerts 的 FV8Utils 实现有返回值的 Eval
|
||||
// 在 C++ 侧: 持有 v8::Isolate 和 v8::Context,直接 eval 并封装结果
|
||||
```
|
||||
|
||||
**方案 E:最简单且实用的方案 —— 通过 Console 命令 + 注册回调**
|
||||
|
||||
不暴露 JsEnv,而是让项目 C++ 注册一个回调,TS 通过 `EasyEditorPlugin.AddConsoleCommand` 或其他绑定来调用它:
|
||||
|
||||
```cpp
|
||||
// 项目 C++ 代码中使用 EasyEditorPlugin
|
||||
void FMyProjectModule::StartupModule()
|
||||
{
|
||||
auto& EEP = FModuleManager::LoadModuleChecked<FEasyEditorPluginModule>("EasyEditorPlugin");
|
||||
|
||||
// 在 JS 环境就绪后注册
|
||||
EEP.OnJsEnvCreated.AddLambda([this]() {
|
||||
// 告诉 TS 侧:C++ 准备好了
|
||||
EEP.Eval(TEXT("if (global.__onCppReady) global.__onCppReady()"));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
但这种模式是 **TS 调用 C++**,而非用户需求的 **C++ 调用 TS**。
|
||||
|
||||
---
|
||||
|
||||
### 推荐的 C++ → TS 调用方案总结
|
||||
|
||||
| 方案 | 侵入性 | 类型安全 | 需要修改 Puerts | 推荐度 |
|
||||
|------|--------|---------|----------------|--------|
|
||||
| A: 扩展 FJsEnv::GetGlobalValue | 低 | ✅ | ❌ 需要 | ⭐⭐⭐ |
|
||||
| B: 注入 JS 辅助函数 | 中 | ✅ | 不需要 | ⭐⭐ |
|
||||
| C: require() 注册 | 中 | ✅ | 不需要 | ⭐⭐ |
|
||||
| D: Eval 改造成有返回值 | 低 | ❌ 需手动转换 | 不需要 | ⭐⭐ |
|
||||
| E: 回调注册 | 低 | ❌ | 不需要 | ⭐(反向模式) |
|
||||
|
||||
**最佳实践:方案 C(PUERTS_MODULE addon)+ 方案 A(扩展 FJsEnv)**
|
||||
|
||||
在实际项目中,建议**对 Puerts 做少量扩展**(fork 或 patch),因为 Puerts 本身设计就预留了扩展点。
|
||||
|
||||
---
|
||||
|
||||
## 方案二:基于 Puerts 创建自己的 Runtime 插件(运行时代码场景)
|
||||
|
||||
如果业务代码需要在**打包后的游戏**中运行(Gameplay 逻辑、AI、战斗系统等),EasyEditorPlugin 的 Editor-only 限制是致命的。
|
||||
|
||||
此时正确的做法是:**直接基于 Puerts 创建自己的 Runtime 插件**。
|
||||
|
||||
### 插件骨架
|
||||
|
||||
```
|
||||
MyBusinessPlugin/
|
||||
├── MyBusinessPlugin.uplugin
|
||||
├── Content/
|
||||
│ └── BusinessScripts/ ← TS 业务代码
|
||||
│ ├── Main.ts ← 入口
|
||||
│ ├── combat/
|
||||
│ ├── ai/
|
||||
│ └── ui/
|
||||
└── Source/
|
||||
└── MyBusinessPlugin/
|
||||
├── MyBusinessPlugin.Build.cs
|
||||
├── Public/
|
||||
│ └── MyBusinessPluginModule.h
|
||||
│ └── BusinessAPI.h ← 公开给项目 C++ 的 API
|
||||
└── Private/
|
||||
├── MyBusinessPluginModule.cpp
|
||||
└── MyBinding.cpp ← PUERTS_MODULE 注册
|
||||
```
|
||||
|
||||
### .uplugin
|
||||
|
||||
```json
|
||||
{
|
||||
"FriendlyName": "MyBusinessPlugin",
|
||||
"Modules": [{
|
||||
"Name": "MyBusinessPlugin",
|
||||
"Type": "Runtime", // ← Runtime!可在打包游戏中运行
|
||||
"LoadingPhase": "Default"
|
||||
}],
|
||||
"Plugins": [{
|
||||
"Name": "Puerts",
|
||||
"Enabled": true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Build.cs
|
||||
|
||||
```csharp
|
||||
PublicDependencyModuleNames.AddRange(new string[] {
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"JsEnv" // ← 依赖 Puerts 的 JsEnv
|
||||
});
|
||||
```
|
||||
|
||||
### `MyBusinessPluginModule.h`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "JsEnv.h"
|
||||
#include "JsObject.h"
|
||||
|
||||
class FMyBusinessPluginModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
// 公开给项目 C++ 的 API
|
||||
FJsObject GetExport(const FString& Name) const;
|
||||
TSharedPtr<puerts::FJsEnv> GetJsEnv() const { return JsEnv; }
|
||||
|
||||
private:
|
||||
TSharedPtr<puerts::FJsEnv> JsEnv;
|
||||
void InitJsEnv();
|
||||
void UnInitJsEnv();
|
||||
};
|
||||
```
|
||||
|
||||
### `MyBusinessPluginModule.cpp`
|
||||
|
||||
```cpp
|
||||
#include "MyBusinessPluginModule.h"
|
||||
#include "JSModuleLoader.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
|
||||
void FMyBusinessPluginModule::StartupModule()
|
||||
{
|
||||
InitJsEnv();
|
||||
}
|
||||
|
||||
void FMyBusinessPluginModule::ShutdownModule()
|
||||
{
|
||||
UnInitJsEnv();
|
||||
}
|
||||
|
||||
void FMyBusinessPluginModule::InitJsEnv()
|
||||
{
|
||||
// 获取插件 Content 目录
|
||||
TSharedPtr<IPlugin> Plugin = IPluginManager::Get()
|
||||
.FindPlugin(TEXT("MyBusinessPlugin"));
|
||||
FString PluginContent = Plugin.IsValid()
|
||||
? Plugin->GetContentDir()
|
||||
: FPaths::ProjectPluginsDir() / TEXT("MyBusinessPlugin/Content");
|
||||
|
||||
// 创建自定义 ModuleLoader,从插件 Content/BusinessScripts/ 加载
|
||||
auto Loader = MakeShared<FPluginJSModuleLoader>(
|
||||
PluginContent / TEXT("BusinessScripts"));
|
||||
|
||||
JsEnv = MakeShared<puerts::FJsEnv>(
|
||||
Loader,
|
||||
MakeShared<puerts::FDefaultLogger>(),
|
||||
-1, // debugPort, -1 = 禁用调试
|
||||
nullptr,
|
||||
FString(),
|
||||
nullptr, nullptr);
|
||||
|
||||
// 启动业务代码入口
|
||||
JsEnv->Start(TEXT("Main"));
|
||||
}
|
||||
|
||||
void FMyBusinessPluginModule::UnInitJsEnv()
|
||||
{
|
||||
JsEnv.Reset();
|
||||
}
|
||||
|
||||
FJsObject FMyBusinessPluginModule::GetExport(const FString& Name) const
|
||||
{
|
||||
// 实现同方案一的讨论
|
||||
// 推荐方式: 对 Puerts 做 patch,给 FJsEnv 加 GetGlobalValue
|
||||
}
|
||||
```
|
||||
|
||||
### `BusinessAPI.h` — 给项目 C++ 的调用接口
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "JsObject.h"
|
||||
|
||||
namespace BusinessAPI
|
||||
{
|
||||
// 获取插件的 JsEnv
|
||||
TSharedPtr<PUERTS_NAMESPACE::FJsEnv> GetBusinessJsEnv();
|
||||
|
||||
// 获取 JS 导出函数
|
||||
PUERTS_NAMESPACE::FJsObject GetExport(const FString& Name);
|
||||
|
||||
// 便捷调用封装
|
||||
inline int32 ValidateLevel(const FString& LevelPath)
|
||||
{
|
||||
auto Func = GetExport(TEXT("validateLevel"));
|
||||
if ( /* Func is valid */ )
|
||||
{
|
||||
return Func.Func<int32, FString>(LevelPath);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 项目 C++ 调用方式
|
||||
|
||||
```cpp
|
||||
// 项目 C++ 代码 —— 极简调用
|
||||
void AMyGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 加载插件(自动启动 TS 业务逻辑)
|
||||
FModuleManager::Get().LoadModule("MyBusinessPlugin");
|
||||
|
||||
// 调用 TS 函数
|
||||
auto Func = BusinessAPI::GetExport(TEXT("onGameStart"));
|
||||
Func.Action(this); // 传递 UObject,自动转换为 TS 侧 UE 类型
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 热重载支持
|
||||
|
||||
无论方案一还是方案二,热重载都是可用的。只需确保 `FSourceFileWatcher` 监听的是正确的目录:
|
||||
|
||||
```cpp
|
||||
// 插件 Content 目录下的 BusinessScripts/
|
||||
FString WatchDir = PluginContentDir / TEXT("BusinessScripts");
|
||||
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()));
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改造清单
|
||||
|
||||
### 最小改造(编辑器工具,方案一)
|
||||
|
||||
| 步骤 | 文件 | 改动 |
|
||||
|------|------|------|
|
||||
| 1 | 新建 `PluginJSModuleLoader.h` | 自定义模块加载器,从插件 Content 搜索 |
|
||||
| 2 | `EasyEditorPlugin.cpp:InitJsEnv()` | 切换为 `FPluginJSModuleLoader` |
|
||||
| 3 | `EasyEditorPlugin.h` | 添加 `GetJsEnv()` 和 `GetJsExport()` |
|
||||
| 4 | 插件 `Content/BusinessScripts/` | 移入 TS 业务代码 |
|
||||
| 5 | (可选) Puerts `JsEnv.h/cpp` patch | 添加 `GetGlobalValue()` 方法 |
|
||||
|
||||
### 完整改造(Runtime 支持,方案二)
|
||||
|
||||
| 步骤 | 文件 | 改动 |
|
||||
|------|------|------|
|
||||
| 1 | 创建新插件 `MyBusinessPlugin` | `.uplugin`, `Build.cs`, 目录结构 |
|
||||
| 2 | `PluginJSModuleLoader.h`(复用) | 自定义模块加载器 |
|
||||
| 3 | `MyBusinessPluginModule.h/cpp` | 模块实现,参考 EasyEditorPlugin |
|
||||
| 4 | `BusinessAPI.h` | 公开 C++ 调用 API |
|
||||
| 5 | 插件 `Content/BusinessScripts/` | 移入 TS 业务代码 |
|
||||
| 6 | Puerts `JsEnv.h/cpp` patch | 添加 `GetGlobalValue()` 方法 |
|
||||
| 7 | 项目 `.uproject` | 添加插件依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **线程安全**:FJsObject 在 `THREAD_SAFE` 模式下自动加锁,但建议所有 TS 调用都在 GameThread 进行
|
||||
2. **JsEnv 生命周期**:确保在 `ShutdownModule()` 中正确销毁 JsEnv,避免 V8 isolate 泄漏
|
||||
3. **GC 联动**:参考 EasyEditorPlugin 的地图卸载时 GC 处理,避免 "GC Memory Leak" 断言
|
||||
4. **模块解析**:自定义 `IJSModuleLoader` 需要正确实现 `Search` 和 `Load` 两个方法
|
||||
5. **Puerts 版本**:不同 Puerts 版本的 V8 API 可能不同(V8 8.x vs 11.x),注意 `V8_MAJOR_VERSION` 条件编译
|
||||
Reference in New Issue
Block a user