Skip to content

组队下副本:被动开房(Party)+ 队伍聊天

常见落地组合:matchmaker_room 的 Party(房间码)组固定队伍,等人或加载阶段用 chat_team_room 做队内沟通,人满后由房主 party:start 进入 game_room(副本 / 对局)。约定与 匹配与开房消息协议JWT 与房间鉴权 对齐。

能力对照

能力房间 / 入口说明
被动开房、房间码、人满开局matchmaker_roomparty:createparty:joinparty:start,见下文消息表
队内文字聊天chat_team_room进房 options 带 teamId,仅同 teamId 玩家互见(TeamChatRoom
副本战斗 / 帧同步game_roomparty:start 或匹配成功后由 match:found 引导 joinById

teamId 与 Party 的衔接:服务端未把 partyId 自动写入聊天;推荐客户端在拿到 partyId 后,全队统一使用 同一字符串 作为 teamId(例如直接使用 partyId),再进入 chat_team_room。这样无需额外「建队」接口即可对齐队伍聊天与 Party 成员。

整体流程(推荐)

mermaid
sequenceDiagram
  participant A as 房主客户端
  participant M as matchmaker_room
  participant T as chat_team_room
  participant G as game_room

  A->>M: join + party:create
  M-->>A: party:created (partyId, partyCode)
  Note over A,T: 全队用 partyId 作 teamId 进队伍房
  A->>T: joinOrCreate(chat_team_room, { token, teamId: partyId })
  A->>M: party:start (满员后)
  M-->>A: match:found
  A->>G: joinById + joinOptions + token
  1. 登录:HTTP 或小游戏登录拿到 JWT(API 概览、小游戏教程)。
  2. 匹配房joinOrCreate("matchmaker_room", { token }),等待 mm:ready
  3. 房主send("party:create", { modeId, playersPerMatch, region? }),收到 party:created 后把 partyCode 发给好友。
  4. 队员send("party:join", { partyCode }),收到 party:joined;全员监听 party:update 看人数。
  5. 队伍聊天(可选但推荐):全员 joinOrCreate("chat_team_room", { token, teamId: partyId })partyId 来自 party:created / party:joined 的 payload)。发 chat、收下行 chat消息协议)。
  6. 开局:房主在人数 ≥ playersPerMatchsend("party:start", {});全员收到 match:found,再 joinById(roomId, { ...joinOptions, token }) 进入 game_room
  7. 离开队伍:未开局前可 party:leave;进副本后 Party 已关闭,聊天房可 leave 或保留至副本结束由客户端策略决定。

