房间系统设计
概述
棋牌、桌游、休闲竞技类游戏的核心玩法都围绕房间展开:玩家先进入一个房间,凑齐人数后开局对战。房间是游戏服务器的"最小对战单元",承担状态管理、成员管理、消息广播三大职责。本文设计完整的房间系统:房间创建/加入/退出/解散、房间状态机(Waiting/Ready/Playing/End)、房间属性配置。
一、房间在游戏中的角色
1.1 为什么需要房间
对战的最小组织单元:
2-10 人凑在一起才开局
房间内消息高频广播(出牌、移动、结算)
房间状态决定玩法阶段(等待/进行/结束)
与玩家的关系:
一个玩家同时只在一个房间(或大厅)
玩家 ↔ 房间 一对多(多局历史)但同时一对单一1.2 房间分类
| 类型 | 说明 | 例子 |
|---|---|---|
| 匹配房 | 系统撮合,随机进入 | 快速开始 |
| 好友房 | 邀请好友,私人房间 | 组队开黑 |
| 练习房 | 人机/单人 | 单人练习 |
| 比赛房 | 赛事专用配置 | 锦标赛 |
设计上:统一 Room 模型,不同 mode 配置不同规则二、房间创建与加入
2.1 创建流程
1. 玩家请求建房(携带配置:模式、人数、是否私人)
2. 服务器校验:次数限制、配置合法
3. 生成房间:roomId + 配置 + 房主
4. 房主加入房间
5. 返回房间信息给房主
私人房附加:
生成邀请码(如 6 位数字/字母)
好友凭码加入2.2 加入流程
1. 玩家请求加入(房号/匹配/邀请)
2. 校验:
房间存在、未开局、未满员
玩家不在其他房间
私人房校验邀请码
3. 加入成员列表,广播"新玩家进入"
4. 同步房间当前状态给新玩家java
public class RoomService {
public Room createRoom(long playerId, RoomConfig config) {
validateConfig(config);
Room room = new Room(nextRoomId(), config);
room.setOwnerId(playerId);
room.addMember(new Member(playerId, MemberState.NOT_READY));
roomManager.add(room);
return room;
}
public void joinRoom(Room room, long playerId) {
if (!room.canJoin()) {
throw new GameException("房间不可加入");
}
room.addMember(new Member(playerId, MemberState.NOT_READY));
// 广播新成员
room.broadcast(new RoomEvent("PLAYER_JOIN", playerId));
}
}2.3 加入时机与房间恢复
玩家掉线后重连:
还在房间宽限期内 → 恢复回房间
已离开 → 重新加入/重新匹配
断线期间房间可继续(托管/等待,按规则)三、房间状态机
3.1 四个状态
| 状态 | 含义 | 关键动作 |
|---|---|---|
Waiting | 等待玩家 | 加入/离开/准备 |
Ready | 全员就绪 | 开局倒计时 |
Playing | 对局中 | 回合流转、结算 |
End | 对局结束 | 结算展示、返回大厅 |
状态流转:
Waiting ──全员准备──▶ Ready ──倒计时结束──▶ Playing
Playing ──胜负判定──▶ End ──确认/超时──▶ 解散或回 Waiting
任何状态 → 解散(成员全离/房主离开/超时)3.2 状态机的实现
java
public class Room {
private int state = STATE_WAITING;
private final List<Member> members = new CopyOnWriteArrayList<>();
public boolean changeState(int from, int to) {
// CAS 式状态切换,防并发乱流转
synchronized (this) {
if (this.state != from) {
return false;
}
this.state = to;
return true;
}
}
public void onReady(long playerId) {
if (state != STATE_WAITING) {
return;
}
member(playerId).setReady(true);
if (allReady() && members.size() >= config.getMinPlayers()) {
// 触发开局倒计时
changeState(STATE_WAITING, STATE_READY);
}
}
}状态机要点:
状态切换必须校验前置状态(非法流转拒绝)
单房间操作串行(房间锁),防并发
每个状态定义"允许的消息"(非法消息拒绝)3.3 开局条件
开局判定(Ready 阶段):
人数达标(minPlayers 起)
全员就绪(或房主强制开始)
配置校验(模式、局数、筹码一致)
开局动作:
状态 → Playing
生成对局:发牌/分配位置/决定先手
广播开局消息(含种子、座位信息)四、退出与解散
4.1 退出场景
| 场景 | 处理 |
|---|---|
| 主动退出(Waiting 期) | 直接移除,广播 |
| 主动退出(Playing 期) | 按规则:判负/托管/等待重连 |
| 掉线(短时间) | 宽限期保留,标记离线 |
| 掉线(超时) | 移除/判负 |
| 房主离开 | 转移房主/解散(按配置) |
Playing 期退出策略(按玩法):
棋牌:托管(AI 代打)或判负
桌游:等待重连(回合暂停/跳过)
轮流出牌类:超时跳过回合4.2 解散条件
解散时机:
所有成员离开
房主离开且无转移
对局结束全员离开
房间超时空置(无人)
解散动作:
状态 → 清理
通知所有成员"房间已解散"
释放房间对象(内存)与定时器五、房间属性配置
5.1 配置字段
| 配置 | 说明 | 示例 |
|---|---|---|
| mode | 玩法模式 | 斗地主/德州/桌游 |
| minPlayers / maxPlayers | 人数范围 | 2-10 |
| rounds | 局数 | 1/3/5 局 |
| isPrivate | 是否私人房 | 是/否 |
| inviteCode | 邀请码 | 6 位 |
| baseScore | 基础分/底注 | 10 |
| timeLimit | 回合时限 | 15s |
| allowSpectate | 是否允许观战 | 是/否 |
配置来源:
公共房:读取玩法配置表(策划配)
私人房:玩家自定义(受上限约束)
配置校验在建房时完成5.2 配置驱动设计
配置驱动的好处:
新玩法 = 新配置模板,不改代码
房间规则全部读配置
示例:timeLimit 决定回合倒计时
baseScore 决定结算基础分六、房间管理组件
6.1 RoomManager
java
public class RoomManager {
private final ConcurrentHashMap<Long, Room> rooms = new ConcurrentHashMap<>();
public Room get(long roomId) { return rooms.get(roomId); }
public void add(Room room) { rooms.put(room.getRoomId(), room); }
public void remove(long roomId) { rooms.remove(roomId); }
public int count() { return rooms.size(); }
}补充职责:
按玩家索引:playerId → roomId(判断"玩家在哪个房间")
定时清理:空房间、超时房间
统计:在线房间数、房间内人数(监控)6.2 房间线程模型
房间并发安全:
同房间操作串行(房间级锁 / 单线程)
跨房间天然并行
实现选择:
简单:synchronized(room)(房间粒度)
进阶:按 roomId 路由到固定线程(Actor 思想)
注意:
房间锁内不做慢操作(查库、RPC)
广播遍历成员时用快照,避免并发修改七、常见问题
| 问题 | 处理 |
|---|---|
| 房间状态乱流转 | 状态机校验前置状态 |
| 满员后还能进 | canJoin 校验 + 并发安全 |
| 掉线后房间卡住 | 宽限期 + 托管/跳过机制 |
| 广播风暴 | 成员快照 + 限制单房间人数 |
| 房间泄漏 | 定时清理空/超时房间 |
八、小结
房间系统是"对战的组织层":Room 对象聚合成员列表、状态机与配置,通过 RoomManager 统一管理;状态机用 Waiting/Ready/Playing/End 四态约束生命周期,每个状态只允许合法消息,状态切换校验前置状态并用房间级串行保证并发安全;创建/加入/退出/解散各有明确流程与异常分支,Playing 期的退出按玩法选择托管或判负。房间把"2-10 人的实时交互"封装成独立单元,为后续匹配、回合流转、消息同步与结算提供稳定的承载容器。