Skip to content

小游戏邀请好友对战(Party 邀约)

适用于微信/抖音等小游戏:玩家通过分享卡片或邀请链接拉好友进房,再由房主开局进入对战房。

场景目标

  • 让“好友邀请”与 matchmaker_room 的 Party 流程对齐,避免自建第二套开房协议。
  • 控制加入权限(例如 inviteToken、有效期、来源校验),防止房间码被外部滥用。
  • 全员最终仍走统一链路:match:found -> joinById(game_room)

推荐消息流

  1. 房主与队员都先 joinOrCreate("matchmaker_room", { token })
  2. 房主 party:create,拿到 partyCode(可写入分享参数)。
  3. 受邀玩家用 invite:join(自定义入口)或直接 party:join
  4. 服务端校验邀请参数后转发到 Party 标准流程。
  5. 人满后房主 party:start,全员收到 match:foundjoinByIdgame_room

服务端扩展点(继承 MatchmakerRoom

  • 在子类 onCreate 里先 super.onCreate(options)
  • 新增 invite:join 消息,校验 inviteToken / 时间戳 / 签名。
  • 校验通过后,按标准流程转发 party:join,减少重复逻辑。

invite:join 等自定义入口的服务端转发与 mm:hintparty:join 的写法,可参考:匹配与开房 中「示例 B:被动开房邀约匹配(party:join)」。小游戏侧房主建 Party、好友凭房间码加入、开局进 game_room客户端骨架见本文 客户端示例代码(Party)

客户端要点

  • 邀请链接建议包含 partyCode 与一次性 inviteToken
  • 先注册 match:found,再发送邀请入口消息,避免竞态丢消息。
  • 收到 mm:hint 时按服务端提示转发(如 party:join),最终统一 joinById(roomId, { ...joinOptions, token })

常见坑

  • 只校验房间码,不校验邀请参数来源:容易被非邀请用户加入。
  • 房主提前 party:start:人数不足会触发 party:error
  • 忘记把 token 合并到 joinById options:可能在 game_room 鉴权失败。

客户端示例代码(Party)

以下均为 客户端 示例:连接框架提供的 matchmaker_room,按 Party 协议发 party:create / party:join / party:start,再按 match:found 携带的 roomIdjoinOptions 调用 joinById 进入 game_room。不涉及在客户端实现匹配队列或成局算法。

匹配与 Party 成局 由框架内置的 MatchmakerRoommatchmaker_room)处理:固定人数 Party、自动匹配等逻辑已带好,业务侧通常 不必再写一套匹配/组局服务。若要做邀请签名校验、自定义入口消息、与业务后台联动等,再在服务端 继承 MatchmakerRoom 覆写或增消息即可(见上文「服务端扩展点」与 匹配与开房)。

目标:房主创建 Party、好友加入、房主开局,最终统一 match:found -> joinById

游戏场景:微信/抖音小游戏“分享拉人开黑”,常见 1v1 / 2v2 固定人数对战。

party:create 核心参数(与 匹配与开房「被动开房」一致,三端示例里同名同义):

字段含义
modeId玩法 / 模式池标识,字符串由业务约定;决定 Party 与后续 game_room 落在哪条模式线上,并与路人 match:find 的队列维度语义一致。示例 mini_duel 表示「小游戏单挑池」,可改成你方枚举。
playersPerMatch成局所需人数(整数 2~100)。Party 内人数达到该值后,房主才能 party:start;同时也是开局时创建的 game_room 规模。1v1 填 2,2v2 填 4
region分区字符串(排队键的第三段),与 modeIdplayersPerMatch 一起决定落在哪条逻辑队列;不是自动地理选服,含义由业务约定。未传时服务端多按 global。与 match:findregion 必须同字符串才会进同一路人池;Party 好友间也应约定同一值。完整说明见 匹配与开房 · 队列维度与 region
ts
import Colyseus from "colyseus.js";

const ENDPOINT = "ws://127.0.0.1:2567";
const HOST_TOKEN = "HOST_JWT_TOKEN";
const FRIEND_TOKEN = "FRIEND_JWT_TOKEN";