Party 消息速查(matchmaker_room

详见 匹配与开房,此处摘录与「组队下副本」强相关部分。

消息方向要点
party:createC→S{ modeId, playersPerMatch, region? }
party:createdS→CpartyIdpartyCodeplayersPerMatchleaderUserId
party:joinC→C{ partyCode }
party:joined / party:updateS→C人数、是否队长
party:startC→S仅房主;满员后创建 game_room 并广播 match:found
party:leave / party:left双向退队
party:errorS→C错误文案
match:foundS→Cgame_roomroomIdjoinOptionsreconnectKey

队伍聊天(chat_team_room

  • 房间名:chat_team_room(见 消息协议)。
  • 进房 必须 带 JWT,且 options 中 teamId 与队友一致(建议等于 partyId)。
  • 上行:room.send("chat", "文本")room.send("chat", { message: "文本" })(与 BaseChatRoom 解析一致)。
  • 下行:channelteam,payload 含 sendermessagetimestamp 等。

示例代码(TypeScript / colyseus.js

以下用 async/await 串起 Party + 队伍房 + 进副本;可直接迁移到 Cocos(db://colyseus-sdk/colyseus.js)或浏览器调试。请将 ENDPOINTTOKENMODEPLAYERS 换成真实值。

客户端 Room.onMessage 一般为 (type, (payload) => void)(具体以当前 colyseus.js 版本为准)。

(客户端代码)

ts
import Colyseus from "colyseus.js";

const ENDPOINT = "ws://127.0.0.1:2567";
const TOKEN = "YOUR_JWT_ACCESS_TOKEN";
const MODE = "dungeon_normal";
const PLAYERS = 4;

function onceMessage<T = unknown>(room: Colyseus.Room, type: string, ms = 30000): Promise<T> {
  return new Promise((resolve, reject) => {
    let dispose: (() => void) | undefined;
    const timer = setTimeout(() => {
      dispose?.();
      reject(new Error(`${type} 超时 ${ms}ms`));
    }, ms);
    dispose = room.onMessage(type, (payload: T) => {
      clearTimeout(timer);
      dispose?.();
      resolve(payload);
    }) as (() => void) | undefined;
  });
}

async function partyDungeonWithTeamChat(isLeader: boolean, partyCodeFromFriend?: string) {
  const client = new Colyseus.Client(ENDPOINT);
  const mm = await client.joinOrCreate("matchmaker_room", { token: TOKEN });

  await onceMessage(mm, "mm:ready", 15000);

  let partyId = "";

  if (isLeader) {
    mm.send("party:create", { modeId: MODE, playersPerMatch: PLAYERS });
    const created = await onceMessage<Record<string, unknown>>(mm, "party:created");
    partyId = String(created.partyId ?? "");
    console.log("房间码:", created.partyCode, "partyId:", partyId);
  } else {
    if (!partyCodeFromFriend) throw new Error("队员需要 partyCode");
    mm.send("party:join", { partyCode: partyCodeFromFriend });
    const joined = await onceMessage<Record<string, unknown>>(mm, "party:joined");
    partyId = String(joined.partyId ?? "");
  }

  const teamChat = await client.joinOrCreate("chat_team_room", {
    token: TOKEN,
    teamId: partyId,
  });

  teamChat.onMessage("chat", (payload: Record<string, unknown>) => {
    console.log("[队伍]", payload?.message, (payload?.sender as Record<string, string>)?.username);
  });

  teamChat.send("chat", { message: "人已齐,准备进本" });

  mm.onMessage("party:update", (p: Record<string, unknown>) => {
    console.log("队伍人数:", p?.count, "/", p?.playersPerMatch);
  });

  mm.onMessage("match:found", async (payload: Record<string, unknown>) => {
    const roomId = String(payload.roomId ?? "");
    const joinOptions = (payload.joinOptions as Record<string, unknown>) ?? {};
    console.log("进副本房:", roomId);
    const game = await client.joinById(roomId, { ...joinOptions, token: TOKEN });
    game.onMessage("frameSync", (p: unknown) => console.log("帧同步", p));
    game.onMessage("playerJoined", (p: unknown) => console.log("玩家进房", p));
  });

  if (isLeader) {
    /* 演示:2 秒后强制开局。正式项目请在 UI 上确认 count >= playersPerMatch 再 send */
    await new Promise((r) => setTimeout(r, 2000));
    mm.send("party:start", {});
  }
}

// 房主:void partyDungeonWithTeamChat(true);
// 队员:void partyDungeonWithTeamChat(false, "AB12CD");

说明:

  • 上面房主 setTimeout 仅作演示,正式项目应在收到 party:updatecount >= playersPerMatch 后再由玩家点击「开始」调用 party:start
  • match:found 后务必 合并 tokenjoinById 的 options(与 鉴权与权限 一致)。

其它引擎要点(无完整工程粘贴时)

  • Cocos:与 Cocos 接入示例 相同,分别 joinOrCreate 两个房间;teamIdtoken 放在第二个参数对象中。
  • UnityJoinOrCreate / JoinByIdDictionary<string, object> 中设置 tokenteamId;监听 OnMessage 解析 JSON(或使用消息代码生成)。
  • Godotjoin_or_create 的 options 字典含 tokenteamId;信号 message_received 处理 chatmatch:found(与插件 API 一致即可)。

风险与限制

  • Party 与聊天房 相互独立:若队员未进 chat_team_roomteamId 不一致,将收不到队内消息。
  • Party 数据有 Redis TTL(约 30 分钟),超时需重新建房。
  • 生产环境务必关闭或隔离 loadtest_room 等无鉴权房间(人数压力测)。

延伸阅读