# wx-unity-babel

这个目录保存微信 UnityPlugin 在微信小游戏转 vivo 小游戏流程中的本地适配资产。转换工具不会从网络实时下载插件，而是按版本把需要的插件文件和音频适配脚本拷贝到转换产物的 `src` 目录；`@babel/runtime` 只给旧版 1.2.32 使用。

这份文档主要回答三件事：

- 1.2.88 是怎么接入当前微信转 vivo 链路的。
- `unity_plugin_1.2.88.js` 这个大 bundle 内部具体负责哪些能力、启动流程怎样走。
- 后续升级 UnityPlugin 或排查 Unity 微信小游戏问题时，应该检查哪些点。

当前结论：

- 转换链路会按微信工程声明的 UnityPlugin 版本做阈值选择：`>= 1.2.88` 使用本地 `1.2.88`，否则回退到本地 `1.2.32`。
- 转换产物会通过本地 `WxUnityPlugin.js` 加载 `wx_unity_audio.js` 和选中的 `unity_plugin_<version>.js`。
- `unity_plugin_1.2.88.js` 不是普通业务脚本，而是微信 Unity WebGL 插件运行时的 webpack bundle。
- 原始 `unity_plugin_1.2.88.js` 应保留为运行入口。后续如果拆阅读版模块，建议只作为辅助理解，不直接替代运行入口。

## 目录内容

| 文件 / 目录 | 说明 |
| --- | --- |
| `unity_plugin_1.2.88.js` | 微信 UnityPlugin 1.2.88 本地版本，微信声明版本 `>= 1.2.88` 时加载它。 |
| `unity_plugin_1.2.32.js` | 历史本地版本，微信声明版本低于 `1.2.88` 时加载它。 |
| `wx_unity_audio.js` | 在加载 UnityPlugin 前执行的音频适配脚本。 |
| `@babel/runtime/` | 仅 UnityPlugin 1.2.32 及以下旧版需要的 Babel helper。 |

## 转换链路

入口在 `../index.js` 的 Unity 适配分支。当前流程如下：

1. 读取微信 `game.json`，如果发现 `plugins.UnityPlugin.version`，认为这是微信 Unity 插件工程。
2. 根据微信工程声明的 UnityPlugin 版本选择本地适配版本，`>= 1.2.88` 用 `1.2.88`，否则用 `1.2.32`。
3. 写入 vivo `manifest.json`，其中 `engine` 会变成 `wx_unityPlugin_<选中的版本>`。
4. 生成转换产物里的 `src/WxUnityPlugin.js`。
5. `WxUnityPlugin.js` 先执行 `require("wx_unity_audio.js")`，再执行 `require("unity_plugin_<version>.js")`。
6. 按需拷贝对应的本地插件文件；仅 1.2.32 额外拷贝 `@babel/runtime/`。
7. 如果 Unity 导出脚本直接调用 `require("unity_plugin_<version>.js", pluginEnv)`，转换工具会先改写为 `wx.requirePlugin("UnityPlugin", pluginEnv)`，避免 Webpack 把双参数 `require` 当成 AMD require 静态分析。
8. 转换后的模板 `game.js` 会把 `requirePlugin("UnityPlugin")` 重定向到 `require("WxUnityPlugin.js")`。

简化调用关系：

```mermaid
flowchart TD
  A["微信 game.json 声明 UnityPlugin"] --> B["weixin/index.js 识别 Unity 工程"]
  B --> C{"声明版本 >= 1.2.88?"}
  C -->|是| D["选择本地 1.2.88"]
  C -->|否| E["选择本地 1.2.32"]
  D --> F["manifest.engine = wx_unityPlugin_<选中版本>"]
  E --> F
  F --> G["生成 src/WxUnityPlugin.js"]
  G --> H["加载 wx_unity_audio.js"]
  H --> I["加载 unity_plugin_<选中版本>.js"]
  I --> J["导出 UnityPlugin manager"]
```

### 转换阶段源码对应关系

