# 小游戏开发设计文档 ## 1. 目标与适用范围 本文档定义本项目小游戏的统一开发、接入、发布与联调规范。与参考项目中基于 `MiniGameStage` / `IMiniGame` 的内嵌 Stage 方案不同,本项目已落地独立热更小游戏框架:小游戏以独立版本包发布,客户端通过 `XGame.MiniGame` 宿主下载、校验、加载热更 DLL 与资源 AssetBundle,并通过统一网络帧与服务端房间逻辑通信。 适用范围: - 新增一个独立小游戏,例如 `RockPaperScissors`、`FishingQuick`、`TreasureCatch`。 - 维护小游戏客户端热更逻辑、共享 Core 逻辑、服务端房间逻辑。 - 生成小游戏 UI Prefab、贴图、音效等资源。 - 发布小游戏客户端 CDN 包和服务端 deploy 包。 - PC 本地 smoke、网关/CDN 联调、真机资源更新排查。 核心目标: 1. 小游戏逻辑全部使用 C# 实现,通过热更 DLL 加载,不使用 Lua 写小游戏专属逻辑。 2. 小游戏与主工程解耦:主工程只保留宿主、下载、加载、网络、结算和基础服务。 3. 小游戏客户端、服务端共享同一份 Core 规则,避免双端规则漂移。 4. 资源加载统一异步,避免真机读取旧底包资源。 5. 发布产物有版本、MD5、签名和框架版本门槛,加载失败时安全拒绝进入。 ## 2. 总体架构 本项目小游戏由四层组成: ```text 大厅/入口层 ↓ MiniGameHost(主工程常驻宿主) ↓ 热更小游戏客户端 DLL(IGameClient) ↓ 共享 Core DLL + 服务端房间 DLL(IGameServerRoom) ``` ### 2.1 大厅/入口层 大厅负责决定进入哪个小游戏、使用哪个版本、连接哪个网关和 CDN。进入小游戏时将以下信息传给宿主: - `cdnBase`:小游戏 CDN 根地址,例如本地 `http://127.0.0.1:15081/`。 - `gameId`:小游戏标识,例如 `rps`。 - `version`:小游戏版本号。 - `XWebSocket`:已连接的网关 socket。 - 是否由宿主发送匹配请求。 大厅链路如果已经消费了 `MatchFound`,需要通过 `MiniGameHost.SetMatchInfo(roomId, selfPlayerId)` 把房间信息注入宿主,确保结算弹窗能判断胜负。 ### 2.2 宿主层 宿主层在 `Client/Assets/Script/xmain/MiniGame/` 下,主要类如下: | 类 | 职责 | |---|---| | `MiniGameHost` | 小游戏会话宿主,负责下载、加载、创建 `IGameClient`、每帧驱动、结算弹窗和退出清理。 | | `MiniGameDownloader` | 从 CDN 下载 `game.json`、`files.txt`、`files.txt.sig` 和所有清单文件,校验版本、框架版本、签名和 MD5。 | | `MiniGameManifest` | 客户端 `game.json` 视图,包含 `gameId`、`version`、DLL 名、入口类型、资源 AB 列表和文件 MD5 清单。 | | `MiniGameAssemblyLoader` | 从代码 AB 中取出 Core/Client DLL `.bytes`,按 Core → Client 顺序 `Assembly.Load`,反射创建 `IGameClient`。 | | `MiniGameAssetLoader` | 实现 `IAssetLoader`,从已下载的小游戏资源 AB 中异步回调返回 Unity 对象,退出时统一卸载。 | | `MiniGameNetChannel` | 负责 socket 收发、帧解码和 Channel 分发;Game 帧给小游戏,Framework 帧给宿主。 | | `GameClientCtx` | 注入给小游戏客户端的上下文,包含资源、日志、发送 Game 消息、退出请求。 | 宿主只处理通用流程,不写任何具体小游戏玩法逻辑。 ### 2.3 热更小游戏层 每个小游戏至少包含: - `Core`:纯 C# 共享规则、状态、消息编解码、胜负判定,不引用 UnityEngine。 - `Client`:实现 `XWorld.Framework.IGameClient`,负责 UI、输入、表现、客户端状态展示、通过通用玩家服务显示房间用户信息,并向服务端发送 Game 消息。 - `Server`:实现 `XWorld.Framework.IGameServerRoom`,负责权威房间逻辑、AI 兜底、Tick、广播快照和结束房间。 - `res`:Prefab、贴图、音效、动画等资源,打包为资源 AB。 - `publish.json`:发布配置,描述 DLL、入口类型、玩家人数、tickRate 等元信息。 ### 2.4 服务端层 服务端加载 `deploy/minigame/{gameId}/{version}/` 下的服务端产物,通过 `GameModuleLoader` 校验 `minFrameworkVersion` 后加载 `IGameServerRoom`。服务端房间只接收并广播 Game 通道消息;匹配、房间开始、房间结束等框架流程由宿主与网关处理。 ## 3. 推荐目录规范 ### 3.1 客户端小游戏目录 ```text Client/Assets/MiniGames/{GameId}/ ├── README.md ├── publish.json ├── scripts~/ │ ├── Core/ │ │ ├── GameTypes.cs │ │ ├── GameLogic.cs │ │ └── GameCodec.cs │ └── Client/ │ └── GameClient.cs └── res/ ├── UI/ │ ├── Prefab/ │ │ └── UI_{GameId}.prefab │ └── Texture/ │ └── game_icon.png ├── Sound/ └── Effect/ ``` 说明: - `Assets/MiniGames/{GameId}/res` 会被发布流水线收集并打进 `res.unity3d`。 - `scripts~` 为 Unity 隐藏源码目录,避免直接进入 Unity AssetDatabase;实际编译由对应 csproj 控制。 - 示例可参考 `Client/Assets/MiniGames/RockPaperScissors`。 ### 3.2 客户端热更工程目录 ```text Client/HotUpdateGames/{GameId}.Client/ └── {GameId}.Client.csproj ``` 客户端 csproj 编译出 `{GameId}.Client.dll`。如果 `Core` 使用单源共享,Client 工程应链接同一份 Core 源码或引用发布流程指定的 Core DLL,保证 `publish.json` 中的 `coreDll` 和 `clientDll` 能被流水线找到。 ### 3.3 服务端小游戏目录 ```text Server/games-src/{GameId}/ ├── {GameId}.Core/ │ └── {GameId}.Core.csproj └── {GameId}.Server/ ├── {GameId}.Server.csproj └── GameServerRoom.cs ``` 服务端工程编译出: - `{GameId}.Core.dll` - `{GameId}.Server.dll` 服务端薄层应尽量只处理房间上下文、玩家输入、AI、广播和房间结束,玩法规则放在 Core。 ### 3.4 发布产物目录 ```text CDN/minigame/{gameId}/{version}/{platform}/ ├── game.json ├── files.txt ├── files.txt.sig ├── code_.unity3d └── res_.unity3d deploy/minigame/{gameId}/{version}/ ├── game.json ├── files.txt ├── files.txt.sig ├── {server/core dll md5 命名产物} └── {server room dll md5 命名产物} ``` 客户端只有在 CDN 侧完整产物落位后,服务端版本才应该生效。发布流水线当前按“客户端先落 CDN,最后落 deploy”的顺序处理,避免服务器广播了新版本但 CDN 缺包。 ## 4. `publish.json` 规范 每个小游戏根目录必须包含 `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 } ``` 字段说明: | 字段 | 说明 | |---|---| | `gameId` | 小游戏唯一标识,推荐小写短名,例如 `rps`。 | | `serverSrcDir` | 服务端源码目录,相对 `Server/games-src/`。 | | `serverProject` | 服务端 room 工程路径,相对 `Server/games-src/{serverSrcDir}/`。 | | `serverAssembly` | 服务端房间 DLL 文件名。 | | `serverEntryType` | 实现 `IGameServerRoom` 的完整类型名。 | | `clientProject` | 客户端热更工程路径,相对仓库根目录。 | | `coreDll` | 共享 Core DLL 文件名。 | | `clientDll` | 客户端 DLL 文件名。 | | `clientEntryType` | 实现 `IGameClient` 的完整类型名。 | | `minFrameworkVersion` | 最低框架版本;高于当前 `FrameworkInfo.Version` 时客户端/服务端均拒绝加载。 | | `playerCount` | 匹配人数。 | | `tickRateHz` | 服务端房间 Tick 频率,常用 10-20Hz。 | ## 5. 客户端接口与生命周期 ### 5.1 `IGameClient` 小游戏客户端入口必须实现 `XWorld.Framework.IGameClient`: ```csharp public interface IGameClient { void OnEnter(IGameClientCtx ctx); void OnNetMessage(NetMessage message); void OnUpdate(float deltaTime); void OnExit(); } ``` 职责划分: - `OnEnter`:缓存 `ctx`,读取房间玩家列表,加载 UI/资源,初始化本地状态,绑定按钮事件。此时房间可能尚未 ready,不要依赖 `MatchFound` 已到达。 - `OnNetMessage`:处理服务端 Game 通道消息,例如快照、对手操作、倒计时、结算预告。 - `OnUpdate`:处理客户端表现层更新、倒计时显示、动画过渡和输入冷却。权威胜负不要放在客户端。 - `OnExit`:解绑事件、销毁 UI、停止计时器、清理对象引用。 ### 5.2 `IGameClientCtx` 宿主注入的上下文: ```csharp public interface IGameClientCtx { IAssetLoader Assets { get; } ILogger Logger { get; } void Send(NetMessage message); void Exit(); } ``` 使用约束: - 加载资源只能通过 `ctx.Assets.Load(path, onLoaded)`,不要直接同步 `AssetBundle.LoadAsset` 或 `Resources.Load`。 - 房间玩家列表和玩家展示数据通过 `ctx.Players` 读取或异步请求,不要在小游戏内重复实现玩家信息协议。 - 向服务端发送玩法消息使用 `ctx.Send(new NetMessage(opcode, payload))`。 - 玩家主动退出时调用 `ctx.Exit()`,不要直接销毁宿主或 socket。 - 日志使用 `ctx.Logger`,便于按小游戏前缀过滤。 ### 5.3 房间玩家与用户数据服务 通用层应向小游戏提供统一的房间玩家列表和玩家展示数据查询能力,避免每个小游戏自行拼协议、缓存昵称或重复请求角色数据。 建议在框架共享层扩展通用数据结构: ```csharp public sealed class PlayerInfo { public int PlayerId; public string Name; public bool IsAI; public int Level; public int AppearanceType; public int AvatarId; } public interface IRoomPlayerService { IReadOnlyList GetRoomPlayers(); bool TryGetRoomPlayer(int playerId, out PlayerInfo player); void RequestPlayerInfo(int playerId, Action onLoaded); } ``` 客户端建议通过 `IGameClientCtx` 暴露该服务: ```csharp public interface IGameClientCtx { IAssetLoader Assets { get; } ILogger Logger { get; } IRoomPlayerService Players { get; } void Send(NetMessage message); void Exit(); } ``` 语义约定: - `GetRoomPlayers()` 返回当前房间玩家快照,至少包含 `PlayerId`、`Name`、`IsAI`。 - `TryGetRoomPlayer()` 用于读取已缓存的玩家数据,不触发网络请求。 - `RequestPlayerInfo()` 用于异步补全昵称、等级、头像、外形类型等展示字段,内部由通用层复用主工程已有玩家信息协议或缓存。 - AI 玩家由框架或小游戏服务端提供稳定的 `PlayerInfo`,例如 `Name = "AI"`、`IsAI = true`,不再向账号服查询。 - 小游戏只消费 `PlayerInfo`,不要直接依赖主工程角色系统、排行榜面板或具体协议类。 服务端侧 `IGameServerRoom.OnRoomStart(IReadOnlyList players, IRoomCtx ctx)` 已能拿到房间玩家列表;后续可把 `PlayerInfo` 扩展为同一套基础展示字段,让服务端可按等级、外形类型或 AI 标记做玩法初始化。服务端仍然只信任房间上下文传入的玩家数据,不接受客户端上报的名称、等级或外形。 客户端显示玩家信息的推荐流程: ```text IGameClient.OnEnter(ctx) ↓ ctx.Players.GetRoomPlayers() 显示基础座位和占位信息 ↓ 对缺失展示字段的真人调用 ctx.Players.RequestPlayerInfo(playerId, callback) ↓ 回调中刷新昵称、等级、头像、外形类型 ↓ 后续 Game 快照只携带 playerId,通过本地玩家缓存映射到展示信息 ``` 这样小游戏消息只需要传 `playerId`,玩家展示信息由通用层统一维护,减少带宽和重复实现。 ### 5.4 客户端进入流程 ```text 大厅选择小游戏 ↓ MiniGameHost.EnterGame(cdnBase, gameId, version, socket) ↓ MiniGameDownloader.Run ↓ 下载 game.json / files.txt / files.txt.sig / AB 文件 ↓ 校验 gameId、version、minFrameworkVersion、签名、MD5 ↓ MiniGameAssemblyLoader.Load ↓ 加载 Core DLL,再加载 Client DLL ↓ 反射 clientEntryType,创建 IGameClient ↓ 创建 MiniGameNetChannel + MiniGameAssetLoader + GameClientCtx ↓ 必要时发送 FrameworkOpcode.MatchRequest ↓ IGameClient.OnEnter(ctx) ↓ 每帧 Pump 网络 + IGameClient.OnUpdate(deltaTime) ``` ### 5.5 客户端退出流程 ```text 玩家主动退出 / 房间结束确认 / 宿主异常兜底 ↓ MiniGameHost.ExitGame() ↓ 销毁结算弹窗 ↓ IGameClient.OnExit() ↓ 清空网络通道和 ctx ↓ MiniGameAssetLoader.UnloadAll() ↓ OnGameExited 回调大厅 ``` 小游戏不得绕过 `ctx.Exit()` 或宿主 `ExitGame()` 自行切主流程。 ## 6. 服务端接口与生命周期 服务端房间入口必须实现 `XWorld.Framework.IGameServerRoom`: ```csharp public interface IGameServerRoom { void OnRoomStart(IReadOnlyList players, IRoomCtx ctx); void OnMessage(int playerId, NetMessage message); void OnTick(float deltaTime); void OnRoomEnd(); } ``` 推荐职责: - `OnRoomStart`:缓存玩家、初始化 Core 状态、发送初始快照、启动倒计时。 - `OnMessage`:校验玩家输入合法性,转换为 Core 操作,不信任客户端结果。 - `OnTick`:以 `tickRateHz` 调用 Core Step,必要时广播快照。 - `OnRoomEnd`:释放房间内临时状态。 `IRoomCtx` 提供权威随机、计时器、存储、日志、单播、广播和结束房间能力。小游戏结束时调用: ```csharp ctx.EndRoom(new RoomEndResult { WinnerPlayerId = winnerId, ResultBlob = resultBytes }); ``` `ResultBlob` 建议放 UTF-8 文本,客户端宿主会解码并显示在通用结算弹窗中。文本不应超过 4096 字节。 ## 7. 网络消息设计 ### 7.1 Channel 分工 | Channel | 消费方 | 内容 | |---|---|---| | `Framework` | 宿主/框架 | 匹配请求、`MatchFound`、`RoomEnd`、心跳等通用流程。 | | `Game` | 小游戏客户端/服务端 | 玩法输入、状态快照、回合事件、表现提示等。 | 小游戏客户端只处理 `IGameClient.OnNetMessage` 收到的 Game 消息;不要解析 Framework 帧。宿主会在 `MiniGameNetChannel` 中把 Framework 帧分给 `MiniGameHost.OnFrameworkFrame`。 ### 7.2 Opcode 规划 每个小游戏自行维护 Game 通道 opcode。建议: ```text 1-99 客户端 → 服务端输入 100-199 服务端 → 客户端快照/事件 200-299 双向调试/扩展 ``` 同一小游戏内 opcode 必须稳定,不同小游戏之间可以独立编号。消息编解码放在 Core,客户端和服务端复用,避免协议漂移。 ### 7.3 权威性原则 - 客户端只发送意图,例如“选择石头”“点击发射”“移动方向”。 - 服务端校验时序、玩家身份、冷却、距离、分数和胜负。 - 客户端显示可以预测,但最终结果以服务端快照/RoomEnd 为准。 - 随机数使用服务端 `IRoomCtx.Random`,不要客户端自判随机结果。 ## 8. UI 与资源规范 ### 8.1 UI 制作流程 本项目禁止手写 prefab YAML。小游戏 UI 必须遵循 `Client/Assets/Doc/Rule/UnityProject.md` 的 JSON → Prefab 管线: 1. 在 `Client/Assets/Doc/UIPrefabCreater/` 新建 UI JSON,例如 `UI_RockPaperScissors.json`。 2. JSON 中声明 `schemaVersion`、`prefabName`、`prefabPath`、`canvas`、`assets`、`nodes`、`bindings`、`events`。 3. `canvas.referenceResolution` 默认使用 `[2048, 1024]`。 4. 文本使用 `TextMeshProUGUI`。 5. 需要运行时脚本时声明 `MonoBehaviour` 组件和 `typeName`。 6. 切回或重新激活 Unity,让工具自动生成 Prefab。 7. Prefab 生成后移动或直接输出到 `Assets/MiniGames/{GameId}/res/UI/Prefab/`,由发布流水线打入资源 AB。 运行时客户端 C# 通过 `transform.Find`、递归按名查找、`GetComponent