14 KiB
小游戏打包流程: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:
{
"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/**+ProjectReferenceRPS.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 build0 错;客户端改动照惯例 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提示 「可随时用菜单发布,运行中即生效」,不打断。 已知边界:同版本覆盖重发会因(已被 §4.3.2 消除)。LoadFromAssemblyPath文件锁失败- 大厅列表为实时扫描结果,客户端重新拉列表即见新游戏,不做推送(YAGNI)。
4.3.2 修订(2026-07-07,用户定方案 B):字节加载去锁 + 内容指纹自动失效
消除"同版本覆盖重发被文件锁挡住"的问题,使覆盖重发也发布即生效(新房间用新代码, 已入房玩家打完旧代码):
- GameModuleAlc 字节加载:
LoadEntry与私有依赖解析两处LoadFromAssemblyPath改为File.ReadAllBytes+LoadFromStream——加载后不持有任何文件句柄,覆盖/删除 目录永远成功;Unload()仍惰性异步(无锁故无需强制 GC)。已知约束:字节加载的 AssemblyLocation为空,小游戏 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同实例,否则_retiredReferenceEquals)。RefCount/IsLoaded语义指_current。生产环境版本不可变、指纹恒命中,行为零变化。 - 测试(TDD):字节加载后磁盘 DLL 可删/可覆盖;同 key 内容变化 → 新实例 + 旧条目归零 后弱引用 GC 断言卸载;新旧并存期各自 Release 正确;指纹未变命中缓存;现有 Registry/AlcUnload/RoomHost 测试零回归。
- 配套:PcMiniGameSmoke README 的文件锁注意事项改为"覆盖重发即生效"语义说明。
4.4 兼容性说明
- 旧格式(裸 .bytes、无平台段)的既有 CDN 产物在运行时改造后不再可用;当前仅 RPS 一款、 由本流水线重新生成即可,无迁移负担。
- 服务器端 game.json/DLL 布局不变,网关/GameModuleLoader 零改动。