| 阶段 | 位置 | 关键点 |
| --- | --- | --- |
| 识别 Unity 工程 | `../index.js` | 读取 `game.json`，发现 `plugins.UnityPlugin.version` 后设置 `vivoGameManifestJson.isUnity = true`。 |
| 版本选择 | `../index.js` | `>= 1.2.88` 选 `1.2.88`，否则选 `1.2.32`。 |
| manifest 标记 | `../index.js` | `manifest.engine` 写成 `wx_unityPlugin_<选中的版本>`。 |
| 生成本地入口 | `../index.js` | 生成 `src/WxUnityPlugin.js`，先加载音频适配，再加载选中的 `unity_plugin_<version>.js`。 |
| 复制依赖 | `../index.js` | 只拷贝选中的插件文件；仅 1.2.32 额外拷贝 `@babel/runtime/`。 |
| 双参数 require 改写 | `../index.js` | 把 Unity 导出的 `require("unity_plugin_<version>.js", pluginEnv)` 改成 `wx.requirePlugin("UnityPlugin", pluginEnv)`。 |
| requirePlugin 重定向 | `../wx-templates/src/game.js` | `wx.requirePlugin("UnityPlugin")` 返回 `require("WxUnityPlugin.js")`。 |
| Unity framework 补丁 | `../index.js` | 对 `webgl.wasm.framework.unityweb.js` 做 `unityInstance -> Module`、`Runtime`、`HandleError`、WebSocket close reason 等兼容处理。 |

转换后的实际入口内容类似：

```js
global.pluginEnv = global.pluginEnv || {}
let plugin
try {
  require("wx_unity_audio.js")
  plugin = require("unity_plugin_<version>.js")
} catch (error) {
  console.error("Failed to load WxUnityPlugin:", error)
  throw error
}
module.exports = plugin
```

## `unity_plugin_1.2.88.js` 做了什么

`unity_plugin_1.2.88.js` 是微信 Unity WebGL 插件的 webpack bundle。它不是单一功能脚本，而是一套 Unity 小游戏运行时。

主要职责：

- 识别微信小游戏插件环境、普通小游戏环境和部分 MagicBrush 环境。
- 从 `pluginEnv` / `customEnv` 中接入 `wx`、`canvas`、`document`、`unityNamespace`、`coverview`、`gameTransfer`、`reportKeyValue` 等能力。
- 初始化 Unity manager，并暴露 `init`、`startGame`、`start`、`hideLoadingPage`、`reportGameStageError` 等能力。
- 创建 `webgl`、`webgl2`、`wxwebgl`、`wxwebgl2` 或 `wxmetal` 上下文。
- 初始化 Unity 全局对象，包括 `UnityLoader.SystemInfo`、`UnityCache.XMLHttpRequest` 和 `Module`。
- 加载主 wasm code 分包和 Unity data package。
- 支持 wasm 代码分包、subwasm、wasm patch、缺失函数上报和性能采集。
- 管理 CoverView / 长视频 / 交互视频 loading 页。
- 接管资源下载、缓存、文件系统读写和 worker 写文件。
- 处理音频、视频、字体、日志、性能、错误和启动阶段上报。

从文件结构看，它大致由三层组成：

| 层级 | 作用 |
| --- | --- |
| webpack runtime | 顶层维护模块表、模块缓存、`require` 实现，最后 `module.exports = s`。 |
| 平台适配层 | 探测小游戏插件环境、普通小游戏环境、MagicBrush 环境，并接入 `wx`、`canvas`、`document`、`pluginEnv`。 |
| Unity manager 层 | 导出默认 manager class，负责 `init()`、`startGame()`、WASM/data 加载、loading、上报和生命周期。 |

可以用这些关键词在 bundle 中定位重点：

