# C# 热更联机小游戏架构设计 - 日期:2026-06-18 - 状态:已通过头脑风暴评审,待编写实现计划 - 适用范围:客户端(Unity)+ 服务端(.NET)统一的 C# 热更小游戏平台 ## 1. 背景与目标 在现有工程基础上,建立一套**客户端与服务端都用 C# 热更**的联机小游戏架构,支持内部团队**独立制作小游戏并上传**,客户端**动态下载小游戏资源、加载并运行**。 ### 现有工程现状 - 客户端:Unity + URP,已集成 **HybridCLR**(C# 热更,`Client/Assets/Base/LoadDll.cs` 负责增量下载 AOT/热更 DLL + AssetBundle,已适配 android/ios/webgl/微信小游戏)+ **XLua**(Lua 热更)。 - 资源分发:AssetBundle(`.unity3d`)+ md5 清单(`dllfiles.txt`/`files.txt`)做增量下载。 - 已有样例:`Client/Assets/MiniGames/RockPaperScissors`(石头剪刀布联机小游戏,旧版 IMGUI + Lua,服务端 `main.lua`,含匹配 + 15 秒 AI 兜底;已按 §2.4 目录规范迁入 `MiniGames/`,待 C# 重写)。 - 服务端:`Server/` 目录为空,尚未实现。 - 现有规则(`Doc/Rule/UnityProject.md`)规定小游戏逻辑用 Lua、不用 C#——本设计将**取代该规则**。 ### 已锁定的核心决策(评审确认) 1. 小游戏**全面转向 C# 热更**,弃用 Lua 小游戏路线;`RockPaperScissors` 后续用 C# 重写。 2. **权威服务端**模型:服务端动态加载每个小游戏的 C# 逻辑,跑权威对局。 3. 小游戏代码组织为**共享逻辑核心 + 两端薄层**。 4. 上传者仅限**内部可信团队**;安全从轻,以签名校验 + 版本管理为主,不上重量级进程沙箱。 5. 目标平台:**App(Android/iOS,HybridCLR)+ 微信小游戏(WASM)都要**,传输层统一 **WebSocket**。 6. 联机节奏:**轻实时(状态同步 10–20Hz)+ 回合制**。 ### 关键技术选型(评审确认) - **A1**:共享核心源码一份,客户端/服务端各自编译(客户端编进 HybridCLR 热更 DLL,服务端编成普通 .NET DLL)。 - **B1**:服务端单进程 + 可卸载 `AssemblyLoadContext` 动态加载小游戏逻辑(预留将来子进程隔离 B2 的接口)。 - **C1**:复用并扩展现有 `LoadDll` + md5 清单机制,叠加"小游戏 manifest"维度,不另起炉灶。 ## 2. 整体分层与小游戏组成 ### 2.1 三层 + 两宿主 ``` ┌─────────────────────────────────────────────┐ │ 框架层 (Framework, 常驻不热更) │ │ 客户端宿主: LoadDll/AB加载/网络/UI根/小游戏生命周期 │ │ 服务端宿主: 网关/匹配/房间宿主/ALC加载器/持久化 │ │ 共享: 网络协议编解码 / 小游戏SDK接口定义 │ └───────────────┬─────────────────────────────┘ │ 通过稳定接口加载/调用 (不反向依赖小游戏) ┌────────────────────┼────────────────────┐ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ 小游戏·共享核心 │ │ 小游戏·客户端薄层 │ │ 小游戏·服务端薄层 │ │ .Core.dll │ │ .Client.dll│ │ .Server.dll │ │ 纯C#:规则/状态/ │ │ HybridCLR加载: │ │ .NET ALC加载: │ │ 协议/校验 │ │ 渲染/UI/输入/表现 │ │ 房间/AI/持久化 │ │ 依赖→平台能力接口 │ │ 依赖→Core+客户端SDK│ │ 依赖→Core+服务端SDK│ └─────────────────┘ └─────────────────┘ └──────────────────┘ ``` **依赖方向铁律**:框架 ← 小游戏(小游戏依赖框架接口,框架永不依赖具体小游戏)。Core 只依赖纯 C# + 平台能力接口,绝不引用 `UnityEngine`、`System.IO`、socket 等平台 API。 ### 2.2 一个小游戏发布包的组成 | 内容 | 说明 | 跑在哪 | |---|---|---| | `.Core.dll` | 平台无关规则/状态/协议(A1:源码一份,两端各编一次) | 客户端 + 服务端 | | `.Client.dll` | 客户端表现层(HybridCLR 热更 DLL) | 客户端 | | `.Server.dll` | 服务端权威逻辑(普通 .NET DLL,服务端侧不打 AB,原样下发 ALC) | 服务端 ALC | | 客户端 AB `.unity3d` 若干 | **脚本与美术统一打 AB**:`.Core.dll`/`.Client.dll` 以 `.bytes`(TextAsset) 打进 AB,`res/` 美术/UI/音效另打 AB(§2.4) | 客户端 | | `game.json` 元数据 | 游戏 id、版本、入口类型名、最小框架版本、人数、AI 兜底参数等 | 两端读取 | | `manifest`(md5 清单) | 上述文件 md5 + size + 依赖,挂到现有 `LoadDll` 体系(C1) | 客户端下载用 | > 客户端发布产物**全部是 AB(`.unity3d`)**:热更 DLL 与美术资源都走同一套 AB 下载/版本/校验管线,避免两条管线(独立 `.bytes` vs AB)并存。服务端的 `.Server.dll`/`.Core.dll` 仍是普通 .NET DLL(ALC 直接 `Assembly.LoadFrom`,不打 AB)。 ### 2.3 小游戏入口契约 框架定义稳定接口,小游戏实现,框架按 `game.json` 的类型名反射实例化: - `IGameClient`:客户端薄层实现(生命周期 + 网络消息 + 帧更新)。 - `IGameServerRoom`:服务端薄层实现(房间生命周期 + 消息 + tick)。 - `IGameLogic`(Core 内):纯函数式推进状态 `Step(state, input) -> (newState, events)`,两端共用做权威计算与客户端预演。 新增一个小游戏 = 写 1 个 Core + 各 1 个薄层实现接口,框架零改动。 ### 2.4 客户端源码目录规范 每个小游戏的**客户端源文件统一收敛在一个目录**下,便于团队独立维护、打包工具按目录扫描、热更资源整目录打 AB: ``` Client/Assets/MiniGames// # 一个小游戏一个根目录( = gameId,如 RockPaperScissors) scripts/ # 客户端脚本(C# 热更源码) Core/ # 共享核心源码(A1:一份源码,客户端编进 HybridCLR 热更 DLL,服务端 链接同一批 .cs 编 .NET DLL) Client/ # 客户端薄层:IGameClient 实现 + 渲染/UI/输入/表现 res/ # 美术 / UI / 预制体 / 音效(按 AB 目录规则组织,打成 AssetBundle) ``` 约定: - **`scripts/` 与 `res/` 都参与打 AB**。`scripts/` 编出的热更 DLL(`.Core.dll`/`.Client.dll`)以 `.bytes`(TextAsset) 形式打进 AB;`res/` 资源按目录打 AB。客户端发布产物全是 `.unity3d`(§2.2)。 - **依赖方向**:`scripts/Core` 不得引用 `UnityEngine`(纯 C#,§2.1 铁律);`scripts/Client` 可引用 `UnityEngine` + 框架客户端 SDK + Core。 - **共享 Core 一份源码**:`scripts/Core` 是 Core 的**唯一源码位置**;服务端薄层工程(`Server/games-src//.Server`)通过 `` 链接同一批 `.cs`,杜绝两端逻辑漂移(A1)。服务端薄层(`IGameServerRoom` 实现)属服务端源码,仍留在 `Server/games-src/`,不进客户端 `MiniGames/`。 - 框架加载器(`Client/Assets/Script/xmain/MiniGame/`)是**常驻框架**,不属于任何单个小游戏,不放进 `MiniGames/`。 ## 3. 三条核心数据流 ### 3.1 发布流程(制作 → 上传 → 上架) ``` 开发者本地 ├─ 写 .Core / .Client / .Server (Core/Client 源码在 Client/Assets/MiniGames//scripts/,Server 在 Server/games-src//) [§2.4] ├─ 一键构建工具(Editor菜单): │ 1. Core 编两次 → 热更DLL(给HybridCLR) + .NET DLL(给服务端) [A1] │ 2. Client 编成 HybridCLR 热更 DLL │ 3. 脚本+美术统一打 AB:Core/Client 热更 DLL 转 .bytes(TextAsset) 打进 AB + res/ 按目录打 AB → 全是 .unity3d [§2.2/§2.4] │ 4. 生成 game.json + 计算各文件 md5 → 小游戏 manifest │ 5. 全部 → 签名(防篡改) └─ 上传到「发布服务」(内部后台) │ 发布服务 ├─ 校验签名 + 最小框架版本兼容 ├─ 写入版本库(灰度/正式通道), 资源/DLL 落 CDN ├─ 客户端侧: 把小游戏 manifest 并入 LoadDll 的清单维度 [C1] └─ 服务端侧: 通知房间宿主「有新版本 vN 可用」 ``` 关键点:**一次构建产出客户端包和服务端包**,版本号绑定,避免两端逻辑漂移。 ### 3.2 客户端:进入 → 下载 → 加载 → 运行 ``` 玩家点击某小游戏 → 框架查 manifest: 对比本地 md5, 增量下载缺失/变更文件 (复用 LoadDll, 断点续传/重试) → 校验签名+md5 → 加载该小游戏全部 AB(.unity3d):含脚本 AB 与美术 AB → 从脚本 AB 取出 Core/Client 的 .bytes(TextAsset) → HybridCLR.LoadMetadataForAOTAssembly + Assembly.Load(Core, Client) → 反射实例化 game.json 指定的 IGameClient 入口 → 框架建小游戏根节点/UI根, 注入 ctx(资源加载器/网络通道/退出回调) → IGameClient.OnEnter() 接管; OnUpdate 每帧驱动 退出: IGameClient.OnExit() → 卸载AB → 卸载小游戏根 → (DLL常驻, 内存可控) ``` 微信小游戏分支:传输强制 WebSocket,代码下载走 WX 文件系统缓存。**热更代码下载需符合微信平台政策**(见 6.6 降级策略)。 ### 3.3 对局流程(权威服务端 + 轻实时状态同步) ``` 客户端 服务端(网关/房间宿主) │── 请求匹配(gameId,version) ──▶ 匹配队列(按gameId+version分桶) │ ├─ 凑齐N人 或 15s超时→AI兜底(沿用RPS规则) │ ├─ 房间宿主: 若未加载该版本Server DLL │ │ → 用可卸载ALC加载 .Server.dll [B1] │ └─ new IGameServerRoom, OnRoomStart(players) │◀──── 房间就绪/初始状态快照 ─────┤ │ │ 服务端权威循环: │── 玩家输入(intent) ──────────▶ OnMessage → Core.Step() 推进权威状态 │ │ OnTick(10-20Hz): 生成状态快照/增量 │◀──── 状态快照/事件广播 ────────┤ (轻实时: 客户端可用Core.Step本地预演+插值) │ IGameClient渲染表现 │ │── ... 直到结束 ──────────────▶ OnRoomEnd → 结算落库 → 通知客户端 │◀──── 结算结果 ────────────────┤ → ALC 在无房间引用时可卸载(热更新版本) ``` **权威性保证**:胜负/随机/关键状态只认服务端 `Core.Step` 的结果;客户端 `Core.Step` 仅用于预演和插值,最终以服务端快照校正。两端跑同一份 Core,天然一致。 ## 4. 服务端运行时架构 ### 4.1 进程内组件(单进程,B1) ``` ┌──────────────────────── 服务端主进程 (.NET 8+) ──────────────────────────┐ │ [接入层] WebSocket 网关 (Kestrel/ASP.NET Core) │ │ ├─ 连接/鉴权/心跳, 统一收发框架协议帧 │ │ └─ Session 管理(playerId ↔ 连接), 断线重连窗口 │ │ [路由层] 消息路由: 框架消息(匹配/心跳/大厅) vs 小游戏消息(转给房间) │ │ [匹配服务] 按 (gameId, version) 分桶队列 │ │ ├─ 凑齐N人建房; 15s超时→请求房间用该游戏AI兜底 │ │ └─ 不跨版本匹配(避免 Core 不一致) │ │ [房间宿主 RoomHost] ★核心 │ │ ├─ GameModuleRegistry: gameId+version → 已加载的 ALC + 入口工厂 │ │ ├─ ALC 加载器: 可卸载 AssemblyLoadContext, 按需加载 .Server.dll │ │ │ + 对应 Core.dll; 注入服务端能力(IRandom权威/ITimer/日志/存储) │ │ ├─ Room 实例池: 每对局 1 个 IGameServerRoom; 统一 Tick 调度(10-20Hz) │ │ └─ 房间结束回收; 某版本无房间引用 → 标记可卸载 │ │ [基础设施] 持久化(对局结果/玩家数据) · 日志/监控 · 配置 · 发布通知订阅 │ └──────────────────────────────────────────────────────────────────────────┘ ``` ### 4.2 ALC 生命周期(服务端热更核心) ``` 新版本发布通知到达 → 房间宿主创建新的可卸载 ALC, 加载 .Server.dll vN+1 → 新匹配的房间用 vN+1; 旧房间(vN)继续用旧 ALC 跑完, 互不影响 (版本共存) → 旧版本所有房间结束 + 无引用 → ALC.Unload() + GC → 内存回收 约束: - Core/Server DLL 与框架之间只通过框架程序集里的接口交互 (接口类型必须在「框架默认ALC」, 不随小游戏卸载, 否则类型无法跨界) - 小游戏 DLL 不得静态持有跨房间/跨版本的全局状态(否则阻碍卸载) ``` **不重启热更**的关键:老对局用老逻辑跑完,新对局用新逻辑,平滑切换;绝不强制踢掉在玩的对局。 ### 4.3 技术栈 | 关注点 | 选择 | 理由 | |---|---|---| | 运行时 | .NET 8 LTS(或更新 LTS) | `AssemblyLoadContext.Unload` 成熟,性能好,跨平台 | | 接入 | ASP.NET Core Kestrel + WebSocket | 多端统一 WS(微信小游戏只能 WS),生态成熟 | | 协议编解码 | 共享层(框架协议)+ 各小游戏 Core 自定义消息 | 两端同一份编解码,杜绝不一致 | | 调度 | 单线程 per-room 逻辑 + 房间分配到线程池/channel | 房间内逻辑无锁、确定性好;房间间并行 | | 持久化 | 先用轻量(SQLite/文件)跑通,接口抽象预留 MySQL/Redis | 内部阶段不过度设计 | ### 4.4 确定性与随机 权威 `IRandom` 由服务端房间持有种子,关键随机只在服务端 `Core.Step` 产生;客户端预演用服务端下发的种子/结果校正。保证"客户端预测—服务端权威"一致,也方便回放/复盘。 ## 5. 关键接口契约 ```csharp // —— 共享核心层 (.Core, 纯 C#) —— public interface IGameLogic { TState CreateInitial(RoomConfig cfg, IRandom rng); StepResult Step(TState s, TInput input, IRandom rng); // 纯函数式权威推进 byte[] Encode(TState s); TState Decode(byte[] data); // 快照编解码(两端共用) } // —— 框架提供的平台能力(注入给小游戏, 隔离平台差异) —— public interface IRandom { int Next(int max); } // 服务端权威/客户端用下发种子 public interface ITimer { float Now { get; } } public interface ILogger { void Info(string m); /* ... */ } public interface IStorage { /* ... */ } // 仅服务端注入 // —— 客户端薄层入口 —— public interface IGameClient { void OnEnter(IGameClientCtx ctx); // ctx: 资源加载/网络发送/退出 void OnNetMessage(NetMessage msg); void OnUpdate(float dt); void OnExit(); } // —— 服务端薄层入口 —— public interface IGameServerRoom { void OnRoomStart(IReadOnlyList players, IRoomCtx ctx); void OnMessage(int playerId, NetMessage msg); void OnTick(float dt); // 10-20Hz, 内部调用 Core.Step + 广播快照 void OnRoomEnd(); } ``` > 这些接口类型**定义在常驻框架程序集**(不随小游戏 ALC 卸载),保证跨边界类型一致。 ## 6. 错误处理与容错 | 场景 | 处理 | |---|---| | 客户端下载失败/md5 不符 | 重试 + 断点续传;超阈值回大厅报错,不进半成品小游戏(复用 LoadDll 现有逻辑) | | 签名/版本不兼容 | 拒绝加载,提示更新框架版本 | | 小游戏 Server 逻辑抛异常 | 房间隔离:捕获→该房间安全结束并落败/重开,不拖垮主进程;记录日志告警 | | 玩家断线 | 重连窗口内恢复 Session;超时→AI 接管或判负(按 game.json 配置) | | ALC 卸载失败(有泄漏引用) | 监控驻留版本数;告警并保留旧 ALC,绝不强卸导致崩溃 | | 微信小游戏热更代码政策受限(6.6) | 合规审核通道发版 / 必要时主包内置基础小游戏,热更仅增量 | ### 6.6 微信小游戏热更风险(需重点验证) 微信小游戏平台对"下载并执行远端代码"有政策约束。落地前必须验证 HybridCLR 在 WX-WASM 环境下的热更代码下载是否合规。降级策略:基础/首发小游戏内置主包,热更仅做增量与资源更新;正式发版走微信审核通道。 ## 7. 安全(内部可信,从轻) - **签名校验**:发布产物全部签名,客户端/服务端加载前验签(防中间人篡改 DLL)。 - **传输**:WSS(TLS)。 - **来源限定**:只加载发布服务登记过的 manifest,不接受任意 URL。 - 不上进程沙箱 / 字节码校验(内部信任,YAGNI);第 5 节的能力接口注入天然限制小游戏可触达的 API,预留将来收紧空间。 ## 8. 测试策略 | 层 | 测试方式 | |---|---| | Core(纯 C#) | 单元测试:`Step` 确定性、编解码往返、规则边界。两端共用 Core,测一次双端受益 | | 协议编解码 | 往返一致性 + 跨版本兼容用例 | | 服务端房间 | 用假网络/假玩家驱动 `IGameServerRoom`,含断线/超时/AI 兜底用例 | | ALC 热更 | 集成测试:加载 v1→建房→发布 v2→新房用 v2、旧房跑完→v1 卸载、内存回收断言 | | 端到端 | 用 RPS 跑通:匹配→对局→结算→热更换版本 | ## 9. 渐进落地路线(每步可独立验证) 1. **框架接口与协议层**:定义第 5 节接口、网络协议编解码(共享层),先不接小游戏。 2. **服务端骨架**:Kestrel WS 网关 + Session + 匹配 + RoomHost + ALC 加载器(先加载一个 hello 房间验证热更/卸载闭环)。 3. **客户端骨架**:扩展 `LoadDll` 支持小游戏 manifest 维度 + HybridCLR 加载小游戏 DLL + 生命周期接管。 4. **构建/发布工具链**:一键产出两端包 + manifest + 签名 + 上传(A1 的双编译)。 5. **试金石:用 C# 重写 RockPaperScissors**(Core 规则 + 客户端薄层 + 服务端薄层 + AI 兜底),跑通端到端 + 热更换版本。 6. **沉淀模板**:把 RPS 抽成"新小游戏脚手架模板",验证"写一个新小游戏 = 框架零改动"。 每一步对应第 8 节测试,独立验证后再进入下一步。 ## 10. 待跟进 / 风险清单 ### 10.1 「脚本+美术统一打 AB」改造清单(相对已实现代码的增量) 现有实现把热更 DLL 落成**独立 md5 命名的 `.bytes`**、与美术 AB 走两条管线;本设计(§2.2/§2.4)改为**DLL 以 `.bytes` 打进 AB、与美术同走一条 AB 管线**。需改造(其余模块零改动): - **客户端目录迁移**:已完成——`RockPaperScissors` 迁入 `Client/Assets/MiniGames/RockPaperScissors/`(`Art→res`、`script→scripts`,保留 `.meta` GUID)。新增小游戏一律按 §2.4 落在 `MiniGames//`。 - **Editor 构建(`Client/Assets/Editor/MiniGamePublishMenu.cs`)**:第 3 步由「DLL→.bytes 旁路 + 美术打 AB」改为「DLL→.bytes 作为 TextAsset 与 `res/` 一起 `BuildAssetBundles`」,产物全是 `.unity3d`。 - **发布布局(`Server/PublishTool/PublishLayout.cs` + `GameSpec`/`GameJsonWriter`)**:客户端侧不再 `CopyBytes` 单独落 DLL `.bytes`;改为把脚本 AB 与美术 AB 一并按 md5 命名落盘并登记进 `files.txt`;`game.json` 用 AB 名(含脚本 AB)替代 `coreDll`/`clientDll` 字段语义。同步更新 `PublishTool.Tests`。 - **客户端加载器(`Client/Assets/Script/xmain/MiniGame/`)**:`MiniGameAssemblyLoader` 由 `File.ReadAllBytes(.bytes)` 改为从已加载 AB 取 `TextAsset` 字节再 `Assembly.Load`;`MiniGameDownloader` 不再特判 `.bytes`(全部条目按 AB 增量下载);`MiniGameManifest` 解析 game.json 的 AB 字段。**须 Unity Editor/真机核对**(本环境无 Unity,无法验证 HybridCLR 从 AB 加载 DLL)。 ### 10.2 其他 - 微信小游戏热更代码政策合规性(6.6)——上线前必须验证。 - 现有 `Doc/Rule/UnityProject.md` 的"小游戏用 Lua"规则需更新为 C# 路线。 - HybridCLR 在 WASM 下对小游戏热更 DLL 的 AOT 泛型/裁剪约束需实测。 - ALC 卸载的内存泄漏边界(静态字段、事件订阅、未释放的 native 句柄)需在第 2 步集成测试中重点验证。