gamelet-puerts-proxy
v1.0.0
Published
TypeScript proxy library for remotely controlling Unity objects via Puerts through WebSocket/RPC
Readme
gamelet-puerts-proxy 开发者指南
gamelet-puerts-proxy 是一个 TypeScript 库,允许你在后台(或其他非 Unity 环境)通过 WebSocket/RPC 远程操作 Unity 中的对象。它通过 Proxy 技术拦截本地调用,将其转换为 JSON 消息发送给 Unity 端的 puerts-runtime 执行。
核心差异:从 Puerts 到 gamelet-puerts-proxy
如果你熟悉直接在 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() |
2. 属性访问 (Get/Set)
JavaScript 的 Proxy 无法拦截同步的属性读取并将其转换为异步操作,因此属性访问有特殊语法。
- 获取属性 (Get): 必须使用
$get('propertyName')。 - 设置属性 (Set):
- Fire-and-forget: 直接赋值
go.name = "NewName"(不等待完成,无返回值)。 - Awaitable: 使用
await go.$set('name', "NewName")(等待 Unity 端执行完成)。
- Fire-and-forget: 直接赋值
3. 泛型方法
TypeScript 的泛型在运行时会被擦除,因此需要显式传递类型信息。
- 原生:
go.GetComponent<Animator>() - Proxy:
await go.GetComponent(PuertsProxy.$typeof('Animator'))
快速开始
1. 初始化
通常由框架负责初始化连接,你只需要获取入口对象(如挂载点 GameObject)。
import { UnityEffectProxy, PuertsProxy } from 'gamelet-puerts-proxy';
// 获取当前上下文绑定的 GameObject
const go = await UnityEffectProxy.getMountingGameObject(windowId, handle);2. 常用类型
UnityEffectProxy 提供了常用类型的快捷访问:
const GameObject = UnityEffectProxy.GameObject;
const Vector3 = UnityEffectProxy.Vector3;
const Transform = UnityEffectProxy.Transform;
const Debug = UnityEffectProxy.type('UnityEngine.Debug, UnityEngine.CoreModule');3. 完整示例
// 1. 静态方法
await Debug.Log("Hello from Proxy");
// 2. 创建对象 (注意 $await)
const vec = await (new Vector3(0, 1, 0)).$await();
// 3. 实例方法
await go.transform.Translate(vec);
// 4. 获取组件 (泛型)
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
// 5. 属性操作
// Get: 必须用 $get
const pos = await go.transform.$get('position');
// Set: 可以直接赋值
go.name = "RenamedObject";API 参考
UnityEffectProxy
核心代理类,用于创建和管理代理对象。
getMountingGameObject(windowId, handle): 获取当前挂载的 GameObject。type(assemblyQualifiedName): 获取任意 C# 类型的完整类型代理(支持静态方法调用、静态属性访问)。namespace(ns, assembly): 创建命名空间代理。logProxy(label, proxy): 打印代理对象的调试信息(ID, 类型等)。- 快捷类型属性:
GameObject,Transform,Vector3,Vector2,Quaternion,Color- 用于静态方法调用和对象创建。
提示: 获取类型优先使用
UnityEffectProxy.type()或快捷属性(如UnityEffectProxy.GameObject),支持静态方法调用。
代理对象实例方法 (Instance Methods)
所有代理对象(GameObject, Component 等)都拥有以下特殊方法:
$get(propName): 异步获取属性值。$set(propName, value): 异步设置属性值。$has(propName): 异步检查属性是否存在。$call(methodName, args, paramTypes?, genericTypes?): 显式调用方法(用于处理重载或复杂泛型)。$addListener(event, callback): 添加事件监听。$removeListener(event, callback): 移除事件监听。$release(): 释放远程对象引用(清除缓存并通知 Unity)。$ref: (属性) 获取远程对象的 ID。$type: (属性) 获取远程对象的类型名。$raw: (属性) 获取原始远程对象数据。$inspect(): 获取完整调试信息。
PuertsProxy
类型辅助类,主要用于泛型参数传递。
$typeof(shortNameOrFullName): 创建类型标记,仅用于泛型参数传递(如GetComponent)。- 支持简写:
Animator - 支持全名:
UnityEngine.Animator
- 支持简写:
registerType(shortName, fullName): 注册自定义类型的简写映射。registerTypes(typeMap): 批量注册类型映射。hasType(typeName): 检查类型是否已注册。getFullTypeName(typeName): 获取类型的完整 AssemblyQualifiedName。
重要区分:
PuertsProxy.$typeof()→ 创建类型标记,仅用于泛型参数传递,不支持静态方法调用UnityEffectProxy.type()→ 创建完整类型代理,支持静态方法调用、静态属性访问// ❌ 错误:$typeof 不支持静态方法 await PuertsProxy.$typeof('GameObject').Find("Player"); // ✅ 正确:使用 UnityEffectProxy.type() 或快捷属性 await UnityEffectProxy.GameObject.Find("Player"); await UnityEffectProxy.type('UnityEngine.GameObject, UnityEngine.CoreModule').Find("Player"); // ✅ 正确:$typeof 用于泛型参数 const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
工具函数
enablePuertsLog(enable: boolean): 开启/关闭 Puerts 日志输出(默认关闭)。
场景用例对照表
以下对照表帮助 Puerts 开发者快速理解 gamelet-puerts-proxy 的写法差异。
对象创建与构造函数
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 创建 Vector3 | new Vector3(1, 2, 3) | await (new Vector3(1, 2, 3)).$await() |
| 创建 GameObject | new GameObject("Name") | await (new GameObject("Name")).$await() |
| 创建自定义类 | new MyClass() | await (new CustomType()).$await() |
// gamelet-puerts-proxy 示例
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 | await (await go.$get('transform')).$get('position') |
// gamelet-puerts-proxy 示例
const name = await go.$get('name');
go.name = "NewName"; // 不等待完成
await go.$set('name', "NewName"); // 等待完成
const hasTransform = await go.$has('transform'); // true
// 链式获取属性
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 GameObject.Find("Player") |
| 带返回值 | go.GetComponent<Transform>() | await go.GetComponent(PuertsProxy.$typeof('Transform')) |
| 显式调用 | go.SendMessage("Foo", 123) | await go.$call('SendMessage', ["Foo", 123]) |
// gamelet-puerts-proxy 示例 - 实例方法
await go.SetActive(true);
// 静态方法
const player = await UnityEffectProxy.GameObject.Find("Player");
// 泛型方法 - 必须使用 PuertsProxy.$typeof 传递类型
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
// 显式方法调用(用于处理重载)
const forward = await transform.$call('get_forward');
await transform.$call('set_localPosition', [newPos]);
// 带参数类型指定的调用(用于方法重载)
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'));
// 添加组件
const button = await go.AddComponent(PuertsProxy.$typeof('UnityEngine.UI.Button, UnityEngine.UI'));
// 获取所有组件
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 type = await comp.GetType();
const typeName = await type.$get('Name');
console.log(`Component[${i}]: ${typeName}`);
}事件监听
| 场景 | 原生 Puerts | gamelet-puerts-proxy |
| :--- | :--- | :--- |
| 添加监听 | button.onClick.AddListener(() => {}) | await button.$addListener('onClick', callback) |
| 移除监听 | button.onClick.RemoveListener(cb) | await button.$removeListener('onClick', callback) |
// gamelet-puerts-proxy 示例 - 事件监听
const button = await go.GetComponent(PuertsProxy.$typeof('UnityEngine.UI.Button, UnityEngine.UI'));
// 定义回调函数(需要保存引用才能移除)
const clickCallback = async () => {
console.log("Button clicked!");
};
// 添加监听
await button.$addListener('onClick', clickCallback);
// 移除监听(必须使用同一个函数引用)
await button.$removeListener('onClick', clickCallback);
// 注意:传统的 onClick.AddListener 写法也支持,但推荐使用 $addListener
// const onClick = await button.$get('onClick');
// await onClick.AddListener(clickCallback);对象销毁
// 销毁 GameObject
const UnityObject = UnityEffectProxy.type('UnityEngine.Object, UnityEngine.CoreModule');
await UnityObject.Destroy(go);
// 销毁组件
await UnityObject.Destroy(component);
// 立即销毁
await UnityObject.DestroyImmediate(go);子对象操作
// 设置父对象
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
const parentTransform = await parentGo.GetComponent(PuertsProxy.$typeof('Transform'));
await transform.SetParent(parentTransform, false); // false = 保持世界坐标
// 获取子对象数量
const childCount = await transform.$get('childCount');
// 获取子对象
const child = await transform.GetChild(0);
// 查找子对象
const foundChild = await transform.Find("ChildName");类型系统
// 预定义类型快捷访问
const GameObject = UnityEffectProxy.GameObject;
const Transform = UnityEffectProxy.Transform;
const Vector3 = UnityEffectProxy.Vector3;
const Vector2 = UnityEffectProxy.Vector2;
const Quaternion = UnityEffectProxy.Quaternion;
const Color = UnityEffectProxy.Color;
// 通过完整类型名获取
const Debug = UnityEffectProxy.type('UnityEngine.Debug, UnityEngine.CoreModule');
const Button = UnityEffectProxy.type('UnityEngine.UI.Button, UnityEngine.UI');
// 通过命名空间获取
const UnityEngineUI = UnityEffectProxy.namespace('UnityEngine.UI', 'UnityEngine.UI');
const ButtonType = UnityEngineUI.Button;
// 注册类型简写(用于泛型参数)
PuertsProxy.registerType('MyButton', 'UnityEngine.UI.Button');
const btnType = PuertsProxy.$typeof('MyButton'); // 等同于 UnityEngine.UI.Button
// 使用 $typeof 传递类型参数
const animator = await go.GetComponent(PuertsProxy.$typeof('Animator'));
const rectTransform = await go.GetComponent(PuertsProxy.$typeof('RectTransform'));调试技巧
// 查看代理对象元数据
UnityEffectProxy.logProxy('MyObject', go);
// 输出: [MyObject] ref: 10001, type: UnityEngine.GameObject...
// 获取代理信息
const ref = go.$ref; // 远程对象 ID
const type = go.$type; // 类型名
const info = go.$inspect(); // 完整调试信息
console.log(`对象 ID: ${ref}, 类型: ${type}`);API 参考
UnityEffectProxy
核心代理类,用于创建和管理代理对象。
| 方法/属性 | 说明 |
| :--- | :--- |
| getMountingGameObject(windowId, handle) | 获取当前挂载的 GameObject |
| type(assemblyQualifiedName) | 通过完整类型名获取类型代理 |
| namespace(ns, assembly) | 创建命名空间代理 |
| GameObject | GameObject 类型代理(快捷访问) |
| Transform | Transform 类型代理 |
| Vector3 | Vector3 类型代理 |
| Vector2 | Vector2 类型代理 |
| Quaternion | Quaternion 类型代理 |
| Color | Color 类型代理 |
代理对象实例方法
所有代理对象(GameObject, Component 等)都拥有以下特殊方法:
| 方法/属性 | 说明 |
| :--- | :--- |
| $get(propName) | 异步获取属性值 |
| $set(propName, value) | 异步设置属性值(等待完成) |
| $has(propName) | 异步检查属性是否存在 |
| $call(methodName, args, paramTypes?, genericTypes?) | 显式调用方法(用于处理重载) |
| $addListener(event, callback) | 添加事件监听 |
| $removeListener(event, callback) | 移除事件监听 |
| $ref | 获取远程对象 ID(属性) |
| $type | 获取类型名(属性) |
| $inspect() | 获取完整调试信息 |
PuertsProxy
类型辅助类,用于创建类型标记和管理类型映射。
重要区分:
PuertsProxy.$typeof()创建的是类型标记,仅用于泛型参数传递(如GetComponent),不支持调用静态方法UnityEffectProxy.type()创建的是完整类型代理,支持静态方法调用、静态属性访问如果需要调用静态方法,请使用
UnityEffectProxy.type()而非PuertsProxy.$typeof()
| 方法 | 说明 |
| :--- | :--- |
| $typeof(shortNameOrFullName) | 创建类型标记,仅用于泛型参数传递。支持简写 Animator 或全名 UnityEngine.Animator |
| registerType(shortName, fullName) | 注册单个自定义类型的简写映射 |
| registerTypes(typeMap) | 批量注册类型映射 |
| hasType(typeName) | 检查类型是否已注册 |
| getFullTypeName(typeName) | 获取类型的完整 AssemblyQualifiedName |
| getTypeMap() | 获取所有已注册的类型映射表(用于调试) |
| isTypeProxy(obj) | 检查对象是否为类型代理标记 |
| getTypeName(typeProxy) | 从类型代理中提取类型名 |
// ❌ 错误:PuertsProxy.$typeof 不支持静态方法调用
const goType = PuertsProxy.$typeof('GameObject');
await goType.Find("Player"); // 报错!$typeof 创建的只是类型标记
// ✅ 正确:使用 UnityEffectProxy.type() 获取完整类型代理
const GameObject = UnityEffectProxy.type('UnityEngine.GameObject, UnityEngine.CoreModule');
await GameObject.Find("Player"); // 正常工作
// ✅ 正确:PuertsProxy.$typeof 用于泛型参数
const transform = await go.GetComponent(PuertsProxy.$typeof('Transform'));
// 注册自定义类型
PuertsProxy.registerType('PlayerController', 'Game.Controllers.PlayerController, Assembly-CSharp');
PuertsProxy.registerTypes({
'EnemyAI': 'Game.AI.EnemyAI, Assembly-CSharp',
'InventorySystem': 'Game.Systems.InventorySystem, Assembly-CSharp',
});
// 检查类型是否已注册
if (PuertsProxy.hasType('PlayerController')) {
const controller = await go.GetComponent(PuertsProxy.$typeof('PlayerController'));
}
// 获取完整类型名
const fullName = PuertsProxy.getFullTypeName('Animator');
// 返回: 'UnityEngine.Animator, UnityEngine.AnimationModule'工具函数
| 函数 | 说明 |
| :--- | :--- |
| enablePuertsLog(enable: boolean) | 开启/关闭 Puerts 日志输出(默认关闭) |
import { enablePuertsLog } from 'gamelet-puerts-proxy';
// 开启调试日志
enablePuertsLog(true);常见问题
Q: 为什么 console.log(go) 看不到属性?
A: 代理对象是空的,所有数据都在 Unity 端。请使用 UnityEffectProxy.logProxy('Label', go) 查看代理的元数据(ID, 类型)。
Q: 报错 Cannot read property 'then' of undefined
A: 可能是你忘记加 await,或者尝试直接访问属性(如 go.transform.position)而不是分步访问(await (await go.$get('transform')).$get('position'))。
Q: 如何传递回调函数? A: 直接传递 JS 函数即可,Proxy 会自动将其注册并发送 ID 给 Unity。
const clickCallback = async () => {
console.log("Clicked!");
};
await button.$addListener('onClick', clickCallback);
// 移除时使用同一个引用
await button.$removeListener('onClick', clickCallback);Q: 泛型方法如何调用?
A: 使用 PuertsProxy.$typeof() 传递类型参数:
// 错误写法
const anim = await go.GetComponent<Animator>(); // ❌ 泛型被擦除
// 正确写法
const anim = await go.GetComponent(PuertsProxy.$typeof('Animator')); // ✓Q: 如何处理方法重载?
A: 使用 $call 方法显式指定参数类型:
await go.$call('SendMessage', ["MyMethod", 42], ['System.String', 'System.Int32']);Q: 属性赋值后如何确认生效?
A: 使用 $set 替代直接赋值:
go.name = "NewName"; // fire-and-forget,不等待
await go.$set('name', "NewName"); // 等待 Unity 端执行完成Q: 如何操作数组返回值?
A: 数组也是代理对象,需要使用 $get('Length') 和 GetValue(index):
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);
}Q: Reflect error: Unknown reflect operation: has 报错?
A: $has 方法需要 Unity 端支持,请确保 puerts-runtime 版本支持该操作。
Q: Method not found 报错?
A: 检查方法名是否正确,注意大小写。对于静态方法,确保使用类型代理而非实例代理:
// 正确:静态方法从类型代理调用
const UnityObject = UnityEffectProxy.type('UnityEngine.Object, UnityEngine.CoreModule');
await UnityObject.Destroy(go);
// 错误:从实例调用静态方法
await go.Destroy(go); // ❌