| 关键词 | 说明 |
| --- | --- |
| `const B = "1.2.88"` | 插件版本常量，也是确认当前 bundle 版本的最快入口。 |
| `isMiniGamePlugin` | 判断是否运行在微信小游戏插件模式。 |
| `pluginEnv.customEnv` | 插件模式下读取 `wx`、`canvas`、`document`、`unityNamespace` 的入口。 |
| `class Zi` | Unity manager 主类，包含构造函数、`init()`、`startGame()`、`hideLoadingPage()` 等核心方法。 |
| `loadWasmCode()` | 主 wasm code 分包加载入口。 |
| `loadDataPackage()` | Unity data package 加载入口。 |
| `UnityCache: { XMLHttpRequest: ho }` | Unity 资源请求缓存 XHR 的注入点。 |
| `getAllResponseHeaders() || ""` | 当前本地 1.2.88 已加的空值兜底补丁点。 |
| `wxwebgl` / `wxmetal` | GLX / Metal 上下文选择相关逻辑。 |

核心启动流程：

```mermaid
flowchart TD
  A["require unity_plugin_1.2.88.js"] --> B["webpack runtime 初始化"]
  B --> C["初始化平台环境 env"]
  C --> D["初始化 mgp/gameTransfer/reportKeyValue"]
  D --> E["导出 Unity manager"]
  E --> F["new Unity manager(options)"]
  F --> G["拉取特性开关并初始化上报基线"]
  G --> H["init(): 创建 GL 上下文"]
  H --> I["initGameGlobal(): 写 UnityLoader 和 UnityCache"]
  I --> J["startGame(): 展示 loading"]
  J --> K["prepareModule(): 绑定 Unity Module"]
  K --> L["并行加载 wasm code 和 data package"]
  L --> M["可选加载 subwasm / wasm patch"]
  M --> N["调用 UnityModule 并进入游戏"]
  N --> O["持续上报启动阶段、错误和性能数据"]
```

更细一点，运行时会经历下面这些步骤：

1. `game.js` 覆盖 `global.wx.requirePlugin`。
2. 游戏业务侧或 Unity 导出的入口调用 `requirePlugin("UnityPlugin", pluginEnv)`；如果入口直接调用 `require("unity_plugin_<version>.js", pluginEnv)`，转换阶段会先改写成 `wx.requirePlugin("UnityPlugin", pluginEnv)`。
3. 模板把 `pluginEnv` 保存到 `global.pluginEnv`，然后加载 `WxUnityPlugin.js`。
4. `WxUnityPlugin.js` 先执行 `wx_unity_audio.js`，把 Unity 音频接口适配到 vivo 的 `qg.createInnerAudioContext()`。
5. `WxUnityPlugin.js` 再执行 `unity_plugin_1.2.88.js`，bundle 初始化 webpack 模块表和平台环境。
6. 插件探测当前是否有 `pluginEnv`。有则进入小游戏插件模式，没有则尝试普通小游戏模式。
7. 插件从 `pluginEnv.customEnv` 中拿到 Unity 运行必需对象：`wx`、`canvas`、`document`、`unityNamespace`、`WXWASMSDK`。
8. 插件导出 Unity manager class。
9. 游戏侧创建 manager，构造函数初始化文件系统、特性开关、上报器、监控器、loading 管理器等。
10. `manager.init()` 创建 GL context，并把 `UnityLoader.SystemInfo`、`UnityCache.XMLHttpRequest` 注入到 Unity 全局对象。
11. `manager.startGame()` 展示 loading 页，并行加载 wasm code 分包和 data package。
12. wasm code 下载完成后调用 `UnityModule(gameInstance.Module)`。
13. data package 下载或读取缓存完成后解析 `UnityWebData1.0` / `TuanjieWebData1.0` 格式，并写入 Unity FS。
14. wasm 和 data 都准备好后进入 `start()`，执行编译动画，最后 `doStart()` 交给 Unity framework 继续启动。
15. 启动过程中持续记录阶段耗时、网络状态、缓存命中、错误、前后台切换和游戏可交互耗时。

## 关键能力说明

### 平台环境桥接

插件会判断当前环境是否存在 `pluginEnv`。如果存在，会认为自己运行在微信小游戏插件环境中，并从 `pluginEnv.customEnv` 中读取：

- `wx`
- `canvas`
- `document`
- `unityNamespace`
- `events`
- `WXWASMSDK`

