@overworld-engine/adapters-weapp
微信小游戏/小程序适配器:存储、socket、音频后端、R3F 画布入口与触摸摇杆
微信环境适配器合集。所有导出都是注入物,喂给既有包的注入点即可,
游戏其余代码与 Web 端共用;小游戏获得完整 3D(R3F createRoot +
wx.createCanvas()),小程序(WXML)可使用除 3D / 触摸外的无头能力。
依赖
core+input+platform三个工作区包 —— 适配层的例外 (适配器的职责就是粘合它所服务的包);系统包"只依赖 core"的依赖规则不变。
注入对照表
| 导出 | 注入到 |
|---|---|
createWeappStorage() | core persistOptions({ storage }) / createSaveSlots({ storage }) |
WeappWebSocket | net createWebSocketTransport({ WebSocketImpl: WeappWebSocket }) |
createWeappAudioBackend() | audio createAudioManager({ backend }) |
createWeappTouchJoystick(target) | scene <Player externalInput={target}> |
createWeappPointerBridge(canvasRoot) | 场景里网格上的 onClick / onPointerXxx(R3F 指针事件 + 射线拾取) |
registerWeappBridge() | platform createBridge()(注册 weapp 桥) |
快速开始(小游戏)
// game.js 顶部先加载官方 weapp-adapter polyfill(模板内锁定版本)
import { createMovementInput } from '@overworld-engine/input'
import { setLabelCanvasFactory } from '@overworld-engine/scene'
import {
createWeappCanvasRoot,
createWeappTouchJoystick,
registerWeappBridge,
} from '@overworld-engine/adapters-weapp'
registerWeappBridge()
setLabelCanvasFactory(() => wx.createCanvas()) // SpriteLabel 的画布来源
const movement = createMovementInput()
createWeappTouchJoystick(movement)
const { render } = createWeappCanvasRoot()
render(<Game externalInput={movement} />) // SceneShell(labelMode="sprite")等导出说明
createWeappStorage(): EnumerableStorage—wx同步存储 API 包装, 键枚举来自getStorageInfoSync().keys。注意wx.getStorageSync对缺失键 返回'',空字符串值读取为null(持久层只存 JSON,实际无影响)。WeappWebSocket—wx.connectSocketSocketTask 上的标准WebSocket包装类,与 net 的WebSocketConstructor结构兼容(本包不依赖 net): 一行注入即可沿用 net 的{ from, data }信封、连接期缓冲、限次重连与close()语义。服务器域名需加入小游戏 socket 白名单。WS_CONNECTING/WS_OPEN/WS_CLOSING/WS_CLOSED是与浏览器 WebSocket readyState 数值一致的公开常量。createWeappAudioBackend(): AudioBackend—wx.createInnerAudioContext映射为 audio 的后端契约(接口结构一致,契约测试对 weapp 与 HTMLAudio 两个后端跑同一套断言)。配合pauseOnHide: true与平台桥,切后台自动静音恢复。createWeappCanvasRoot(options?)— R3F 底层createRoot(canvas)+wx.createCanvas();尺寸取getSystemInfoSync(),dpr 钳制 2,缺省gl: { antialias: true, alpha: false }、frameloop: 'always',options.renderProps可覆盖合并(camera/shadows/onCreated等)。 返回{ root, canvas, size, store, render, dispose };store在首次render()后可用,供指针桥接入。缺省不接线 R3F 指针事件(events: undefined, 与 v1.1 逐字节一致);要用onClick/ 射线拾取见下一条。computeCanvasRootSize公开同一套尺寸计算,MAX_CANVAS_DPR为 2。createWeappTouchJoystick(target, { region?, size?, deadZone?, runThreshold? })— 无 DOM 浮动摇杆:消费wx全局触摸事件,锚定在触点落点,复用 input 包的 纯函数摇杆数学,写入MovementInputRef;返回{ dispose }。region: 'left-half'(缺省)只响应左半屏。createWeappPointerBridge(canvasRoot, { region?, tapMaxDurationMs?, tapMaxDistance?, canvasOrigin? })— 在render()之后挂载,把 R3F 指针事件全程交给wx触摸驱动:每个触摸合成一个 pointer 事件喂给 fiber 的指针管线(onPointerDown/Move/Up),轻点(短按不拖动) 额外派发onClick。于是场景里<mesh onClick>/<group onClick>收到真实射线拾取。 从不监听真实 DOM,所以 wx-shim(真实浏览器)里与真机同一条路径。返回{ dispose }(解绑触摸、停用事件层)。纯坐标映射另导出touchToOffset/offsetToNdc/touchToNdc。详见下文「指针 / 射线拾取」。registerWeappBridge()/createWeappBridge()— 注册 platform 的weapp桥:storage()用 wx 存储、wx.onShow/onHide→app:resumed/app:paused、safe-area 读系统信息、openExternal为 warn no-op(平台政策)。getWx()与Wx*类型 —wx全局的最小结构化类型与访问器 (非微信环境抛出带指引的错误),同时是测试 fake 的契约。
指针 / 射线拾取(createWeappPointerBridge)
小游戏没有 DOM,R3F 默认拿不到指针事件。指针桥用 wx.onTouchStart/Move/End
喂 fiber 自己的指针管线(而非真实 DOM),所以 wx-shim 与真机同一条代码路径:
import {
createWeappCanvasRoot,
createWeappPointerBridge,
} from '@overworld-engine/adapters-weapp'
const canvasRoot = createWeappCanvasRoot({ renderProps: { camera } })
canvasRoot.render(<World />) // 先 render:指针桥需要 R3F store
const bridge = createWeappPointerBridge(canvasRoot, { region: 'full' })
// 场景里:<group onClick={() => openDialogue(id)}><BaseNPC .../></group>
// ...退出:bridge.dispose()- 轻点 = 拾取,拖动 ≠ 拾取:只有「短按且几乎不移动」的触摸才派发
onClick; 拖动摇杆不会被误判成场景点按。因此指针桥可与createWeappTouchJoystick同屏共存,无需划分区域 —— 摇杆吃左半屏拖动做移动,指针桥吃轻点做拾取。 需要硬隔离时传region: 'right-half',把拾取限制在非摇杆一侧。 - 冒泡:R3F 指针事件从被拾取的子网格冒泡到祖先,给整个 NPC 裹一层
<group onClick>即可拾取其模型/胶囊体/名牌任意网格。 - 坐标:全屏 wx 画布左上角即视口原点,
offsetX/Y == clientX/Y;非全屏画布传canvasOrigin。默认compute由纯函数offsetToNdc生成 NDC(有单测)。
useGLTF 加载模型(需 vendor XHR polyfill)
useGLTF → GLTFLoader → three 的 FileLoader。three r0.170 的 FileLoader
走 fetch(),而小游戏(及老基础库 WebGL1 真机)既无 fetch 也无
XMLHttpRequest。模板的 vendor/weapp-adapter.js 因此补齐一条完整链路:
- 一个
XMLHttpRequestpolyfill,由wx.request支撑(responseType'text' | 'arraybuffer',GLB 必须走'arraybuffer'),包内本地文件走wx.getFileSystemManager().readFile/wx.downloadFile; - 其上一层薄薄的
fetch/Request/Headers,让FileLoader的fetch()透过XMLHttpRequestpolyfill 落到wx.request。
仅当 wx.request 存在时安装,并会覆盖宿主原生实现——这样 wx-shim(真实浏览器)
跑的与真机是同一条链路,不被原生 fetch/XHR 绕过。用法:模型放包内
(如 public/models/*.glb → 打包进 /models/*.glb),给 BaseNPC 传 modelPath
即可;网络模型需把域名加入小游戏 request 合法域名。
已知约束(小游戏)
- drei
Text(troika)不可用:标签用 scene 的SpriteLabel(labelMode="sprite"+setLabelCanvasFactory); useGLTF依赖上面的 vendor XHR/fetch polyfill:模型放包内或把域名加入 request 合法域名(GLB 用arraybuffer);- weapp-adapter 版本在模板内锁定;基础库最低 2.19(WebGL1 兜底)。
测试
单测全部基于 vi.stubGlobal('wx', fake)(存储枚举、socket 信封语义、
audio 契约两后端对齐、摇杆触摸序列、canvasRoot 尺寸/配置/释放);
真实 WebGL 渲染由 wx-shim 浏览器 harness 验证,微信开发者工具预览为
最终人工确认。