Skip to content

多 V 多自动匹配:多模式 × 动态人数

同一套 matchmaker_room:换玩法时改 modeId + playersPerMatch(可选 region)即可,不必为 2 人 / 4 人 / 10 人各写一套房间类型。

先用浏览器跑通

服务端仓库静态页 MatchmakingDemo.html(标题「匹配赛演示 - 小游码匠」):

MatchmakingDemo:连接与参数、主动匹配、被动开房、对局与重连、日志

说明
路径源码 src/public/MatchmakingDemo.html,构建后在 dist/public/
打开http://localhost:<PORT>/MatchmakingDemo.html,默认端口 2567PORT 环境变量)
入口根页 http://localhost:<PORT>/ 内也有链到该演示
子页组局后可进 MatchmakingGame.html(需从演示页带上的 sessionStorage,勿单独冷开)

多开标签、不同 JWT / demo 用户,在页里改 modeId / playersPerMatch / region,即可试不同人数与队列。

三个参数(与排队键一致)

服务端队列键形如 queue:{modeId}:{playersPerMatch}:{region}(实现见 匹配与开房)。

字段作用备注
modeId玩法 / 模式字符串例如 duelbrawl_10 要分开,避免混排
playersPerMatch一局凑几人服务端限制 2~100,且写入键;同 modeId4 人与 10 人不会同一队列
region分区字符串(排队键第三段)非地理 API;不传多为 global。行会/赛事可用行会 ID 收窄池子。与 modeIdplayersPerMatch 关系见 匹配与开房 · 队列维度与 region

match:findparty:create 都要带 modeId + playersPerMatch;人满后进 game_roommaxClients 与人数一致。

常见配法速查

场景modeId 示例playersPerMatch
1v1 / 切磋duel / rank_1v12
4 人小队dungeon_squad / crafter_44
10 人场brawl_10 / battlefield10
行会内战guild_scrim 等,region 用行会或赛区 ID按赛制 5 / 10 / …

行会「仅本会可进」还要在业务里校验 guildId 等,见 消息协议

主动匹配 vs 房间码(Party)

做法客户端要点
路人排队match:find → 等 match:foundjoinById
好友房party:create / party:join → 人满后 party:start,同样会 match:found 再进对局

示例代码(TypeScript / colyseus.js

依赖 colyseus.js(与仓库 MatchmakingDemo.html 一致)。请先通过 POST /api/auth/login 等拿到 token(见 API 概览JWT 与房间鉴权)。Room.onMessage 的回调形态以当前 SDK 版本为准。

下面假设 每个排队客户端各执行一份脚本(例如本地开 多个浏览器标签,每标签不同 TOKEN);playersPerMatch2 时开两个标签即可组局。把 ENDPOINTTOKENMODE_IDPLAYERS 换成你的环境。

主动匹配:match:findmatch:foundjoinById

(客户端代码)

ts
import Colyseus from "colyseus.js";

/** 与 `src/services/matchmaking/types.ts` 中 `MatchFoundPayload` 对齐 */
interface MatchFoundPayload {
  roomName: string;
  roomId: string;
  matchId: string;
  seatIndex: number;
  reconnectKey: string;
  joinOptions: Record<string, unknown>;
}

const ENDPOINT = "ws://127.0.0.1:2567";
const TOKEN = "YOUR_JWT_ACCESS_TOKEN";
const MODE_ID = "duel";
const PLAYERS = 2;
const REGION = "global"; // 可选:不需要分区则省略下行里的 region

function onceMessage<T = unknown>(room: Colyseus.Room, type: string, ms = 120000): 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 activeMatchMultiV() {
  const client = new Colyseus.Client(ENDPOINT);
  const mm = await client.joinOrCreate("matchmaker_room", { token: TOKEN });

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

  // 必须先监听再发送,避免极端情况下抢不到 match:found
  const foundPromise = onceMessage<MatchFoundPayload>(mm, "match:found", 120000);

  mm.send("match:find", {
    modeId: MODE_ID,
    playersPerMatch: PLAYERS,
    region: REGION,
    // skill: 1000,
    // tags: ["guild:42"],
  });

  const found = await foundPromise;

  const game = await client.joinById(found.roomId, {
    ...found.joinOptions,
    token: TOKEN,
  });

  // game 即 game_room;若本地演示页使用 demo 用户,可额外传 demoUserId(与 MatchmakingDemo 一致)
  // await client.joinById(found.roomId, { ...found.joinOptions, token: TOKEN, demoUserId: "u1" });

  return { mm, game, found };
}

activeMatchMultiV().catch(console.error);

取消排队:mm.send("match:cancel", {}),下行 match:cancelledok 表示是否从队列移除)。

Party(房间码)最小发送序列

房主与其它客户端仍留在 matchmaker_room 内发消息即可(完整 async 示例见 组队下副本):

(客户端代码)

ts
mm.send("party:create", { modeId: "crafter_4", playersPerMatch: 4, region: "global" });
// 收到 party:created → 把 partyCode 发给队友
mm.send("party:join", { partyCode: "ABC123" });
// 人满且你是房主:
mm.send("party:start", {});
// 全员同样会收到 match:found,再 joinById 进 game_room(与上一段相同)

对局与重连

game_room 后的座位、断线、reconnectKey 等见 帧同步与游戏房

压测与多实例

进阶(算法与票据)

match:find 票据里的 skill / tags、段位排序、多实例 Pub/Sub 等以匹配服务代码与 匹配与开房 为准。

延伸阅读