同时从 `pluginEnv` 读取：

- `gameTransfer`
- `reportKeyValue`
- `coverview`
- `instanceId`
- `getPrivateFileSystemManager`
- `setFileSpaceStatistics`
- `getWxCommonFont`

这些能力决定了资源上报、CoverView loading、字体、私有文件系统和 wasm 分包能力是否可用。

对当前 vivo 转换链路来说，最关键的是：

- `wx.requirePlugin("UnityPlugin", pluginEnv)` 必须尽量带完整 `pluginEnv`。
- `pluginEnv.customEnv.wx` 通常应指向微信 API 适配对象，也就是转换后的 `wx/qg` 兼容层。
- `pluginEnv.customEnv.canvas` 必须能创建 WebGL context。
- `pluginEnv.customEnv.unityNamespace` 必须包含 Unity 导出的配置、`Module`、`UnityModule`、资源 MD5、CDN 地址等信息。
- `pluginEnv.gameTransfer` 和 `pluginEnv.reportKeyValue` 可以缺省，但 1.2.88 会频繁访问这些字段，因此至少要有对象级兜底，不能让 `pluginEnv` 本身不存在。

### 渲染上下文选择

`init()` 会根据 `contextConfig` 和系统能力选择上下文：

- 默认 `webgl`
- Unity 配置 WebGL2 时使用 `webgl2`
- 开启 GLX 且环境支持时使用 `wxwebgl` / `wxwebgl2`
- 开启 Metal 且环境支持时使用 `wxmetal`

创建失败时会弹窗，并通过上报通道记录 `GET_CONTEXT` 错误。

### Unity 全局对象注入

`initGameGlobal()` 会把外部传入的 manager 配置写入 `unityNamespace`，并设置两个关键对象：

```js
UnityLoader: {
  SystemInfo: {
    width,
    height,
    gpu,
    browser: "wx",
    browserVersion: "0.0",
    language,
    hasWebGL
  },
  UnityCache: {
    XMLHttpRequest: ho
  }
}
```

这意味着 Unity framework 后续发出的资源请求不会直接使用原始 `XMLHttpRequest`，而是走插件封装的缓存 XHR。这个封装负责缓存命中、文件读取、下载失败上报、预加载任务复用和写文件。

### 资源下载与缓存

插件封装了 `UnityCache.XMLHttpRequest`，用于 Unity 资源包下载、缓存读取和缓存写入。资源下载通常使用 arraybuffer，并记录：

- 下载耗时
- 资源大小
- 网络状态
- 是否命中缓存
- 写文件失败原因
- 缓存空间统计

当前本地 `unity_plugin_1.2.88.js` 已补充：

```js
e.getAllResponseHeaders() || ""
```

这个兜底用于避免运行时 `getAllResponseHeaders()` 返回空值后继续 `.trim()` 导致崩溃。

缓存 XHR 的主要流程：

1. `open(method, url, async)` 标准化 URL，并判断资源是否可缓存。
2. 可缓存资源先计算本地缓存路径。
3. 如果存在预加载任务，则等待预加载结果或直接复用预加载内容。
4. 如果本地缓存存在且内容非空，直接创建 response 并触发 `load`。
5. 如果没有缓存，走原始 XHR 下载。
6. 下载成功后，按资源类型写入本地文件系统。
7. 下载失败、空文件、写缓存失败都会触发 `LOAD_ASSET_BUNDLE` 类上报。

### WASM code 分包

主 wasm 通常通过小游戏分包加载，例如 `wasmcode` 或高性能模式下的 `wasmcode_wk`。流程大致是：

1. 根据 `CODE_FILE_MD5` 和游戏名计算 wasm 文件名。
2. 调用 `wx.loadSubpackage()` 下载 wasm code 分包。
3. 下载完成后写入标记文件。
4. 调用 `UnityModule(gameInstance.Module)`。
5. 触发 `ModulePrepared`。
6. 进入资源包处理和 Unity 启动。

失败时会弹窗提示，并上报 `LOAD_SUBPACKAGE` 错误。

