Overworld
包参考

@overworld-engine/adapters-weapp

微信小游戏/小程序适配器:存储、socket、音频后端、R3F 画布入口与触摸摇杆

微信环境适配器合集。所有导出都是注入物,喂给既有包的注入点即可, 游戏其余代码与 Web 端共用;小游戏获得完整 3D(R3F createRoot + wx.createCanvas()),小程序(WXML)可使用除 3D / 触摸外的无头能力。

依赖 core + input + platform 三个工作区包 —— 适配层的例外 (适配器的职责就是粘合它所服务的包);系统包"只依赖 core"的依赖规则不变。

注入对照表

导出注入到
createWeappStorage()core persistOptions({ storage }) / createSaveSlots({ storage })
WeappWebSocketnet 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(): EnumerableStoragewx 同步存储 API 包装, 键枚举来自 getStorageInfoSync().keys。注意 wx.getStorageSync 对缺失键 返回 '',空字符串值读取为 null(持久层只存 JSON,实际无影响)。
  • WeappWebSocketwx.connectSocket SocketTask 上的标准 WebSocket 包装类,与 net 的 WebSocketConstructor 结构兼容(本包不依赖 net): 一行注入即可沿用 net 的 { from, data } 信封、连接期缓冲、限次重连与 close() 语义。服务器域名需加入小游戏 socket 白名单。 WS_CONNECTING / WS_OPEN / WS_CLOSING / WS_CLOSED 是与浏览器 WebSocket readyState 数值一致的公开常量。
  • createWeappAudioBackend(): AudioBackendwx.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/onHideapp: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)

useGLTFGLTFLoader → three 的 FileLoader。three r0.170 的 FileLoaderfetch(),而小游戏(及老基础库 WebGL1 真机)既无 fetch 也无 XMLHttpRequest。模板的 vendor/weapp-adapter.js 因此补齐一条完整链路:

  • 一个 XMLHttpRequest polyfill,由 wx.request 支撑(responseType 'text' | 'arraybuffer',GLB 必须走 'arraybuffer'),包内本地文件走 wx.getFileSystemManager().readFile / wx.downloadFile;
  • 其上一层薄薄的 fetch / Request / Headers,让 FileLoaderfetch() 透过 XMLHttpRequest polyfill 落到 wx.request

仅当 wx.request 存在时安装,并会覆盖宿主原生实现——这样 wx-shim(真实浏览器) 跑的与真机是同一条链路,不被原生 fetch/XHR 绕过。用法:模型放包内 (如 public/models/*.glb → 打包进 /models/*.glb),给 BaseNPCmodelPath 即可;网络模型需把域名加入小游戏 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 验证,微信开发者工具预览为 最终人工确认。

本页目录