Skip to content

排位赛

适用于「路人单排 / 多排自动匹配」:玩家进 matchmaker_room 排队,服务端按 modeId、人数、分区 等成局,全员收到 match:foundjoinById 进入 game_room。与 小游戏邀请好友对战(Party 邀约) 的「建房拉人」互补:排位走 主动匹配 match:find,一般不走 party:create

场景目标

  • 用框架自带的 matchmaker_room + match:find / match:cancel 完成排队与成局,无需在客户端或服务端自写一套匹配循环
  • modeId(如 rankedranked_solo)区分段位赛、娱乐赛等;playersPerMatch 表示凑满几人成局,同时也是开局时创建的 game_room 内玩家规模(1v1 填 2,2v2 填 4,4v4 填 8,以此类推);与 region 组合即可多模式共存,且受服务端 2~100 的统一人数限制。
  • 段位、MMR、禁赛、赛季等 业务规则 放在 HTTP / 数据库或 继承 MatchmakerRoom 的服务端扩展里(例如入队前校验、匹配成功写战绩),框架侧只保证房间契约与 Redis 队列语义。

推荐消息流

  1. 客户端 joinOrCreate("matchmaker_room", { token }),收到 mm:ready 后再发业务消息(避免丢下行)。
  2. 发送 match:find,body 中带 modeIdplayersPerMatchregion 等与玩法一致的参数。
  3. 等待 match:queued / match:found;需要离开时发 match:cancel
  4. 收到 match:found 后使用返回的 roomIdjoinOptions,合并本端 tokenjoinById 进入 game_room

协议字段、Redis 与继承扩展见 匹配与开房。下面给出本场景专用的 三端客户端示例(与匹配文档基线一致,侧重 modeId + 可选 skill)。

客户端示例代码(match:find)

以下为 客户端 示例:连接默认 matchmaker_room先注册 match:found(或先 message_received 再于 mm:ready 后发排队),再发 match:findmodeId 用排位池标识(如 ranked / ranked_solo),可选 skill 传 MMR 或段位分(与 MatchFindRequest 一致);成局后 joinById。离开队列发 match:cancel,服务端回 match:cancelled。不在此实现匹配算法,排队与成局由框架 MatchmakerRoom 完成。

match:find 核心参数(与 匹配与开房「主动匹配」一致;regionmodeIdplayersPerMatchskilltags 如何共同划分队列队列维度与 region):

字段含义
modeId玩法 / 模式池标识;排位常用 rankedranked_solo 等,与 Redis 队列维度绑定,不同字符串互相独立排队。
playersPerMatch凑满几人成一局(整数 2~100,与 Party、match:find 共用同一套限制)。达到人数后服务端创建 game_room 并下发 match:found;该值即本局 game_room 的预期玩家总数。写法与对战规模对应:1v1 → 22v2 → 44v4 → 8,依此类推。
region分区字符串(常见实现里排队键第三段:queue:{modeId}:{playersPerMatch}:{region})。与 modeIdplayersPerMatch 绑定:三者完全一致才会进同一条路人队。"global" = 全服一条队;也可用 "cn"、行会 ID 等收窄池子。自动定位。详见 匹配与开房 · 队列维度与 region
skill(可选)匹配分 / 段位分,预留字段;具体是否参与撮合以服务端 MatchmakingService 为准,可配合继承扩展使用。
ts
import Colyseus from "colyseus.js";

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";