分包名选择逻辑和 iOS 高性能模式有关：

- 普通路径：`wasmcode`
- 高性能模式下的特定路径：`wasmcode_wk`
- wasm 文件名：`${CODE_FILE_MD5}.${GAME_NAME}.wasm.code.unityweb.wasm.br`

### subwasm 和 wasm patch

如果工程启用了 wasm 代码分包，插件还会处理：

- `subwasm` 预加载和编译耗时上报。
- 缺失函数收集。
- `gamewxagwasmsplitwap_reportsplitmisselem` 上报。
- `gamewxagwasmsplitwap_reportsplitcalledfuncs` 上报。
- wasm patch 下载、写入、编译和缺失 patch 函数上报。

这部分主要是为了降低首包 wasm 体积，并在运行时按需补齐函数。

当前转换脚本还会对 wasm split 相关代码做一个兼容替换：

```js
GameGlobal.canUseH5Renderer || true ? xxx.wasm_split.logCall =
```

目的是避免 wasm 分包路径在目标 Runtime 中因为能力判断过严导致卡顿或不走期望分支。后续升级插件时要重点验证这个替换的正则是否仍能匹配新 Unity framework。

### Loading 页

插件支持多种 loading 类型：

- 长视频 loading。
- CoverView loading。
- 交互视频 loading。

CoverView loading 会根据 `loadingPageConfig` 创建背景图、进度条、文本、图标和扩展按钮，并在资源下载、wasm 编译、游戏准备等阶段更新进度。

当前模板还监听了全局 `manager` 和 `managerConfig`：

- 当 `managerConfig.loadingPageConfig.materialConfig.backgroundImage` 存在时，优先尝试创建 vivo 自定义 loading。
- 当 manager 的 `hideLoadingPage()` 被调用时，会先移除 vivo 自定义 loading，再调用原插件逻辑。

### 音频适配

`WxUnityPlugin.js` 会先加载 `wx_unity_audio.js`，随后加载 UnityPlugin。UnityPlugin 内部还会使用微信的 `createInnerAudioContext()`，并包含 `createAudio`、`playAudio`、`pauseAudio` 等 frame 级适配逻辑。

`wx_unity_audio.js` 主要做的是把 Unity WebAudio 侧导出的 `QG_JS_Sound_*` 系列函数接到 `window.qg.createInnerAudioContext()`：

- `QG_JS_Sound_Load()` 从 Unity heap 里取音频数据，写入小游戏用户目录。
- `QG_JS_Sound_Create_Channel()` 创建播放通道，并处理循环、音量、结束回调。
- `QG_JS_Sound_Play()` 复用或重建 inner audio context，规避同一实例连续播放问题。
- `QG_JS_Sound_Stop()`、`SetPaused()`、`SetVolume()` 等函数对齐 Unity 音频控制。

### 上报与日志

插件有多条上报通道，主要覆盖：

- 插件启动信息。
- 文件系统初始化耗时。
- 获取 GL 上下文耗时。
- wasm code 下载、编译和失败。
- data package 下载、读取、解压和失败。
- CoverView 初始化、背景图下载。
- wasm 分包缺失函数和 patch。
- 游戏切前台 / 后台。
- 游戏主场景可交互耗时。

这些上报很多会通过 `gameTransfer` 或 `reportKeyValue` 走微信插件能力。

如果目标 Runtime 没有完整微信插件上报能力，应保证缺省函数只降级为 no-op 或日志，不要抛异常。启动成功优先级高于上报完整性。

## 与 Unity 导出文件的关系

Unity 微信小游戏通常包含几类关键文件：