async function hostCreateAndStart() {
  const hostClient = new Colyseus.Client(ENDPOINT);
  const mm = await hostClient.joinOrCreate("matchmaker_room", { token: HOST_TOKEN });

  // 1)先监听关键消息
  const foundP = new Promise<any>((resolve) => mm.onMessage("match:found", resolve));
  const createdP = new Promise<any>((resolve) => mm.onMessage("party:created", resolve));

  // 2)房主创建 Party 并拿到房间码(参数含义见上文表格)
  mm.send("party:create", {
    modeId: "mini_duel", // 玩法池标识,与 game_room 模式对齐;可自定义
    playersPerMatch: 2, // 满几人后房主可 party:start;1v1=2,2v2=4;服务端限制 2~100
    region: "global", // 与 modeId、playersPerMatch 组成 queue:…:{region};全服常用 global
  });
  const created = await createdP;
  console.log("party code:", created.partyCode);

  // 3)等待好友加入后,房主开局(示例用 setTimeout)
  setTimeout(() => mm.send("party:start", {}), 2000);

  // 4)收到 match:found 后进入 game_room
  const found = await foundP;
  const game = await hostClient.joinById(found.roomId, { ...found.joinOptions, token: HOST_TOKEN });
  console.log("host joined game:", game.roomId);
}

async function friendJoinByCode(partyCode: string) {
  const friendClient = new Colyseus.Client(ENDPOINT);
  const mm = await friendClient.joinOrCreate("matchmaker_room", { token: FRIEND_TOKEN });
  const foundP = new Promise<any>((resolve) => mm.onMessage("match:found", resolve));

  mm.send("party:join", { partyCode });
  const found = await foundP;
  const game = await friendClient.joinById(found.roomId, { ...found.joinOptions, token: FRIEND_TOKEN });
  console.log("friend joined game:", game.roomId);
}
csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Colyseus;
using Colyseus.Schema;

public static class MiniInviteBattleExample
{
    public static async Task HostRun(Colyseus.Client hostClient, string hostToken)
    {
        var mm = await hostClient.JoinOrCreate<DynamicSchema>(
            "matchmaker_room",
            new Dictionary<string, object> { { "token", hostToken } });

        // 1) 先挂关键下行
        var createdTcs = new TaskCompletionSource<Dictionary<string, object>>();
        var foundTcs = new TaskCompletionSource<Dictionary<string, object>>();
        mm.OnMessage<Dictionary<string, object>>("party:created", p => createdTcs.TrySetResult(p));
        mm.OnMessage<Dictionary<string, object>>("match:found", p => foundTcs.TrySetResult(p));

        // 2) 房主建房(modeId / playersPerMatch / region 含义见上文表格)
        mm.Send("party:create", new Dictionary<string, object> {
            { "modeId", "mini_duel" }, // 玩法池标识
            { "playersPerMatch", 2 }, // 满员人数,1v1=2,2v2=4;范围 2~100
            { "region", "global" }, // 分区键第三段,与 match:find 同义
        });

        var created = await createdTcs.Task;
        Console.WriteLine($"party code: {created["partyCode"]}");

        // 3) 示例里直接开局;实际应在人数满足后再 start
        mm.Send("party:start", new Dictionary<string, object>());

        // 4) 统一收口:match:found -> JoinById
        var found = await foundTcs.Task;
        var roomId = found["roomId"]?.ToString();
        var joinOptions = found["joinOptions"] as Dictionary<string, object> ?? new Dictionary<string, object>();
        var gameOptions = new Dictionary<string, object>(joinOptions) { { "token", hostToken } };
        await hostClient.JoinById<DynamicSchema>(roomId, gameOptions);
    }
}
gdscript
var _host_client: Colyseus.Client
var _host_mm: Colyseus.Room
var _host_token := "HOST_JWT_TOKEN"

func host_run() -> void:
	_host_client = Colyseus.Client.new("ws://127.0.0.1:2567")
	_host_mm = _host_client.join_or_create("matchmaker_room", {"token": _host_token})
	if _host_mm:
		_host_mm.message_received.connect(_on_host_mm_message)
		# 1)房主创建 Party(字段含义见上文表格)
		_host_mm.send_message("party:create", {
			"modeId": "mini_duel", # 玩法池标识,与 game_room 模式对齐
			"playersPerMatch": 2, # 满员人数;1v1=2,2v2=4;服务端 2~100
			"region": "global", # 分区键;与 modeId、playersPerMatch 共同决定排队池
		})

func _on_host_mm_message(type: Variant, data: Variant) -> void:
	var t := str(type)
	if t == "party:created" and typeof(data) == TYPE_DICTIONARY:
		var d: Dictionary = data
		print("party code: ", d.get("partyCode", ""))
		# 2)示例里直接开局;实际应在人数满足后再 start
		_host_mm.send_message("party:start", {})
	elif t == "match:found" and typeof(data) == TYPE_DICTIONARY:
		# 3)统一收口:match:found -> join_by_id
		var d2: Dictionary = data
		var opts := {"token": _host_token}
		var join_options: Variant = d2.get("joinOptions", {})
		if typeof(join_options) == TYPE_DICTIONARY:
			for k in (join_options as Dictionary):
				opts[k] = (join_options as Dictionary)[k]
		_host_client.join_by_id(str(d2.get("roomId", "")), opts)

相关文档