4.9 KiB
结算结果传递(RoomEndResult)设计
日期:2026-07-09
状态:已与用户确认(方案 A:IRoomCtx.EndRoom 加带结果重载)
背景 / 问题
对局结束后框架结算弹框只能显示"游戏结束 + 房间号":网关 ServerLoop.OnRoomEnded 硬编码
RoomEndMsg { WinnerPlayerId = 0, ResultBlob = empty }(ServerLoop.cs:412),而游戏逻辑
(RpsServerRoom)的胜负与比分只写了日志,框架 IRoomCtx.EndRoom() 没有传结果的通道。
这是既有占位欠账(见 minigame 进度备忘"RoomEndMsg.WinnerPlayerId=0 是占位")。
协议层 RoomEndMsg(FrameworkMessages.cs:219)早已有 WinnerPlayerId + ResultBlob
两字段,客户端 MiniGameHost.ShowResultDialog 也已按这两字段渲染——缺的只是服务端从
游戏逻辑到网关的管道。零协议改动。
用户确认的结算内容:胜负 + 比分(标题胜利/失败/平局,正文最终比分文本)。
设计
1. 共享接口(Client/Assets/Framework/Shared/)
(实施修订:RoomEndResult 按仓库 DTO 惯例落在 GameTypes.cs,接口重载在 IGameServerRoom.cs。)
public sealed class RoomEndResult
{
public int WinnerPlayerId; // 0 = 平局/无胜者(与 RoomEndMsg 语义一致)
public byte[] ResultBlob; // 小游戏自定义结算数据;约定:放 UTF-8 文本时框架弹框直接显示
}
public interface IRoomCtx
{
// ...现有成员不变...
void EndRoom(); // 保留:等价 EndRoom(null)
void EndRoom(RoomEndResult result); // 新增
}
兼容性:游戏 DLL 只消费不实现 IRoomCtx,给接口加成员不破坏已发布旧模块
(旧模块调无参版 → 结果为 null → 弹框行为同现状)。实现方仅框架内 RoomCtx 与测试
fake,一起改。不使用默认接口方法(避开 HybridCLR/mono DIM 运行时风险)。
2. 服务端宿主(Server/Server.Host/)
RoomCtx:构造参数Action endRoom→Action<RoomEndResult>;EndRoom()→ 回调(null);EndRoom(result)→ 回调(result)。Room:RequestEnd(RoomEndResult result)存入public RoomEndResult EndResult属性(多次调用以最后一次为准);保留无参RequestEnd()= 传 null。RoomHost.TickAll返回IReadOnlyList<string>→IReadOnlyList<EndedRoom>:public readonly struct EndedRoom { public string RoomId; public RoomEndResult Result; }, 在ReleaseRoom前捕获room.EndResult。现有使用 TickAll 的调用点与测试同步更新。- "Start 即结束"路径:
ServerLoop.CreateRoomForSeats持有Room引用,直接读room.EndResult。
3. 网关(Server/Gateway/ServerLoop.cs)
OnRoomEnded(string roomId, RoomEndResult result):
var msg = new RoomEndMsg {
WinnerPlayerId = result?.WinnerPlayerId ?? 0,
ResultBlob = result?.ResultBlob ?? Array.Empty<byte>()
};
4. RPS 服务端(Server/games-src/RPS/RPS.Server/RpsServerRoom.cs)
OnRoomStart记录_pidOfSeat[2](座位→pid;AI 玩家也有 pid)。- 结束时(
Phase == Finished):Winner == 2(平局)→WinnerPlayerId = 0,文本"平局 {s0} : {s1}";- 否则 →
WinnerPlayerId = _pidOfSeat[winner],文本"最终比分 {s0} : {s1}"。 - 文本按座位序,双端同文本(广播);UTF-8 编码进
ResultBlob,调EndRoom(result)。
5. 客户端弹框(Client/Assets/Script/xmain/MiniGame/MiniGameHost.cs,框架层)
RPS 客户端零改动。
ResultTitle:winner == _selfPlayerId(非 0)→ "胜利";winner != 0且非自己 → "失败";winner == 0:blob 有文本 → "平局";blob 空 → "游戏结束" (旧模块/异常结束无结果时保持中性标题,不误报平局)。
ResultMessage:blob 解出非空文本 → 只显示 blob 文本(去掉裸 pid 行); 空 → 维持现状回退(房间号)。
测试(TDD)
- Server.Host.Tests:①
RoomCtx.EndRoom(result)→Room.EndResult捕获; ②TickAll返回的EndedRoom携带结果;③未设结果时Result == null; ④RpsServerRoomTests:对局打完,以正确胜者 pid 与比分文本调了EndRoom(含平局→pid 0 分支)。 - Gateway.Tests:RPS 端到端断言收到的 RoomEnd 帧
WinnerPlayerId/ResultBlob非占位;build-test-games.sh重建 rps 测试夹具(RPS.Server 源码变更)。 - 客户端(本环境无 Unity):IDE 诊断/csc 0 错;用户 Unity Play 一局人工核对弹框 (胜利/失败/平局标题 + 比分文本,点确定正常退出)。
部署影响
- 服务端:网关 + Server.Host 一体重编部署即生效。
- RPS 服务器模块:须经发布流水线重发
deploy/minigame/rps/<ver>/(旧包兼容不崩,只是弹框无结算内容)。 - 客户端:
XWorld.Link与XWorld.Framework.Shared热更 DLL 重出。
不做(YAGNI)
SetResult()两步接口(无场景);- 结构化结算(每回合出招记录等,用户选了胜负+比分);
- 宿主强制结束时的结果通道(结果恒 null → 弹框中性"游戏结束",可接受)。