| Unity 文件 / 配置 | 插件如何使用 |
| --- | --- |
| `webgl.wasm.framework.unityweb.js` | Unity framework 主逻辑。转换脚本会做兼容替换，插件最终调用里面暴露的 `UnityModule()`。 |
| `wasmcode/` | 主 wasm code 分包，插件通过 `wx.loadSubpackage()` 加载。 |
| `wasmcode_wk/` | iOS 高性能模式可能使用的 wasm code 分包。 |
| `data-package/` | data package 分包模式下的资源包目录。 |
| `DATA_CDN` | CDN 模式下 data package 下载基址。 |
| `STREAMING_CDN` | StreamingAssets 下载基址。 |
| `CODE_FILE_MD5` | 拼接 wasm code 文件名。 |
| `DATA_FILE_MD5` | 拼接 data package 文件名和缓存文件名。 |
| `DATA_FILE_SIZE` / `OPT_DATA_FILE_SIZE` | 校验 data package 字节数，防止资源包损坏。 |
| `GAME_NAME` | 拼接 wasm/data 文件名。 |

适配时如果出现 wasm 或 data 加载失败，优先检查这些配置是否在 `unityNamespace` 中存在，且文件名能和实际分包、CDN 文件对应上。

## 1.2.88 适配注意点

### `pluginEnv` 更敏感

1.2.88 在加载和初始化阶段会读取 `pluginEnv.gameTransfer`、`pluginEnv.reportKeyValue`、`pluginEnv.customEnv` 等字段。如果 `pluginEnv` 不存在或不是对象，容易出现空引用。

因此转换工具生成的 `WxUnityPlugin.js` 中需要先兜底：

```js
global.pluginEnv = global.pluginEnv || {}
```

注意这个兜底只能避免 `pluginEnv` 变量不存在导致的 `ReferenceError`。如果已经进入插件模式，完整启动仍依赖 `pluginEnv.customEnv` 中的 `wx`、`canvas`、`document`、`unityNamespace` 等对象。

### `getAllResponseHeaders()` 需要空值兜底

部分运行时或适配层里，XHR 的 `getAllResponseHeaders()` 可能返回空值。1.2.88 内部会继续调用 `.trim()`，所以本地文件需要保留：

```js
getAllResponseHeaders() || ""
```

### manifest 版本和实际加载版本必须一致

转换流程会先读取微信工程声明的 UnityPlugin 版本，再通过 `UNITY_PLUGIN_THRESHOLD_VERSION` 和 `UNITY_PLUGIN_LEGACY_VERSION` 选择本地适配版本。新增版本时需要确保：

- 阈值常量和旧版回退常量符合当前兼容策略。
- 本目录存在 `unity_plugin_<version>.js`。
- `manifest.engine` 写入同一个版本。
- `WxUnityPlugin.js` require 同一个版本。

### Unity framework 补丁要继续验证

转换脚本会对 `webgl.wasm.framework.unityweb.js` 做几类运行时补丁：

- `unityInstance` 替换为 `Module`，用于支持 vivo Unity SDK 回调。
- `Runtime` 替换为 `window.Runtime`，用于日志打印。
- `HandleError(err, code)` 中插入 `console.error(err)`。
- `ev.reason` 替换为 `ev.reason || ""`，规避 WebSocket close 事件没有 reason。
- `_UnloadbyPath` 卸载 AssetBundle 时同步释放 WXFS 的 `path2fd`、`fd2wxStream` 和 `_url2path` 映射，避免已卸载资源的元数据累积。
- `wxfile:` 替换为 `internal://files`。
- wasm split 能力判断替换，避免分包逻辑卡住。

这些补丁不在 `unity_plugin_1.2.88.js` 内，但属于 1.2.88 接入能否成功的同一条链路。升级 Unity 导出版本时也要一起验证。

### 不建议直接运行拆分后的模块

`unity_plugin_1.2.88.js` 是 webpack 打包产物，模块之间依赖 webpack runtime 的模块缓存、默认导出包装、全局对象和闭包变量。`unity_plugin_1.2.32.js` 则额外依赖 `@babel/runtime`。即使为了阅读把它拆成多个 JS，也建议：

