目录
- 概述
- 整体架构
- 核心模块详解
- JavaScript 与 C++ 桥接机制
- 对象生命周期管理
- 属性与函数转换系统
- 蓝图与 TypeScript 集成
- 容器类型包装
- 委托系统
- 调试支持
- 性能优化策略
- 总结
1. 概述
1.1 Puerts 是什么
Puerts(Puerts Unreal)是腾讯开源的一个 Unreal Engine 插件,它为 UE 提供了完整的 TypeScript/JavaScript 脚本支持。通过 Puerts,开发者可以:
- 使用 TypeScript 编写游戏逻辑
- 在运行时热更新脚本代码
- 调用 UE 的所有 C++ API 和蓝图函数
- 将 TypeScript 类映射到 UE 蓝图类
1.2 技术选型
Puerts 基于 V8 JavaScript 引擎(也支持 QuickJS 和 Node.js),通过以下技术实现 JS 与 C++ 的互操作:
- V8 API: 直接使用 V8 的 C++ API 进行 JavaScript 对象的创建、属性访问和函数调用
- 反射系统: 利用 UE 的反射系统动态绑定 UClass、UStruct、UFunction 等
- pesapi: 抽象的嵌入式脚本 API,支持跨引擎使用
1.3 插件模块结构
根据 Puerts.uplugin 的定义,插件包含以下模块:
Puerts
- WasmCore - WebAssembly 运行时支持 (PreDefault)
- JsEnv - JavaScript 运行环境核心 (PreDefault)
- DeclarationGenerator - TypeScript 声明文件生成器 (Editor)
- ParamDefaultValueMetas - 参数默认值元数据处理 (Program)
- Puerts - 主模块 (PostEngineInit)
- PuertsEditor - 编辑器扩展 (PostEngineInit)
2. 整体架构
2.1 核心类关系图
FJsEnv (包装器) [Public/JsEnv.h]
|
v
FJsEnvImpl (核心实现) [Private/JsEnvImpl.h]
实现接口:
- IJsEnv: JS 环境主接口
- IObjectMapper: UE 对象映射接口
- FUObjectDeleteListener: UE 对象销毁监听
|
+----+----+
| | |
v v v
FCppObjectMapper FStructWrapper ContainerWrapper
C++对象映射 结构体包装 容器包装
|
v
V8 Isolate & Context (JavaScript 执行环境)
2.2 数据流向
UE Object (UObject) --UE->JS--> JS Object (v8::Object)
^ |
| |
+----------JS->UE------------------+
数据转换层
FPropertyTranslator
FFunctionTranslator
DataTransfer
3. 核心模块详解
3.1 FJsEnv - 环境入口
FJsEnv 是 Puerts 对外暴露的主要接口,它是一个薄包装器,实际的逻辑由 FJsEnvImpl 实现:
主要职责:
- 创建和管理 V8 Isolate(隔离实例)
- 加载 JavaScript 模块
- 提供调试器支持
- 管理垃圾回收
3.2 FJsEnvImpl - 核心实现
FJsEnvImpl 是 Puerts 的心脏,实现了多个关键接口:
关键成员解析:
| 成员 | 类型 | 作用 |
|---|---|---|
| MainIsolate | v8::Isolate* | V8 隔离实例,JS 执行环境 |
| DefaultContext | v8::Global<v8::Context> | 默认执行上下文 |
| ObjectMap | TMap<UObject*, ...> | UE 对象到 JS 对象的映射缓存 |
| StructCache | TMap<void*, ...> | 结构体实例缓存 |
| ContainerCache | TMap<void*, ...> | 容器(Array/Map/Set)缓存 |
| DelegateMap | std::map<void*, ...> | 委托代理映射 |
3.3 IObjectMapper - 对象映射接口
IObjectMapper 定义了 UE 对象与 JS 对象之间映射的抽象接口:
- UObject 绑定: Bind, UnBind, FindOrAdd
- UStruct 绑定: BindStruct, FindOrAddStruct
- 容器绑定: FindOrAddContainer
- 委托绑定: FindOrAddDelegate, NewDelegate
4. JavaScript 与 C++ 桥接机制
4.1 pesapi - 便携式嵌入式脚本 API
Puerts 设计了一个抽象的脚本 API 层 pesapi,使得核心逻辑可以脱离 V8 使用:
设计亮点:
- 版本化 API(PESAPI_VERSION 11)
- 支持动态加载插件
- 跨引擎兼容性
4.2 DataTransfer - 数据传输层
DataTransfer 类提供了高效的数据转换工具:
指针存储策略:
- V8 对象的 InternalField 存储原生指针
- 64位指针拆分为两个32位字段存储
- 提供快速的类型安全访问
4.3 V8Utils - V8 工具类
V8Utils 提供了常用 V8 操作的封装:
- 异常抛出
- 指针获取
- UObject 获取(带有效性检查)
- 字符串转换
- 错误处理
5. 对象生命周期管理
5.1 对象绑定流程
当 UE 对象需要暴露给 JavaScript 时,会经历以下流程:
- 检查缓存
- 获取或创建类模板
- 创建 JS 对象
- 绑定原生指针
- 缓存映射
5.2 垃圾回收回调
当 JavaScript 对象被垃圾回收时,需要通知 C++ 侧:
- 需要释放内存的回调 (OnGarbageCollectedWithFree)
- 不需要释放内存的回调(引用类型)
5.3 UE 对象销毁监听
Puerts 监听 UE 对象的销毁事件,及时清理 JS 侧的引用
5.4 对象缓存节点
为了支持同一指针的不同生命周期管理,Puerts 使用了链表结构的缓存节点
6. 属性与函数转换系统
6.1 FPropertyTranslator - 属性转换器
FPropertyTranslator 是 UE 属性与 JS 值之间转换的核心:
- 工厂方法 - 根据属性类型创建对应转换器
- 核心转换接口: UEToJs, JsToUE
- 属性联合体 - 支持所有 UE 属性类型
- 设置 V8 访问器
派生类示例:
- FNumericPropertyTranslator - 数值属性转换器
- FObjectPropertyTranslator - 对象属性转换器
6.2 FFunctionTranslator - 函数转换器
FFunctionTranslator 处理 UE 函数与 JS 函数的绑定:
- 生成 V8 函数模板
- 调用 JS 函数(从 C++ 到 JS)
- 调用 UE 函数(从 JS 到 C++)
6.3 扩展方法支持
Puerts 支持将蓝图函数库中的静态函数绑定为类型的扩展方法
7. 蓝图与 TypeScript 集成
7.1 TypeScriptGeneratedClass
UTypeScriptGeneratedClass 是连接蓝图与 TypeScript 的桥梁:
- 动态调用器
- 需要重定向的函数列表
- 原生函数指针存储(用于恢复)
- 静态构造函数
- 函数重定向/恢复
7.2 函数重定向机制
当蓝图函数需要由 TypeScript 实现时,会修改 UFunction 的执行入口
7.3 模块绑定流程
当 TypeScript 模块加载时,会执行以下绑定流程:
- 加载 TypeScript 模块
- 获取默认导出的类
- 获取原型对象
- 遍历蓝图函数,检查是否有 TypeScript 实现
7.4 JSGeneratedClass - 动态类生成
Puerts 还支持从 JavaScript 动态创建 UE 类
8. 容器类型包装
8.1 TArray 包装
FScriptArrayWrapper 将 UE 的 TArray 暴露给 JavaScript:
- Add, Get, GetRef, Set
- FindIndex, Contains, RemoveAt
- Empty, Num
8.2 TMap 包装
FScriptMapWrapper 将 UE 的 TMap 暴露给 JavaScript:
- Add, Get, Set, Remove
- GetKey, Empty
8.3 TSet 包装
FScriptSetWrapper 将 UE 的 TSet 暴露给 JavaScript:
- Add, Get, Contains, RemoveAt
8.4 容器扩展类型
为了正确管理容器元素的生命周期,Puerts 定义了扩展容器类型:
- FScriptArrayEx
- FScriptMapEx
- FScriptSetEx
9. 委托系统
9.1 单播委托包装
FDelegateWrapper 处理 UE 的单播委托:
- Bind, IsBound, Unbind, Execute
9.2 多播委托包装
FMulticastDelegateWrapper 处理 UE 的多播委托:
- Add, Remove, Clear, Broadcast
9.3 委托代理
当 JavaScript 函数绑定到 UE 委托时,需要创建代理对象 (UDynamicDelegateProxy)
绑定流程:
- JavaScript 传入回调函数
- 创建 UDynamicDelegateProxy 对象
- 将代理对象的 native 方法绑定到委托
- 委托触发时,代理对象调用 JS 函数
10. 调试支持
10.1 V8 Inspector
Puerts 实现了 V8 Inspector 协议,支持 Chrome DevTools 调试
10.2 调试器等待机制
WaitDebugger 方法支持等待调试器连接,可设置超时
10.3 堆栈追踪
TryCatchToString 方法提供完整的错误堆栈追踪
11. 性能优化策略
11.1 模板缓存
Puerts 缓存 V8 函数模板,避免重复创建
11.2 对象缓存
所有跨语言传递的对象都会被缓存
11.3 快速指针访问
通过 InternalField 直接存储指针,避免属性查找
11.4 函数模板复用
通过宏 PUERTS_REUSE_STRUCTWRAPPER_FUNCTIONTEMPLATE 控制模板复用
11.5 延迟加载
UE 类型按需加载,减少启动时间
12. 总结
12.1 架构亮点
-
分层设计
- pesapi 抽象层:支持多 JS 引擎
- IObjectMapper 接口层:支持多宿主环境
- 具体实现层:V8 + UE 特定实现
-
生命周期管理
- 双向监听:UE 对象销毁 + V8 GC
- 智能缓存:避免重复创建 JS 包装
- 引用计数:正确处理所有权
-
类型系统映射
- 完整支持 UE 反射类型
- 自动属性/函数绑定
- 容器类型透明包装
12.2 关键技术点
| 技术点 | 实现方式 |
|---|---|
| 对象绑定 | V8 InternalField + 指针分割存储 |
| 函数调用 | FFunctionTranslator 参数/返回值转换 |
| 委托绑定 | UDynamicDelegateProxy 代理模式 |
| 类型反射 | UStruct -> v8::FunctionTemplate |
| 调试支持 | V8 Inspector 协议 |
| 热重载 | 模块重新加载 + 函数重定向 |
12.3 源码文件索引
| 文件 | 职责 |
|---|---|
| JsEnv.h/cpp | 环境入口,公共 API |
| JsEnvImpl.h/cpp | 核心实现,对象管理 |
| PropertyTranslator.h/cpp | 属性类型转换 |
| FunctionTranslator.h/cpp | 函数调用转换 |
| StructWrapper.h/cpp | UE 结构体包装 |
| CppObjectMapper.h/cpp | C++ 对象映射 |
| ContainerWrapper.h/cpp | 容器类型包装 |
| DelegateWrapper.h/cpp | 委托类型包装 |
| TypeScriptGeneratedClass.h/cpp | 蓝图集成 |
| DataTransfer.h | 数据传输工具 |
| V8Utils.h/cpp | V8 工具函数 |
| pesapi.h | 便携式脚本 API |
文档版本:基于 Puerts 1.0.5
最后更新:2026-02-25