gamelet-puerts-proxy
v2.0.0
Published
TypeScript proxy library for remotely controlling Unity objects via Puerts through WebSocket/RPC
Downloads
577
Readme
gamelet-puerts-proxy 开发者指南
gamelet-puerts-proxy 是一个 TypeScript 库,允许你在 WebView(异步 RPC)或 Puerts 内部 V8(同步直连)中,以完全相同的代码远程操作 Unity 中的 C# 对象。
它通过 Proxy 拦截本地访问,把调用打包成 ReflectPayload,再交给底层 Transport 执行:
| Transport | 运行环境 | 派发方式 | 性能 |
| :--- | :--- | :--- | :--- |
| AsyncTransport (mode=1) | 外部 WebView | external.sendMessage JSON RPC | 受 IPC 延迟影响 |
| NativeTransport (mode=0) | Puerts 同 V8 | 直接读 globalThis.CSharp / globalThis.Puerts | 接近原生 |
业务侧 await 一次写完,两种模式都能跑。
目录
核心差异:从 Puerts 到 gamelet-puerts-proxy
1. 全异步操作
所有与 Unity 交互的操作(方法调用、属性获取、对象创建)都是异步的,必须 await。
| 操作 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 方法调用 | go.SetActive(true) | await go.SetActive(true) |
| 获取返回值 | let x = tf.position | let x = await tf.$get('position') |
| 对象创建 | new Vector3(1,2,3) | await (new Vector3(1,2,3)).$await() |
同步模式(
NativeTransport)下,await一个已 resolve 的 Promise 仅产生纳秒级 microtask 开销,不会引入 IPC 延迟。
2. 属性访问 (Get/Set)
JavaScript Proxy 无法把同步属性读取转换成异步操作,因此属性访问有特殊语法。
- 获取属性:
await proxy.$get('propertyName') - 设置属性:
- fire-and-forget:
proxy.name = 'NewName'(不等待,无返回值,由settrap 走notify) - awaitable:
await proxy.$set('name', 'NewName')(等待 Unity 端执行完成)
- fire-and-forget:
3. 泛型方法
TypeScript 泛型在运行时会被擦除,需要显式传递类型信息。
| 写法 | 含义 |
| :--- | :--- |
| await go.GetComponent(PuertsProxy.$typeof('Animator')) | 推荐:Proxy 自动识别 args 中的 $typeof 标记,剥离为泛型参数 |
| await go.$call('GetComponent', [], undefined, ['UnityEngine.Animator, UnityEngine.AnimationModule']) | 显式:用于复杂重载 |
Transport 配置
ts-proxy 内部抽象出一层 Transport,对外只暴露两个 API:configureTransport 和 getTransport。所有 Reflect 调用最终都走当前 transport,业务代码不需要关心选了哪一个。
1. 模式
| 模式值 | 实现 | 适用场景 |
| :--- | :--- | :--- |
| 0 | NativeTransport | ts-proxy 与 Puerts 同 V8(如 Puerts 内部脚本),直接读 globalThis.CSharp |
| 1 | AsyncTransport | ts-proxy 在 WebView,通过 external.sendMessage 与主进程 Puerts 通信(默认) |
2. 显式配置
import { configureTransport, getTransport } from 'gamelet-puerts-proxy';
// 入口处显式选择模式(推荐在第一次 reflect 调用前调用一次)
configureTransport({ mode: 0 }); // 同步:Puerts 同 V8
configureTransport({ mode: 1 }); // 异步:WebView RPC
// 检查当前 transport
const t = getTransport();
console.log(t.mode); // 'sync' | 'async'行为说明:
- 重复调用、
mode一致 → 直接返回原 transport。 mode变更 → 打 warn 后切换。- 从未调用
configureTransport,第一次getTransport()默认创建AsyncTransport(mode=1)并打日志提示 fallback。 NativeTransport构造时若globalThis.CSharp不存在会抛错,请确保在 Puerts 主机环境下才传mode: 0。
3. ITransport 接口(一般业务无需关心)
interface ITransport {
readonly mode: 'sync' | 'async';
/** 发起 RPC 调用,等待返回值。同步模式下用 Promise.resolve 包装。 */
call(payload: ReflectPayload): Promise<any>;
/** 单向通知,fire-and-forget。 */
notify(payload: ReflectPayload): void;
/** 注册 JS 回调,返回 funcId(异步模式传给后端,同步模式本地映射)。 */
registerCallback(fn: (...args: any[]) => any): string;
}ReflectPayload 字段(双模式共用):
| 字段 | 说明 |
| :--- | :--- |
| op | get / set / has / invoke / create / release / addListener / removeListener |
| type | 完整类型名 Type, Assembly(静态调用 / 创建对象时必填) |
| member | 成员名 |
| target | 实例引用 { $ref, $type } |
| args | 已经过 packArg 的参数列表 |
| genericTypes | 泛型类型参数(AssemblyQualifiedName 数组) |
| paramTypes | 重载消歧用的参数类型 |
| isStatic | 是否静态调用 |
快速开始
1. 初始化
import {
UnityEffectProxy,
PuertsProxy,
configureTransport,
} from 'gamelet-puerts-proxy';
// (可选)显式选择 transport:mode 0 = 同步,1 = 异步
configureTransport({ mode: 1 });
// 获取当前上下文绑定的 GameObject
const go = await UnityEffectProxy.getMountingGameObject(windowId, handle);2. 常用类型
UnityEffectProxy 提供了常用类型的快捷访问,这些都是完整类型代理,可直接调静态方法、new:
const GameObject = UnityEffectProxy.GameObject;
const Vector3 = UnityEffectProxy.Vector3;
const Transform = UnityEffectProxy.Transform;
// 其他:Vector2 / Quaternion / Color
// 不在快捷列表里的类型用 type() 获取
const Debug = UnityEffectProxy.type('UnityEngine.Debug, UnityEngine.CoreModule');3. 完整示例
// 1. 静态方法
await Debug.Log('Hello from Proxy');
// 2. 创建对象(注意 $await)
const vec = await (new UnityEffectProxy.Vector3(0, 1, 0)).$await();
// 3. 实例方法
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
await transform.Translate(vec);
// 4. 泛型方法(args 中的 $typeof 标记会被自动识别为泛型参数)
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
// 5. 属性操作
const pos = await transform.$get('position'); // get:必须用 $get
go.name = 'RenamedObject'; // set:fire-and-forget
await go.$set('name', 'RenamedObject'); // set:等待完成API 参考
UnityEffectProxy
核心代理类,用于创建和管理代理对象。所有方法均为静态。
| 方法 / 属性 | 说明 |
| :--- | :--- |
| getMountingGameObject(windowId, handle) | 获取当前上下文挂载的 GameObject 代理(带缓存) |
| type(assemblyQualifiedName) | 通过完整 Type, Assembly 创建完整类型代理(支持静态方法、new、$get / $set 静态成员) |
| namespace(ns, assembly) | 创建命名空间代理,按需 lazy 创建子类型代理(不缓存) |
| logProxy(label, proxy) | 安全打印代理元数据(ref / type / windowId / handle),不会触发远程调用 |
| clearCache() | 清除所有代理缓存(GameObject 缓存 + 远程对象缓存),调试 / 切场景时使用 |
| call(payload) | 直接走当前 transport 发送原始 ReflectPayload(调试 / 绕过 Proxy) |
| GameObject / Transform / Vector3 / Vector2 / Quaternion / Color | 常用类型的快捷代理(getter,每次返回新代理实例) |
获取类型时,优先使用
UnityEffectProxy.type()或快捷属性。只有它创建的代理才支持静态方法调用。
代理对象实例方法
所有由 getMountingGameObject / 方法返回值 / $get 等产生的实例代理都拥有以下成员:
| 方法 / 属性 | 说明 |
| :--- | :--- |
| $get(member) | 异步获取属性值 |
| $set(member, value) | 异步设置属性值(等待完成) |
| $has(member) | 异步检查成员是否存在 |
| $call(member, args, paramTypes?, genericTypes?) | 显式调用方法(用于重载消歧 / 复杂泛型) |
| $addListener(event, callback) | 添加事件监听 |
| $removeListener(event, callback) | 移除事件监听(必须用同一个函数引用) |
| $release() | 清除该代理的本地缓存并通知 Unity 释放远程引用(只清自己,不递归) |
| $ref (属性) | 远程对象 ID |
| $type (属性) | 远程对象类型名 |
| $raw (属性) | 原始 remote 元数据 { id, type, windowId, handle } |
| $inspect() | 返回完整调试信息对象 |
泛型自动识别:常规方法调用
proxy.Foo(arg1, arg2, $typeof('T'))时,参数列表中的$typeof标记会被自动剥离为genericTypes。需要更精细的控制时改用$call。
PuertsProxy
类型辅助类,主要用于泛型参数传递和类型简写注册。
| 方法 | 说明 |
| :--- | :--- |
| $typeof(shortNameOrFullName) | 创建类型标记(仅用于泛型参数传递)。支持简写 'Animator' 或完整名 'UnityEngine.Animator'。结果带缓存。 |
| registerType(shortName, fullTypeName) | 注册单个简写映射,fullTypeName 必须带 Assembly |
| registerTypes(typeMap) | 批量注册简写映射 |
| hasType(typeName) | 该简写是否已注册 |
| getFullTypeName(typeName) | 解析简写到 AssemblyQualifiedName;含逗号视作已是完整名直接返回;未注册会 fallback 到 , UnityEngine.CoreModule 并 warn |
| getTypeMap() | 返回当前类型映射表副本(调试用) |
| isTypeProxy(obj) | 检查对象是否为 $typeof 产生的类型标记 |
| getTypeName(typeProxy) | 从类型标记中提取类型名(不是标记返回 undefined) |
$typeof()vstype()—— 不要混用:| |
PuertsProxy.$typeof()|UnityEffectProxy.type()| | :--- | :--- | :--- | | 产物 | 类型标记(普通 object,仅带一个 Symbol) | 完整类型代理(Proxy 函数对象) | | 用途 | 仅用于泛型参数传递 | 静态方法 /new/ 静态属性 | | 静态方法 | ❌ 不支持 | ✅ 支持 |// ❌ 错误:$typeof 不支持静态方法 await PuertsProxy.$typeof('GameObject').Find('Player'); // ✅ 正确:用 UnityEffectProxy 的快捷属性 / type() await UnityEffectProxy.GameObject.Find('Player'); // ✅ 正确:$typeof 用于泛型参数 const tr = await go.GetComponent(PuertsProxy.$typeof('Transform'));
Transport 模块
| 方法 / 类型 | 说明 |
| :--- | :--- |
| configureTransport(cfg) | 显式选择 transport,cfg.mode 为 0(同步)或 1(异步) |
| getTransport() | 获取当前 transport;未配置时默认 AsyncTransport(mode=1) |
| ITransport | Transport 接口,含 mode / call / notify / registerCallback |
| AsyncTransport / NativeTransport | 两种内置实现 |
| ReflectPayload | 通用 reflect 操作 payload 类型 |
| TransportModeValue | 字面类型 0 | 1 |
| ProxyTransportConfig | configureTransport 入参类型 |
工具函数
| 函数 | 说明 |
| :--- | :--- |
| enablePuertsLog(enable) | 开关内部 Logger.log(warn / error 始终输出,默认关闭普通日志) |
import { enablePuertsLog } from 'gamelet-puerts-proxy';
enablePuertsLog(true);场景用例对照表
对象创建与构造函数
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 创建 Vector3 | new Vector3(1, 2, 3) | await (new UnityEffectProxy.Vector3(1, 2, 3)).$await() |
| 创建 GameObject | new GameObject('Name') | await (new UnityEffectProxy.GameObject('Name')).$await() |
| 创建自定义类 | new MyClass() | await (new MyType()).$await() (MyType = UnityEffectProxy.type('Ns.MyClass, Asm')) |
const pos = await (new UnityEffectProxy.Vector3(0, 1, 0)).$await();
const go = await (new UnityEffectProxy.GameObject('MyObject')).$await();属性操作
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 获取属性 | go.name | await go.$get('name') |
| 设置属性 | go.name = 'New' | go.name = 'New' (fire-and-forget) |
| 设置属性(等待) | go.name = 'New' | await go.$set('name', 'New') |
| 检查属性存在 | 'transform' in go | await go.$has('transform') |
| 链式属性获取 | go.transform.position.x | 必须分步 $get |
const name = await go.$get('name');
go.name = 'NewName'; // 不等待
await go.$set('name', 'NewName'); // 等待完成
const hasTransform = await go.$has('transform');
// 链式获取(每层都要 await + $get)
const transform = await go.$get('transform');
const position = await transform.$get('position');
const x = await position.$get('x');方法调用
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 实例方法 | go.SetActive(true) | await go.SetActive(true) |
| 静态方法 | GameObject.Find('Player') | await UnityEffectProxy.GameObject.Find('Player') |
| 泛型方法 | go.GetComponent<Transform>() | await go.GetComponent(PuertsProxy.$typeof('Transform')) |
| 重载消歧 | go.SendMessage('Foo', 123) | await go.$call('SendMessage', ['Foo', 123], ['System.String', 'System.Int32']) |
// 实例方法
await go.SetActive(true);
// 静态方法
const player = await UnityEffectProxy.GameObject.Find('Player');
// 泛型方法 —— args 中的 $typeof 标记会被自动识别为泛型参数
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
// 显式调用(用于 property getter / setter / 重载)
const forward = await transform.$call('get_forward');
await transform.$call('set_localPosition', [newPos]);
// 带 paramTypes 的重载消歧
await go.$call('SendMessage', ['MyMethod', 42], ['System.String', 'System.Int32']);组件操作
// 获取组件
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
// 添加组件(未注册简写时,传完整 'Type, Assembly')
const button = await go.AddComponent(PuertsProxy.$typeof('Button'));
// 获取所有组件 + 遍历
const components = await go.GetComponents(PuertsProxy.$typeof('Component'));
const count = await components.$get('Length');
for (let i = 0; i < count; i++) {
const comp = await components.GetValue(i);
const t = await comp.GetType();
const name = await t.$get('Name');
console.log(`Component[${i}]: ${name}`);
}事件监听
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 添加监听 | button.onClick.AddListener(cb) | await button.$addListener('onClick', cb) |
| 移除监听 | button.onClick.RemoveListener(cb) | await button.$removeListener('onClick', cb) |
const button = await go.GetComponent(PuertsProxy.$typeof('Button'));
// 必须保留同一个引用才能 remove
const onClick = async () => console.log('clicked!');
await button.$addListener('onClick', onClick);
await button.$removeListener('onClick', onClick);对象销毁
const UnityObject = UnityEffectProxy.type('UnityEngine.Object, UnityEngine.CoreModule');
await UnityObject.Destroy(go); // 异步销毁
await UnityObject.Destroy(component); // 销毁组件
await UnityObject.DestroyImmediate(go); // 立即销毁子对象操作
const tr = await go.GetComponent(PuertsProxy.$typeof('Transform'));
const parentTr = await parentGo.GetComponent(PuertsProxy.$typeof('Transform'));
await tr.SetParent(parentTr, false); // false = 保持世界坐标
const childCount = await tr.$get('childCount');
const child = await tr.GetChild(0);
const found = await tr.Find('ChildName');类型系统
// 1. 快捷类型(推荐)
const GameObject = UnityEffectProxy.GameObject;
const Vector3 = UnityEffectProxy.Vector3;
// 2. 完整类型代理(任意类型)
const Debug = UnityEffectProxy.type('UnityEngine.Debug, UnityEngine.CoreModule');
const Button = UnityEffectProxy.type('UnityEngine.UI.Button, UnityEngine.UI');
// 3. 命名空间代理(按需 lazy 创建子类型,不缓存)
const UI = UnityEffectProxy.namespace('UnityEngine.UI', 'UnityEngine.UI');
const ButtonType = UI.Button; // 等价于上面的 Button
// 4. 注册自定义类型简写(fullTypeName 必须带 Assembly)
PuertsProxy.registerType(
'PlayerController',
'Game.Controllers.PlayerController, Assembly-CSharp'
);
PuertsProxy.registerTypes({
'EnemyAI': 'Game.AI.EnemyAI, Assembly-CSharp',
'InventorySystem': 'Game.Systems.InventorySystem, Assembly-CSharp',
});
// 5. 用简写传递泛型参数
const ctrl = await go.GetComponent(PuertsProxy.$typeof('PlayerController'));⚠️ 常见错误:
registerType('MyButton', 'UnityEngine.UI.Button')← 缺 Assembly!必须写成'UnityEngine.UI.Button, UnityEngine.UI',否则getFullTypeName会 fallback 到, UnityEngine.CoreModule并报错。
调试技巧
// 安全打印代理元数据(不会触发远程调用)
UnityEffectProxy.logProxy('MyObject', go);
// [PuertsProxy] [MyObject] {"ref":10001,"type":"UnityEngine.GameObject, ...","windowId":1,"handle":2}
// 直接读调试属性
console.log(go.$ref, go.$type, go.$raw);
console.log(go.$inspect()); // { ref, type, windowId, handle }
// 检查 transport 模式
import { getTransport } from 'gamelet-puerts-proxy';
console.log('transport mode:', getTransport().mode);
// 类型注册表
console.log(PuertsProxy.getTypeMap());
// 切场景 / 强制刷新时清缓存
UnityEffectProxy.clearCache();
// 释放单个远程引用(只清自己,不递归)
go.$release();常见问题
Q: console.log(go) 看不到属性?
A: 代理是空的,所有数据都在 Unity 端。用 UnityEffectProxy.logProxy('Label', go) 或 go.$inspect() 查看元数据。
Q: 报错 Cannot read property 'then' of undefined
A: 多半忘了 await,或者直接访问了 go.transform.position(链式访问),改成分步 await proxy.$get('xxx')。
Q: 如何传递回调函数?
A: 直接传 JS 函数即可,packArg 会自动注册并发 funcId。但要 removeListener 必须保留同一个引用:
const cb = async () => console.log('clicked');
await button.$addListener('onClick', cb);
await button.$removeListener('onClick', cb);Q: 泛型方法怎么调用?
A: 用 PuertsProxy.$typeof() 传类型:
const anim = await go.GetComponent<Animator>(); // ❌ 泛型被擦除
const anim = await go.GetComponent(PuertsProxy.$typeof('Animator')); // ✓Q: 如何处理方法重载?
A: 用 $call 显式指定 paramTypes:
await go.$call('SendMessage', ['MyMethod', 42], ['System.String', 'System.Int32']);Q: 属性赋值后如何确认生效?
A: 用 $set 替代直接赋值:
go.name = 'New'; // fire-and-forget,不等待
await go.$set('name', 'New'); // 等待 Unity 端执行完成Q: 数组返回值怎么用?
A: 数组也是代理,用 $get('Length') + GetValue(index):
const arr = await go.GetComponents(PuertsProxy.$typeof('Component'));
const n = await arr.$get('Length');
for (let i = 0; i < n; i++) {
const item = await arr.GetValue(i);
}Q: 同 V8 怎么避免异步开销?
A: 入口处显式 configureTransport({ mode: 0 }),走 NativeTransport。业务代码不用改,await 只剩纳秒级 microtask 开销。
Q: 自定义类型 Method not found 报错?
A: 检查两点:
registerType的 fullTypeName 必须带 Assembly(如, Assembly-CSharp),否则 fallback 到UnityEngine.CoreModule必然解析失败。- 静态方法必须从类型代理调用(
UnityEffectProxy.type()或快捷属性),不能从实例代理调用。
Q: $release() 是递归释放整棵子树吗?
A: 不是。它只清除当前代理在 remoteProxyCache 的 entry 并 notify Unity 释放对应远程引用,子节点引用不受影响。要清空整套缓存用 UnityEffectProxy.clearCache()。
Q: Reflect error: Unknown reflect operation: has 报错?
A: $has 需要 Unity 端 puerts-runtime 支持该 op,请升级 runtime 或改用 $get + 判 null。
