npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-forgetproxy.name = 'NewName'(不等待,无返回值,由 set trap 走 notify
    • awaitableawait proxy.$set('name', 'NewName')(等待 Unity 端执行完成)

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:configureTransportgetTransport。所有 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() vs type() —— 不要混用:

| | 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.mode0(同步)或 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.logwarn / 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: 检查两点:

  1. registerType 的 fullTypeName 必须带 Assembly(如 , Assembly-CSharp),否则 fallback 到 UnityEngine.CoreModule 必然解析失败。
  2. 静态方法必须从类型代理调用(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