- 原始 `unity_plugin_1.2.88.js` 保留为唯一运行入口。
- 拆分文件只放到单独阅读目录，例如 `unity_plugin_1.2.88.parts/`。
- 文件名按功能自动命名，例如 `env-platform.js`、`unity-manager.js`、`wasm-code-loader.js`、`data-package-loader.js`、`cache-xhr.js`，不要只叫 `module_1234.js`。
- README 或 manifest 记录 webpack module id 到功能名的映射，方便回查原 bundle。
- 任何运行时补丁都优先落回原 bundle 或转换脚本，不要让产物同时依赖原 bundle 和阅读拆分版。

## 新增或升级插件版本时的检查清单

1. 把新插件放到本目录，命名为 `unity_plugin_<version>.js`。
2. 修改 `../index.js` 中的 `UNITY_PLUGIN_THRESHOLD_VERSION`、`UNITY_PLUGIN_LEGACY_VERSION` 或选择逻辑。
3. 确认 `manifest.engine`、日志输出和 `WxUnityPlugin.js` require 的版本一致。
4. 检查新插件是否需要 `getAllResponseHeaders()` 空值兜底。
5. 检查新插件是否在加载阶段直接读取 `pluginEnv`、`wx` 或 `GameGlobal`。
6. 如果最终选择的是 1.2.32，确认 `@babel/runtime/` 一并拷贝；如果是 1.2.88，不需要再拷贝 Babel runtime。
7. 执行语法检查：

```bash
node --check packages/cli-packager/src/weixin/wx-unity-babel/unity_plugin_<version>.js
```

8. 执行依赖扫描，确认新版本是否仍有外部 runtime 依赖：

```bash
rg -n '@babel|babel|regeneratorRuntime|require\(' packages/cli-packager/src/weixin/wx-unity-babel/unity_plugin_<version>.js
```

9. 执行转换工具入口检查：

```bash
npx standard packages/cli-packager/src/weixin/index.js
```

10. 使用真实 Unity 微信小游戏转换并运行，重点看：

- `WxUnityPlugin.js` 是否能 require 成功。
- `wx_unity_audio.js` 是否先执行。
- GL context 是否创建成功。
- wasm code 分包是否下载成功。
- data package 是否下载、读取或解压成功。
- loading 页是否能隐藏。
- `gameTransfer` / `reportKeyValue` 缺失时是否能降级。

### 真实游戏验证建议

语法检查只能证明 bundle 可解析，不能证明 Unity 启动链路可用。接入 1.2.88 后至少需要跑一款真实 Unity 微信小游戏，关注以下日志或现象：

| 阶段 | 通过标准 |
| --- | --- |
| require 阶段 | 没有 `Failed to load WxUnityPlugin`。 |
| 环境阶段 | `pluginEnv`、`customEnv`、`unityNamespace` 不为空。 |
| GL 阶段 | `canvas.getContext(...)` 成功，未弹 `不支持 webgl/wxwebgl/wxmetal`。 |
| wasm 阶段 | `wasmcode` 或 `wasmcode_wk` 分包加载成功。 |
| data 阶段 | data package 下载或读取缓存成功，字节数匹配。 |
| Unity FS 阶段 | 能解析 `UnityWebData1.0` 或 `TuanjieWebData1.0`，并写入 FS。 |
| call main 阶段 | `UnityModule(gameInstance.Module)` 被调用，`ModulePrepared` 触发。 |
| 画面阶段 | loading 能隐藏，主场景可交互。 |
| 音频阶段 | 背景音或音效能播放、暂停、停止，不出现连续播放失败。 |

## 常见问题定位

### `Failed to load WxUnityPlugin`

通常是 `unity_plugin_<version>.js` 没有被拷贝到产物 `src`，或 `WxUnityPlugin.js` 里 require 的版本和实际文件名不一致。

检查：

- 本目录是否存在目标版本文件。
- 转换产物 `src` 下是否存在目标版本文件。
- `WxUnityPlugin.js` 中的 require 文件名是否正确。

### `Cannot read properties of undefined (reading 'gameTransfer')`

通常是 `pluginEnv` 没有初始化。检查 `WxUnityPlugin.js` 是否有：

```js
global.pluginEnv = global.pluginEnv || {}
```