function onceMessage<T>(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} timeout`));
    }, ms);
    dispose = room.onMessage(type, (payload: T) => {
      clearTimeout(timer);
      dispose?.();
      resolve(payload);
    });
  });
}

/** 排位:排队 → match:found → 进 game_room */
export async function rankedQueueAndJoin(skill?: number) {
  const client = new Colyseus.Client(ENDPOINT);
  const mm = await client.joinOrCreate("matchmaker_room", { token: TOKEN });

  mm.onMessage("mm:ready", (p) => console.log("mm:ready", p));
  mm.onMessage("match:queued", (p) => console.log("match:queued", p));
  mm.onMessage("match:cancelled", (p) => console.log("match:cancelled", p));

  const foundP = onceMessage<MatchFoundPayload>(mm, "match:found");
  mm.send("match:find", {
    modeId: "ranked", // 排位池标识,与 Redis 队列维度一致;多模式用不同字符串
    playersPerMatch: 2, // 成局人数=本局 game_room 规模;1v1=2,2v2=4,4v4=8;服务端 2~100
    region: "global", // 队列分区第三段;与 Party、其他 match:find 参数共同决定排哪条队
    ...(skill != null ? { skill } : {}), // 可选 MMR/段位分,见上文表格
  });

  const found = await foundP;
  const game = await client.joinById(found.roomId, {
    ...found.joinOptions,
    token: TOKEN,
  });
  console.log("ranked in game_room", game.roomId, found.matchId, "seat", found.seatIndex);
  return { client, mm, game };
}

/** 取消当前排队(仍在 matchmaker_room 内时调用) */
export function rankedCancelQueue(mm: Colyseus.Room) {
  mm.send("match:cancel", {});
}
csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Colyseus;
using Colyseus.Schema;

/// <summary>排位:match:find → match:found → JoinById(game_room)。可选 skill 传 MMR。</summary>
public static class RankedClientExample
{
    public static async Task<(Room<DynamicSchema> mm, Room<DynamicSchema> game)> QueueAndJoin(
        Colyseus.Client client,
        string accessToken,
        int? skillMmR = null)
    {
        var mm = await client.JoinOrCreate<DynamicSchema>(
            "matchmaker_room",
            new Dictionary<string, object> { { "token", accessToken } });

        var foundTcs = new TaskCompletionSource<Dictionary<string, object>>();
        mm.OnMessage<Dictionary<string, object>>("match:found", p => foundTcs.TrySetResult(p));

        // match:find 核心字段见上文表格
        var findBody = new Dictionary<string, object> {
            { "modeId", "ranked" }, // 排位池
            { "playersPerMatch", 2 }, // 成局人数=game_room 规模;1v1=2,2v2=4,4v4=8;2~100
            { "region", "global" }, // 分区键;与 modeId、playersPerMatch 组成排队维度
        };
        if (skillMmR.HasValue)
            findBody["skill"] = skillMmR.Value; // 可选 MMR,见上文表格

        mm.Send("match:find", findBody);

        var found = await foundTcs.Task;
        var roomId = found["roomId"]?.ToString();
        if (string.IsNullOrEmpty(roomId))
            throw new InvalidOperationException("match:found 缺少 roomId");

        var joinOptions = found["joinOptions"] as Dictionary<string, object> ?? new Dictionary<string, object>();
        var gameOptions = new Dictionary<string, object>(joinOptions) { { "token", accessToken } };
        var game = await client.JoinById<DynamicSchema>(roomId, gameOptions);
        return (mm, game);
    }

    public static void CancelQueue(Room<DynamicSchema> mm) =>
        mm.Send("match:cancel", new Dictionary<string, object>());
}
gdscript
# 排位排队:mm:ready 后发 match:find;收到 match:found 后 join_by_id 进 game_room
var _client: Colyseus.Client
var _mm: Colyseus.Room
var _token := "YOUR_JWT_ACCESS_TOKEN"

func start_ranked_queue() -> void:
	_client = Colyseus.Client.new("ws://127.0.0.1:2567")
	_mm = _client.join_or_create("matchmaker_room", {"token": _token})
	if _mm:
		_mm.message_received.connect(_on_ranked_mm_message)

func _on_ranked_mm_message(type: Variant, data: Variant) -> void:
	var t := str(type)
	if t == "mm:ready":
		_mm.send_message("match:find", {
			"modeId": "ranked", # 排位池标识,见上文表格
			"playersPerMatch": 2, # 成局人数=game_room 规模;1v1=2,2v2=4,4v4=8;2~100
			"region": "global", # 分区键第三段;全服常用 global
			"skill": 1500, # 可选 MMR;不需要可整键删除
		})
	elif t == "match:queued":
		print("match:queued ", data)
	elif t == "match:cancelled":
		print("match:cancelled ", data)
	elif t == "match:found" and typeof(data) == TYPE_DICTIONARY:
		var d: Dictionary = data
		var opts := {"token": _token}
		var join_options: Variant = d.get("joinOptions", {})
		if typeof(join_options) == TYPE_DICTIONARY:
			for k in (join_options as Dictionary):
				opts[k] = (join_options as Dictionary)[k]
		_client.join_by_id(str(d.get("roomId", "")), opts)
	elif t == "party:error":
		push_warning("party:error " + str(data))

func cancel_ranked_queue() -> void:
	if _mm:
		_mm.send_message("match:cancel", {})

与 Party、多模式文档的关系

相关文档