Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# 小游戏打包流程:Unity 菜单「XWorld/生成小游戏更新」设计
|
||||
|
||||
日期:2026-07-07
|
||||
状态:已与用户逐段确认(§1-§4)
|
||||
|
||||
## 0. 目标与范围
|
||||
|
||||
一条菜单驱动的发布流水线,对选定的小游戏:
|
||||
|
||||
- 客户端:打**代码热更 AB**(DLL 以 .bytes/TextAsset 进 AB)与**资源热更 AB**,落到
|
||||
`CDN/minigame/<gameId>/<version>/<platform>/`,供多端下载更新(本地 `Cdn.Runner` 或外网 CDN 同一布局)。
|
||||
- 服务器:`Server/games-src/<对应目录>` 编译出普通 DLL,落到
|
||||
`deploy/minigame/<gameId>/<version>/`,整目录上传外网服务器即可作为网关 `--gamesRoot` 的 games 根热更运行。
|
||||
- `Client/Assets/MiniGames` 下有多个小游戏目录时,先弹窗选择其一(单个也弹窗,用于确认版本/平台)。
|
||||
|
||||
本设计同时落地 2026-06-22 的「统一 AB」决策(设计 §10.1):客户端发布产物全部为 `.unity3d`,
|
||||
弃用裸 `.bytes` 直下双管线,并同步改造客户端运行时消费端。
|
||||
|
||||
已确认的决策:
|
||||
- 代码热更形态:**落地统一 AB**(DLL .bytes 进代码 AB,运行时同步改造)。
|
||||
- 平台维度:**加 `<platform>/` 子目录,一次菜单可勾选多平台构建**。
|
||||
- 版本号:**自动递增(扫 CDN 已有版本取 max+1)+ 弹窗可改**,两端共用同一 version。
|
||||
- 元数据:**每游一份 `publish.json`**(解决 RockPaperScissors ↔ RPS ↔ rps 名称映射)。
|
||||
- 签名:**可选**——约定私钥路径存在则签,否则空 sig + 警告。
|
||||
- 实现架构:**方案 B(Editor 编排 + 复用 PublishTool,新增瘦 CLI 壳)**。
|
||||
|
||||
## 1. 产物布局与格式
|
||||
|
||||
### 1.1 publish.json(每个小游戏一份,驱动全流程)
|
||||
|
||||
`Client/Assets/MiniGames/<Game>/publish.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"gameId": "rps",
|
||||
"serverSrcDir": "RPS",
|
||||
"serverProject": "RPS.Server/RPS.Server.csproj",
|
||||
"serverAssembly": "RPS.Server.dll",
|
||||
"serverEntryType": "RPS.Server.RpsServerRoom",
|
||||
"clientProject": "Client/HotUpdateGames/RPS.Client/RPS.Client.csproj",
|
||||
"coreDll": "RPS.Core.dll",
|
||||
"clientDll": "RPS.Client.dll",
|
||||
"clientEntryType": "RPS.Client.RpsGameClient",
|
||||
"minFrameworkVersion": 1,
|
||||
"playerCount": 2,
|
||||
"tickRateHz": 10
|
||||
}
|
||||
```
|
||||
|
||||
- `serverSrcDir` 相对 `Server/games-src/`;`serverProject` 相对 `serverSrcDir`;
|
||||
`clientProject` 相对仓库根。
|
||||
- 菜单只把「含 publish.json 的目录」视为可发布小游戏;新游戏拷模板改字段即接入。
|
||||
|
||||
### 1.2 客户端产物(每平台一份):`CDN/minigame/<gameId>/<version>/<platform>/`
|
||||
|
||||
```
|
||||
game.json gameId/version/clientEntryType/coreDll/clientDll/minFrameworkVersion
|
||||
+ 新字段 "codeAb": "code.unity3d" + "assets": ["res.unity3d"]
|
||||
files.txt {'code.unity3d','<md5>',size}, {'res.unity3d','<md5>',size},
|
||||
files.txt.sig 有私钥则 RSA 签名,否则空文件(沿用两端"未配置则跳过"语义)
|
||||
code_<md5>.unity3d 代码 AB:<coreDll>.bytes + <clientDll>.bytes 两个 TextAsset
|
||||
res_<md5>.unity3d 资源 AB:整个 res/ 目录打一个包(assets 数组支持以后拆多包)
|
||||
```
|
||||
|
||||
- 平台名沿用底包惯例:`pc / android / ios / webgl`(与 `LoadDll.sPlatform`、
|
||||
`CDN/XWorld/<platform>/` 一致)。
|
||||
- 线上文件 md5 命名(`name_<md5>.ext`),files.txt 登记逻辑名——与现有
|
||||
`MiniGameDownloader.Md5Name` 推导一致。
|
||||
- 边界:`res/` 不存在或为空时跳过资源 AB,`assets` 为空数组(仅发代码 AB)。
|
||||
- **不做 AES 加密**(minigame 链路现状如此,与 XWorld 底包 AB 加密体系无关)。
|
||||
|
||||
### 1.3 服务器产物:`deploy/minigame/<gameId>/<version>/`
|
||||
|
||||
```
|
||||
<serverAssembly> / <coreDll> 普通 DLL(不含 Framework.Shared.dll,Private=false 保证)
|
||||
game.json server 视图(serverAssembly/serverEntryType/playerCount/tickRateHz/minFrameworkVersion)
|
||||
files.txt + files.txt.sig 供 GameModuleLoader 配公钥后加载前验签
|
||||
```
|
||||
|
||||
结构与网关 `--gamesRoot` 消费的 `<id>/<ver>/` 完全一致。
|
||||
|
||||
### 1.4 配套修正:Core 单源
|
||||
|
||||
现状问题:`RPS.Client.csproj` 以 `<Compile Include="scripts~\**\*.cs">` 把 Core+Client
|
||||
全编进 RPS.Client.dll(内嵌 Core 类型副本,下载的 RPS.Core.dll 实为冗余);而服务器
|
||||
`RPS.Core.csproj` 编译的是 `Server/games-src/RPS/RPS.Core/` 下另一份已发生格式漂移的拷贝。
|
||||
两端 Core 一旦真漂移即确定性不同步。
|
||||
|
||||
修正:
|
||||
- `Server/games-src/RPS/RPS.Core/` 删除重复 .cs,csproj 改
|
||||
`<Compile Include>` 链接 `Client/Assets/MiniGames/RockPaperScissors/scripts~/Core/**`
|
||||
(恢复「Core 唯一源码位在客户端 scripts~/Core」的原设计)。
|
||||
- `RPS.Client.csproj` 改为只编 `scripts~/Client/**` + `ProjectReference` RPS.Core
|
||||
(Private=false),Client.dll 不再内嵌 Core 副本;发布用的客户端 Core.dll 取自
|
||||
RPS.Core 构建产物(与服务器同一 IL)。
|
||||
|
||||
## 2. 构建编排(Editor 菜单 + PublishTool.Cli)
|
||||
|
||||
### 2.1 Editor 侧
|
||||
|
||||
重写 `Client/Assets/Editor/MiniGamePublishMenu.cs`(替换 stub 菜单「XWorld/生成小游戏」),
|
||||
新增 `MiniGamePublishWindow`(EditorWindow):
|
||||
|
||||
- **游戏选择**:扫 `Assets/MiniGames/*/publish.json`;多个则列表选择,仅一个预选。
|
||||
- **版本号**:默认 = 扫 `CDN/minigame/<gameId>/` 下数字目录取 max+1(无则 1),可手改;
|
||||
覆盖已有版本前弹确认。
|
||||
- **平台勾选**:PC/Android/iOS/WebGL,默认勾选当前激活 BuildTarget 对应项。
|
||||
- **生成按钮**:执行 §2.2 流水线,每步失败 `DisplayDialog` 报错中断。
|
||||
|
||||
### 2.2 流水线
|
||||
|
||||
```
|
||||
1. dotnet build Server/games-src/<serverSrcDir>/<serverProject> -c Release
|
||||
→ <serverAssembly> + <coreDll>
|
||||
2. dotnet build <clientProject> -c Release
|
||||
→ <clientDll>(ProjectReference 引用步骤 1 的 Core 工程产物)
|
||||
3. 拷 Core.dll/Client.dll → Assets/MiniGameStaging/code/*.dll.bytes;
|
||||
AssetDatabase.Refresh 导入为 TextAsset(staging 必须在 Assets 下且**不能带 `~` 后缀**——
|
||||
`~` 目录 Unity 不导入、资产进不了 AB;用完即删含 .meta)
|
||||
4. 对每个勾选平台:
|
||||
BuildPipeline.BuildAssetBundles(平台临时目录,
|
||||
[ code.unity3d ← 两个 .bytes 资产,
|
||||
res.unity3d ← AssetDatabase 枚举 Assets/MiniGames/<Game>/res/ 下全部资产 ],
|
||||
ChunkBasedCompression, 对应 BuildTarget) ← 不切激活平台,直接传 target
|
||||
5. 写 resolved-spec.json(publish.json 字段 + version + AB 文件名列表)到临时目录
|
||||
6. Process 调 PublishTool.Cli(惯例同 XWorldUtil 起 dotnet 的方式):
|
||||
server:调一次 → 临时 out → 成功后原子 Directory.Move 入 deploy/minigame/<id>/<ver>/
|
||||
client:每平台一次 → 临时 out → 成功后移入 CDN/minigame/<id>/<ver>/<platform>/
|
||||
7. 清理 staging;汇总日志 + 完成对话框(列出全部落位路径、是否已签名)
|
||||
```
|
||||
|
||||
先落临时目录、成功后整体 Move 的原因:`CDN/` 可能正被 `Cdn.Runner` 对外提供下载,
|
||||
避免半成品被客户端拉走。
|
||||
|
||||
### 2.3 PublishTool 侧(.NET,本环境可 dotnet test 回归)
|
||||
|
||||
- **`PublishLayout` 改造**:拆 `PublishServer(srcDir, outDir, spec, key?)` 与
|
||||
`PublishClient(srcDir, outDir, spec, key?)`;客户端侧删除 `CopyBytes` 双 .bytes 逻辑,
|
||||
改为登记 `codeAb + assets` 各 AB(md5 命名落盘 + files.txt 登记逻辑名)。
|
||||
- **`GameSpec` / `GameJsonWriter`**:加 `CodeAb` 字段;`WriteClient` 输出 `codeAb`。
|
||||
- **新增 `Server/PublishTool.Cli`**(瘦控制台,入 Server.sln):
|
||||
`--mode client|server --src <dir> --out <dir> --spec <resolved-spec.json> [--key <pem>]`
|
||||
- **签名**:约定私钥 `Tools/keys/minigame-private.pem`(gitignore);Editor 检测到则传
|
||||
`--key`,否则 CLI 写空 sig 并在输出中警告「未签名」。
|
||||
- **`PublishTool.Tests` 同步更新**:新布局断言(codeAb 进清单、无 .bytes 散文件、
|
||||
双端目录形状、无 key 时空 sig)。
|
||||
|
||||
## 3. 客户端运行时改造(XWorld.Link)
|
||||
|
||||
- **`MiniGameManifest`**:解析新字段 `codeAb`(校验非空);`assets` 语义变为
|
||||
「资源 AB 文件名列表」。
|
||||
- **`MiniGameDownloader`**:`RemoteBase` 加平台段 →
|
||||
`<cdnBase><gameId>/<version>/<platform>/`(平台名以编译宏判定,热更层内新增小工具函数,
|
||||
不引用底包 LoadDll)。下载逻辑零改动(按 files.txt 通用增量下载)。
|
||||
`LocalDir` 不加平台段(设备平台固定)。
|
||||
- **`MiniGameAssemblyLoader`**:从 `LocalDir + codeAb`(本地为明文件名)
|
||||
`AssetBundle.LoadFromFile` → `LoadAsset<TextAsset>("<coreDll>.bytes"/"<clientDll>.bytes")`
|
||||
→ `Assembly.Load`(Core 先于 Client)→ `AB.Unload(false)`。
|
||||
找不到 TextAsset 时报清晰错误(列出 AB 内实际资产名)。
|
||||
- **资源 AB 加载**:新增 `MiniGameAssetLoader : IAssetLoader`——按需
|
||||
`AssetBundle.LoadFromFile(LocalDir + 资源AB)`,缓存已开包,按资产路径 `LoadAsset`,
|
||||
小游戏退出统一 `Unload(true)`。`MiniGameHost` 注入 `GameClientCtx` 的 loader 由
|
||||
`UnityAssetLoader`(走 XResLoader 大厅管线,不认识 minigame AB)换成它,并在
|
||||
`ExitGame` 补上此前 TODO 的 AB 卸载(MiniGameHost.cs:111)。
|
||||
|
||||
## 4. 错误处理、测试与配套
|
||||
|
||||
### 4.1 错误处理
|
||||
|
||||
- Editor 流水线每步检查退出码/产物存在性,失败即 `DisplayDialog` + 保留日志,
|
||||
临时目录不移入 CDN/deploy(无半成品)。
|
||||
- 覆盖已有版本目录:确认弹窗后先删旧目录再 Move。
|
||||
- 运行时:沿用 Fail/Error 路径;新增错误信息注明「需要 codeAb 格式,旧 .bytes 包不再支持」。
|
||||
|
||||
### 4.2 测试与验证
|
||||
|
||||
- **本环境可实证**:PublishTool 新布局 + Cli 的 xUnit(`dotnet test Server/Server.sln`
|
||||
全量回归);两端 csproj 改造后 `dotnet build` 0 错;客户端改动照惯例 csc 编译验证。
|
||||
- **须 Unity 人工核对**(本环境无 Unity):EditorWindow 交互、BuildAssetBundles
|
||||
跨平台产出、Editor Play 端到端(菜单生成 → Cdn.Runner 供 CDN → 网关
|
||||
`--gamesRoot deploy/minigame` → 进 RPS 对局)。
|
||||
|
||||
### 4.3 配套调整
|
||||
|
||||
- **删除 `Tools/PcMiniGameSmoke/Build-PcMiniGameSmoke.ps1`**:其裸 .bytes 产物格式
|
||||
运行时不再支持,且 ps1 打不了 AB;README 改指菜单。
|
||||
- **本地开发网关改指 `deploy/minigame`**:`XWorldUtil.MINI_GAME_SERVER_GAMES_ROOT`
|
||||
由 `../../Build/PcMiniGameSmoke/server/games` 改为指向仓库 `deploy/minigame`——
|
||||
菜单产物直接被 Play 自动起的本地网关消费,本地/外网同一份服务器产物布局;
|
||||
目录缺失提示改为「先跑 XWorld/生成小游戏更新」。
|
||||
- `.gitignore`:`Assets/MiniGameStaging/`、`Tools/keys/`(CDN/deploy 产物默认不 ignore;
|
||||
当前 `.git` 不可用、仓库无 .gitignore,此条待启用 git 后生效)。
|
||||
- WebGL 注意:`AssetBundle.LoadFromFile` 在 WebGL 不可用,运行时按平台走
|
||||
`LoadFromMemory(File.ReadAllBytes(...))` 回退(微信 WASM 实测仍是已知开放风险)。
|
||||
|
||||
### 4.3.1 修订(2026-07-07,用户定):本地网关无条件启动,发布即生效
|
||||
|
||||
原 §4.3 给本地网关加的「`deploy/minigame` 下须有 game.json 才启动」检测与热更架构矛盾——
|
||||
服务器本就支持运行中热更(`ServerLoop.BuildGameList` 每次请求重扫目录;
|
||||
`GameModuleRegistry.Acquire` 建房时按需加载,新版本自动淘汰旧版本),不应做启动期存在性检测。
|
||||
|
||||
- `XWorldUtil` 删除 `HasMiniGameServerPackage()` 及其两处调用:`CanOpenLocalServer`
|
||||
只看网关 Runner DLL 存在;`StartMiniGameGatewayRunnerProcess` 删除
|
||||
「PC minigame package not found」弹框分支。
|
||||
- 启动前 `Directory.CreateDirectory(GetMiniGameServerGamesRoot())`(幂等)保证
|
||||
`--gamesRoot` 有效;目录下暂无任何 `*/*/game.json` 时仅 `Debug.Log` 提示
|
||||
「可随时用菜单发布,运行中即生效」,不打断。
|
||||
- ~~已知边界:同版本覆盖重发会因 `LoadFromAssemblyPath` 文件锁失败~~(已被 §4.3.2 消除)。
|
||||
- 大厅列表为实时扫描结果,客户端重新拉列表即见新游戏,不做推送(YAGNI)。
|
||||
|
||||
### 4.3.2 修订(2026-07-07,用户定方案 B):字节加载去锁 + 内容指纹自动失效
|
||||
|
||||
消除"同版本覆盖重发被文件锁挡住"的问题,使覆盖重发也**发布即生效**(新房间用新代码,
|
||||
已入房玩家打完旧代码):
|
||||
|
||||
- **GameModuleAlc 字节加载**:`LoadEntry` 与私有依赖解析两处 `LoadFromAssemblyPath`
|
||||
改为 `File.ReadAllBytes` + `LoadFromStream`——加载后不持有任何文件句柄,覆盖/删除
|
||||
目录永远成功;`Unload()` 仍惰性异步(无锁故无需强制 GC)。已知约束:字节加载的
|
||||
Assembly `Location` 为空,小游戏 DLL 不得依赖它。验签照旧不受影响。
|
||||
- **GameModuleRegistry 内容指纹失效**:`ComputeFingerprint(moduleDir)` = 目录下全部
|
||||
`*.dll`+`game.json` 按名 Ordinal 排序拼 `name=md5;` 再整体 md5。结构改为
|
||||
`_current`(key=`gameId@version`,Entry 加 Fingerprint)+ `_retired` 列表。
|
||||
`Acquire` 命中时重算磁盘指纹:相同 → 计数++返回;目录读不到(覆盖交换瞬间窗口)→
|
||||
视为未变化用缓存;不同 → 先加载新模块(失败旧条目原样保留),成功后旧条目标
|
||||
Superseded(引用 0 即卸载,否则挪 `_retired`),新条目入 `_current`。
|
||||
`Release` 改按模块实例识别(先 `_current` 同实例,否则 `_retired` ReferenceEquals)。
|
||||
`RefCount/IsLoaded` 语义指 `_current`。生产环境版本不可变、指纹恒命中,行为零变化。
|
||||
- 测试(TDD):字节加载后磁盘 DLL 可删/可覆盖;同 key 内容变化 → 新实例 + 旧条目归零
|
||||
后弱引用 GC 断言卸载;新旧并存期各自 Release 正确;指纹未变命中缓存;现有
|
||||
Registry/AlcUnload/RoomHost 测试零回归。
|
||||
- 配套:PcMiniGameSmoke README 的文件锁注意事项改为"覆盖重发即生效"语义说明。
|
||||
|
||||
### 4.4 兼容性说明
|
||||
|
||||
- 旧格式(裸 .bytes、无平台段)的既有 CDN 产物在运行时改造后不再可用;当前仅 RPS 一款、
|
||||
由本流水线重新生成即可,无迁移负担。
|
||||
- 服务器端 game.json/DLL 布局不变,网关/GameModuleLoader 零改动。
|
||||
Reference in New Issue
Block a user