如果仍然报错，继续检查调用 `wx.requirePlugin("UnityPlugin", pluginEnv)` 时是否传入了完整 `pluginEnv`。只有空对象时，插件后续读取 `pluginEnv.customEnv.unityNamespace` 仍可能失败。

### `getAllResponseHeaders` 后续 `.trim()` 崩溃

检查本地插件是否保留：

```js
getAllResponseHeaders() || ""
```

### `不支持 webgl/wxwebgl/wxmetal`

说明 GL context 创建失败。需要检查：

- `contextConfig`
- `canvas`
- runtime 是否支持 GLX / Metal
- iOS 是否处于 Lockdown Mode
- 当前平台是否支持目标 context

### wasm 分包下载失败

重点检查：

- `wasmcode` / `wasmcode_wk` 分包是否存在。
- wasm 文件名是否符合 `${CODE_FILE_MD5}.${GAME_NAME}.wasm.code.unityweb.wasm.br`。
- `wx.loadSubpackage` 是否成功。
- 网络和分包配置是否正确。

### data package 大小不匹配

如果日志出现 `mismatch data size`、`unknown data format` 或资源读取后崩溃，重点检查：

- `DATA_FILE_MD5`、`GAME_NAME` 拼出的文件名是否和实际文件一致。
- `DATA_FILE_SIZE` / `OPT_DATA_FILE_SIZE` 是否和实际下载字节数一致。
- CDN 是否对 `.txt` 文件开启 gzip/br，未开启会明显拖慢下载。
- `UnityWebData1.0` / `TuanjieWebData1.0` 文件头是否被破坏。
- 分包模式和 CDN 模式是否被配置混用。

### loading 页不消失

重点检查：

- Unity 是否调用了 manager 的 `hideLoadingPage()`。
- 模板里的全局 `manager` setter 是否成功包住了 `hideLoadingPage()`。
- `window._loading.remove()` 是否执行。
- `hideAfterCallmain`、`useCoverView`、`loadingPageConfig.visible` 是否符合预期。
- 游戏是否卡在 wasm/data 并行加载阶段，导致还没进入真正的 hide 阶段。

### 音频没有声音或只能播放一次

重点检查：

- `wx_unity_audio.js` 是否在 `unity_plugin_1.2.88.js` 之前加载。
- `window.qg.createInnerAudioContext` 是否存在。
- 用户目录音频临时文件是否写入成功。
- `QG_JS_Sound_Play()` 是否创建了新的 audio context 实例。
- 音量是否被 Unity 设置成大于 1 后被截断到 1。

## 与转换模板的关系

模板 `wx-templates/src/game.js` 会覆盖 `wx.requirePlugin`。当游戏调用：

```js
requirePlugin("UnityPlugin")
```

转换后会进入模板里的 `wx.requirePlugin("UnityPlugin")` 分支，并返回：

```js
require("WxUnityPlugin.js")
```

所以 UnityPlugin 的实际入口不是微信原生插件系统，而是转换工具生成的本地 JS 入口。

模板还做了两件和 UnityPlugin 强相关的事：

- 监听 `managerConfig`，在存在 loading 背景图时尝试创建 vivo 自定义 loading。
- 监听 `manager`，包裹 `hideLoadingPage()`，保证 vivo 自定义 loading 被移除。

因此排查 loading 问题时，不要只看 `unity_plugin_1.2.88.js`，也要看转换模板生成的 `game.js`。

## 维护建议

- 不要直接删除旧版本插件文件，除非确认不再需要兼容旧转换产物。
- 新插件建议保留原始文件，同时记录本地补丁点。
- 对于压缩 bundle，尽量只做必要兼容补丁，避免大范围格式化导致难以比对。
- 如果需要深入理解新版本，可以先按 webpack 模块 ID 拆分为阅读版，但运行入口仍建议保留原始 bundle。
- 每次升级版本后，把本地补丁点写回本 README，尤其是 `pluginEnv`、XHR、GL context、wasm/data、音频和 loading 相关变更。
- 如果拆分阅读版模块，文件命名应按功能自动识别，不要长期保留只有 `module_<id>.js` 的命名方式。
