Files
AIC-Project/docs/superpowers/specs/2026-07-07-minigame-publish-pipeline-design.md
T
2026-07-10 10:24:29 +08:00

14 KiB
Raw Blame History

小游戏打包流程:Unity 菜单「XWorld/生成小游戏更新」设计

日期:2026-07-07 状态:已与用户逐段确认(§1-§4)

0. 目标与范围

一条菜单驱动的发布流水线,对选定的小游戏:

  • 客户端:打代码热更 ABDLL 以 .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 直下双管线,并同步改造客户端运行时消费端。

已确认的决策:

  • 代码热更形态:落地统一 ABDLL .bytes 进代码 AB,运行时同步改造)。
  • 平台维度:<platform>/ 子目录,一次菜单可勾选多平台构建
  • 版本号:自动递增(扫 CDN 已有版本取 max+1)+ 弹窗可改,两端共用同一 version。
  • 元数据:每游一份 publish.json(解决 RockPaperScissors ↔ RPS ↔ rps 名称映射)。
  • 签名:可选——约定私钥路径存在则签,否则空 sig + 警告。
  • 实现架构:方案 BEditor 编排 + 复用 PublishTool,新增瘦 CLI 壳)

1. 产物布局与格式

1.1 publish.json(每个小游戏一份,驱动全流程)

Client/Assets/MiniGames/<Game>/publish.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.sPlatformCDN/XWorld/<platform>/ 一致)。
  • 线上文件 md5 命名(name_<md5>.ext),files.txt 登记逻辑名——与现有 MiniGameDownloader.Md5Name 推导一致。
  • 边界:res/ 不存在或为空时跳过资源 ABassets 为空数组(仅发代码 AB)。
  • 不做 AES 加密(minigame 链路现状如此,与 XWorld 底包 AB 加密体系无关)。

1.3 服务器产物:deploy/minigame/<gameId>/<version>/

<serverAssembly> / <coreDll>   普通 DLL(不含 Framework.Shared.dllPrivate=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/ 删除重复 .cscsproj 改 <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/生成小游戏」), 新增 MiniGamePublishWindowEditorWindow):

  • 游戏选择:扫 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 导入为 TextAssetstaging 必须在 Assets 下且**不能带 `~` 后缀**——
   `~` 目录 Unity 不导入、资产进不了 AB;用完即删含 .meta)
4. 对每个勾选平台:
   BuildPipeline.BuildAssetBundles(平台临时目录,
       [ code.unity3d ← 两个 .bytes 资产,
         res.unity3d  ← AssetDatabase 枚举 Assets/MiniGames/<Game>/res/ 下全部资产 ],
       ChunkBasedCompression, 对应 BuildTarget)      ← 不切激活平台,直接传 target
5. 写 resolved-spec.jsonpublish.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 各 ABmd5 命名落盘 + 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.pemgitignore);Editor 检测到则传 --key,否则 CLI 写空 sig 并在输出中警告「未签名」。
  • PublishTool.Tests 同步更新:新布局断言(codeAb 进清单、无 .bytes 散文件、 双端目录形状、无 key 时空 sig)。
  • MiniGameManifest:解析新字段 codeAb(校验非空);assets 语义变为 「资源 AB 文件名列表」。
  • MiniGameDownloaderRemoteBase 加平台段 → <cdnBase><gameId>/<version>/<platform>/(平台名以编译宏判定,热更层内新增小工具函数, 不引用底包 LoadDll)。下载逻辑零改动(按 files.txt 通用增量下载)。 LocalDir 不加平台段(设备平台固定)。
  • MiniGameAssemblyLoader:从 LocalDir + codeAb(本地为明文件名) AssetBundle.LoadFromFileLoadAsset<TextAsset>("<coreDll>.bytes"/"<clientDll>.bytes")Assembly.LoadCore 先于 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 的 xUnitdotnet 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/minigameXWorldUtil.MINI_GAME_SERVER_GAMES_ROOT../../Build/PcMiniGameSmoke/server/games 改为指向仓库 deploy/minigame—— 菜单产物直接被 Play 自动起的本地网关消费,本地/外网同一份服务器产物布局; 目录缺失提示改为「先跑 XWorld/生成小游戏更新」。
  • .gitignoreAssets/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。结构改为 _currentkey=gameId@versionEntry 加 Fingerprint+ _retired 列表。 Acquire 命中时重算磁盘指纹:相同 → 计数++返回;目录读不到(覆盖交换瞬间窗口)→ 视为未变化用缓存;不同 → 先加载新模块(失败旧条目原样保留),成功后旧条目标 Superseded(引用 0 即卸载,否则挪 _retired),新条目入 _currentRelease 改按模块实例识别(先 _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 零改动。