抖音开放平台Logo
开发者文档
控制台

匹配同玩

收藏
我的收藏
本文对云同步能力,如何接入和使用”匹配同玩“相关接口功能,进行说明。

总览

总流程图:

    1.用户A 和 B 分别将玩法云启动
    2.都进入单播状态
    3.A 和 B 调用 匹配
    a.匹配成功,返回匹配信息会决定其中谁做房主、谁做加入的玩家。
    4.A与B进入同玩状态
    a.其中 A 做房主。
    b.其中 B 作为玩家,系统会将其做切流处理,加入到 A 的云游戏实例。
    c.进行同玩游戏
    5.游戏结束后,调用 结束同玩
    a.B 结束同玩,从A的实例断开退出。回流,返回到自己实例单播状态。
    b.A 结束了同玩状态,回到单播状态。
    6.回到单播状态

功能接口:

匹配同玩接口:
// 访问接口入口: ICloudMatchManager MatchManager = ICloudSync.Instance.MatchManager; // 请求匹配同玩: MatchManager.RequestMatch(...) // 结束同玩: MatchManager.EndMatchGame(...)
  
Event 事件:
// 监听事件:玩家加入/离开 ICloudSync.Instance.OnSeatPlayerJoined ICloudSync.Instance.OnSeatPlayerLeaving // 监听事件:匹配到了其他用户 MatchManager.OnMatchUsers // 监听事件:结束同玩了(返回单播) MatchManager.OnEndMatchEvent

请求匹配同玩

流程图:

单播状态 → 匹配同玩 流程图:
    1.前置:云同步初始化
    2.添加事件监听
    3.调用请求匹配接口:RequestMatch
    a.触发事件:OnMatchUsers 匹配到了用户
    i.用户A 收到匹配信息:自己做房主,匹配到了对方玩家B
    ii.用户B 收到匹配信息:匹配到了对方玩家A 做房主,自己做玩家。
    b.[可选]:取消匹配
    c.如果匹配超时、匹配失败,返回匹配结果:IMatchResult
    4.切流,进入同玩(系统自动完成)
    a.其中,房主A BeginHost 开始房主状态
    b.其中,其他玩家B SwitchTo 切流到房主A的实例
    5.RequestMatch返回匹配结果IMatchResult
    6.触发事件:OnSeatPlayerJoined 玩家加入了(玩家B)
    7.A与B进入同玩状态,进行同玩游戏

1.前置初始化

前置需要完成↗云同步初始化
【示例代码】:云同步初始化:
private async Task<bool> InitCloudGame(string appId) { // 云同步初始化 Init var initResult = await ICloudSync.Instance.Init(appId, new UDeviceFactory(viewProvider)); if (!initResult.IsSuccess) { Debug.LogError($"Init 初始化失败,无法启动云游戏 {initResult.Message}"); return false; } return true; }

2.添加事件监听

匹配同玩相关事件,有以下几个:
    1.必要:玩家加入、离开
    2.可选:匹配到了用户
    3.必要:结束同玩了
【示例代码】
// 添加事件监听 private void InitEvents() { // 监听事件:玩家加入/离开 ICloudSync.Instance.OnSeatPlayerJoined += OnPlayerJoined; ICloudSync.Instance.OnSeatPlayerLeaving += OnPlayerLeaving; MatchManager = ICloudSync.Instance.MatchManager; // 可选:监听事件:匹配到了其他用户 MatchManager.OnMatchUsers += OnMatchUsers; // 监听事件:结束同玩了(返回单播) MatchManager.OnEndMatchEvent += OnEndMatchEvent; }

3.请求匹配 RequestMatch

接口:IMatchManager.RequestMatch :
请求匹配,使用系统自带提供的匹配池。 成功后进入多人同玩状态。
【示例代码】:1. 简单版匹配: (可对照参考 Demo 工程代码的 Scripts/PK/Main.cs ):
/// 开始匹配 private async void StartMatch() { MatchManager = ICloudSync.Instance.MatchManager; // 匹配配置,选择一个系统自带提供的匹配池 var matchConfig = new SimpleMatchConfig() { PoolType = SimpleMatchPoolType.P1v1, // 2人 1v1 MatchTag = "Demo-test-1.0" // 匹配标签,区分开用户 }; // 开始匹配,传入匹配配置。 成功后进入系统自动进入多人同玩状态。 var task = MatchManager.RequestMatch(matchConfig); var result = await task; // 等待结果 // todo: 处理结果:OnMatchResult(result); }
    其中,匹配配置 SimpleMatchConfig 的字段含义说明:
public class SimpleMatchConfig { /// 选择匹配池,对应一套匹配规则 public SimpleMatchPoolType PoolType; /// 匹配标签,区分开用户。 建议用不同值区分开测试环境"test"、线上环境"online",也可以用于分开不同版本"1.0","2.0"。 Example: 例如:"test-1.0", "online-2.0" public string MatchTag; }
    其中,PoolType匹配池,提供以下几种:
public enum SimpleMatchPoolType { /// 2人 1v1 P1v1, /// 4人 2v2 P2v2, /// 4人各自1队 P1x4, /// 3人各自1队 P1x3, }
    其中,MatchTag匹配标签:
    开发者可自定义。不同标签的用户会被区分开,不会匹配到一起。
    建议:用不同值区分开测试环境"test"、线上环境"online",也可以用于分开不同版本"1.0","2.0"。
    Example: MatchTag例如:"test-1.0", "online-2.0"
[可选]【示例代码】:2. 匹配、支持中途取消: 
private CancellationTokenSource _matchCancel; /// 开始匹配 private async void StartMatch() { // 设置本地游戏状态,可自定义表现,例如:显示“匹配中”文字、Loading转圈、按钮置灰等等... SetState(State.Matching); // 创建取消用的 CancellationTokenSource,using使其自动Dispose using CancellationTokenSource cancel = _matchCancel = new CancellationTokenSource(); // 匹配配置,选择一个系统自带提供的匹配池 var matchConfig = ...; // 开始匹配,传入匹配配置,传入可取消用的cancel.Token。 成功后进入系统自动进入多人同玩状态。 var task = MatchManager.RequestMatch(matchConfig, cancel.Token); var result = await task; // 等待结果 _matchCancel = null; // 已出结果,不再需要取消,应把 TokenSource 引用置null // 处理匹配结果: OnMatchResult(result); } /// 主动取消匹配 private void CancelMatch() { if (_matchCancel != null) { // 主动取消。 若成功取消,`RequestMatch`返回结果的`IMatchResult.Code`会是`Cancelled` _matchCancel.Cancel(); // 取消后,应把 TokenSource 引用置null _matchCancel = null; } }

4.事件:匹配到了用户

收到了匹配到的用户是谁的信息。
可以做相关展示、信息提示。相应要做什么表现,这部分开发者可以自定义实现。
【示例代码】
// 监听事件 MatchManager.OnMatchUsers += OnMatchUsers; private void OnMatchUsers(IMatchResult matchResult) { var isHost = matchResult.IsHost; var myIndex = matchResult.MyIndex; var users = matchResult.AllUsers; var opponents = users.Where(s => s.RoomIndex != myIndex).ToList(); var oppoNames = string.Join(", ", opponents.Select(s => s.Nickname).ToList()); ShowHostNote($"匹配到了用户: {oppoNames}"); }

5.处理匹配结果IMatchResult

请求匹配的接口 MatchManager.RequestMatch 返回类型为 Task<IMatchResult>
result = await task等待到结果后,应进行处理。
无论成功、失败、取消,都会返回结果。
    1.若失败!result.IsSuccess ,那么包含多个情况,应根据返回码:result.Code 枚举判断,枚举MatchResultCode类型定义如下:
    a.成功:Success
    b.取消了:Cancelled
    c.超时:Timeout (即:超过了一定时间,没有匹配到)
    d.错误:Error
    2.若成功result.IsSuccess true ,应当按result.IsHost是否为true,区分我方是否房主:
    a.IsHost true 我方是房主,应负责加载启动本局游戏逻辑。
            例如:应加载对局的游戏场景、界面UI等。
    b.IsHost false 我方是要加入同玩的玩家,系统会切流加入到房主A的实例去同玩。 本地无需特殊处理。
            [可选]:可以自定义做展示,例如:显示“与...同玩中”、显示Loading。
    c.result中,可以获取到匹配结果信息内容、对局的各个用户信息。
            例如:标识一场匹配的id`MatchId`,我的座位号`MyIndex`,房主用户`HostUser`;各个玩家用户的列表`AllUsers`,Team列表`Teams`
【示例代码】:处理匹配结果: (可对照参考 Demo 工程代码的 Scripts/PK/Main.cs ):
/// 处理匹配结果 private void OnMatchResult(IMatchResult result) { _matchResult = result; // 处理失败情况 if (!result.IsSuccess) { // 5.a 处理匹配失败 OnMatchFail(result); return; } var isHost = result.IsHost; Debug.Log($"StartMatch 匹配成功 isHost: {isHost}"); // 5.b 处理匹配成功 OnMatchSuccess(isHost); }
【示例代码】:处理匹配失败:
/// 5.a 处理匹配失败 private void OnMatchFail(IMatchResult result) { var debugMsg = $"code: {result.Code} ({(int)result.Code}), msg: {result.Message}"; // 根据返回码:`result.Code`枚举判断 switch (result.Code) { case MatchResultCode.Success: break; case MatchResultCode.Cancelled: Debug.Log($"StartMatch 取消 {debugMsg}"); ShowHostNote("匹配已取消"); break; case MatchResultCode.Timeout: Debug.LogWarning($"StartMatch 匹配超时 {debugMsg}"); ShowHostNote("匹配超时,没有匹配到对手"); break; case MatchResultCode.Error: default: Debug.LogError($"StartMatch 匹配失败 {debugMsg}"); ShowHostNote($"匹配失败! {debugMsg}"); break; } // 重置本地游戏状态、界面,并返回,允许后续操作。 SetState(State.Ready); }
【示例代码】:处理匹配成功:
/// 5.b 处理匹配成功 private void OnMatchSuccess(bool isHost) { var result = _matchResult; if (isHost) { // 匹配成功后,若我方是房主,应负责加载启动本局游戏(包括例如加载游戏资源、切换场景、显示界面UI等)。 // 房主A SetState(State.InGameHost); // 从result中,可以获取到匹配结果信息内容,例如:标识一场匹配的id`MatchId`,我的座位号`MyIndex`,房主用户`HostUser`;各个玩家用户的列表`AllUsers`,Team列表`Teams` var playersCount = result.AllUsers.Count; game.LoadGame(new GameInfo { isPk = true, playersCount = playersCount, }); } else { // 若我方是要加入同玩的玩家,系统会切流加入到房主A的实例去同玩。 本地无需特殊处理,可以自定义显示,例如:显示“与...同玩中”、显示Loading。 // 玩家B SetState(State.InConnectOther); } }

6.事件:玩家加入了

注意,房主需要特别关注其他玩家加入、离开的事件。
    原因:在”房主A“这一侧,匹配成功后,而对方”玩家B“可能由于网络延迟、操作延迟等,时序上并不是完全相同时刻就加入进来,通常会稍晚一点。
    注意避免:房主自己收到匹配成功的结果后,还没有等待其他玩家的加入,自己就开始了游戏内容、走后续玩法逻辑了。
    💡建议通常做法:
匹配成功后,若我方是房主:
    1.先加载游戏资源、切换场景、显示界面UI等。
    2.应当等待和检查后,才开局。 具体包括:
    a.等待和检查玩家B的加入事件 OnSeatPlayerJoined
    b.检查玩家加入到齐、确认OK后,开始正式一局游戏玩法
    c.[可选]:若玩家B还未加入,可以显示例如”等待其他玩家...“
    d.[可选]:若玩家一直不加入、或加入后又退出,可以做自定义的处理,例如:可进行弹框提示,可提供给用户界面按钮,也可以提前结束本局同玩。
【示例代码】:房主加载游戏,并等待玩家加入:(可对照参考 Demo 工程代码的 Scripts/PK/Game.cs ):
// 监听事件:玩家加入/离开 ICloudSync.Instance.OnSeatPlayerJoined += OnPlayerJoined; ICloudSync.Instance.OnSeatPlayerLeaving += OnPlayerLeaving; // 匹配成功 private void OnMatchSuccess(bool isHost) { if (isHost) { LoadGame(...); } } // 记录要参与本局的玩家座位列表 private List<ICloudSeat> _seats; // 2.加载游戏 public void LoadGame(...) { State = GameState.Loaded; // 记录要参与本局的玩家座位列表 _seats = GetSeatsForGame(_gameInfo); // todo: 自定义实现:加载游戏资源、切换场景、显示界面UI等... // 检查确认 CheckStartGame(); } // 示例:按来自gameInfo的玩家数量,记录对应玩家座位列表 private List<ICloudSeat> GetSeatsForGame(GameInfo gameInfo) { var userIndexes = new List<int> { 0 }; if (gameInfo.isPk) userIndexes = Enumerable.Range(0, gameInfo.playersCount).ToList(); var seatManager = ICloudSync.Instance.SeatManager; var seats = userIndexes.Select(u => seatManager.GetSeat((SeatIndex)u)).ToList(); return seats; } // 3.检查确认开始游戏 private void CheckStartGame() { if (State != GameState.Loaded) return; // 检查玩家加入到齐、确认OK后,开始正式一局游戏玩法。 if (ArePlayersJoined(seats)) { InitPlayerInfos(seats); // 开始正式一局游戏玩法 StartGame(); } } private bool ArePlayersJoined(List<ICloudSeat> seats) { var ret = seats.All(s => s is { State: SeatState.InUse }); Debug.Log($"Game ArePlayersJoined: {ret}"); return ret; } // 4.a 收到事件:玩家加入 private void OnPlayerJoined(ICloudSeat seat) { Debug.Log($"Game OnPlayerJoin seat index: {seat.Index}"); if (State == GameState.Loaded) CheckStartGame(); } /// 4.b 收到事件:玩家离开 private void OnPlayerLeaving(ICloudSeat seat) { if (State != GameState.Started) return; if (!_gameInfo.isPk) return; CheckGameEnd(); }

结束同玩

流程图:

同玩中 → 结束同玩流程图:
    1.调用结束同玩 EndMatchGame
    默认:所有人结束同玩
    可选:EndMatchGame(SeatIndex)踢掉单个,指定让单个用户退出
    返回:成功 / 失败
    2.触发事件:OnEndMatchEvent 结束同玩了(返回单播)
    a.玩家B,回流到自己实例后,收到的事件参数中, EndType = EndMatchGame
    b.房主A,结束同玩相关操作结束后,收到的事件参数中, EndType = EndHostState
    3.触发事件:OnSeatPlayerLeaving 玩家 B 离开

1.结束同玩 EndMatchGame

接口:IMatchManager.EndMatchGame
结束同玩:让所有人、或让指定座位号的玩家,回到单播状态。
方法有多种重载:
Task<IEndResult> EndMatchGame(string endInfo = "")
结束同玩:所有人回到单播状态。
Task<IEndResult> EndMatchGame(InfoMapping infoMapping)
结束同玩:所有人回到单播状态。可以给每个用户分别发送不同的结束信息。
Task<IEndResult> EndMatchGame(SeatIndex seatIndex, string endInfo = "")
指定座位号,单个用户结束同玩:使他退出、回到单播状态。
    参数 endInfo :结束信息,用json透传,传递给同玩的用户
    参数 infoMapping:结束信息映射,可以给每个用户分别发送不同的结束信息
    参数 SeatIndex:可以指定座位号,单个用户结束同玩(使他退出、回到单播状态)
    返回 IEndResult:结束同玩结果。包含:
    bool IsSuccess 是否成功
    EndResultCode Code 返回码
    string Message 返回信息、错误信息
【使用举例】:
    当 用户A 是房主,用户B 是加入的同玩玩家:
    同玩一局游戏结束,显示结算界面,本局分数情况。 界面中显示按钮【退出】
    当点击【退出】时,应区分是否房主,选择如何结束同玩:
    若是房主A点击【退出】调用EndMatchGame(string endInfo) 结束所有人同玩(不指定座位号),endInfo 传入结束信息
    玩家B 会退出同玩、回流,到自己的原实例,进入单播状态。
    玩家B 会收到OnEndMatchEvent事件,事件参数中, EndType = EndMatchGame,表示结束同玩了,且事件会携带结束信息 endInfo 。
    房主A 在相关操作成功后,会结束房主状态,进入单播状态。
    房主A 也会收到OnEndMatchEvent事件,事件参数中, EndType = EndHostState,表示结束房主状态了。
    注:相关说明:
    调用EndMatchGame,是在A实例的玩法进程中调用
    玩家B 回流:会断开从A实例的拉流。 此时单播状态:单播自己的画面、不再从A拉流画面。
    玩家B 收到OnEndMatchEvent事件参数中,携带的结束信息 endInfo,内容为当时调用结束同玩时,传入的 endInfo(或infoMapping)。
    注意:玩家B 收到OnEndMatchEvent事件,是在 B 自己原来实例的玩法进程中收到,而不是在刚才A的实例的玩法进程中。
    若是玩家B点击【退出】,调用EndMatchGame(SeatIndex seatIndex, string endInfo) 且 seatIndex 指定座位号 index1,[可选] endInfo 传入自定义结束信息
    所指定的玩家 B 会退出同玩、回流,回到自己的单播状态。
    玩家 B 原实例中,会收到OnEndMatchEvent事件
    注:相关说明:
    调用指定座位号的EndMatchGame,也是在A实例的玩法进程中调用。只不过一般是由玩家B 的 操作点击触发的,但执行的代码还是在A这里。
    座位号 SeatIndex 序号从 0 开始,0 为房主自己,从 1 开始为其他同玩玩家。
    [可选] endInfo 传入自定义结束信息,因为他是自己主动退了, 他已经看到了结算信息,所以可按需要自定义透传什么结束信息。
    注意:玩家B 收到OnEndMatchEvent事件,是在自己原来实例的玩法进程中收到(同上结束所有人同玩的情况一样)。
    如果其他玩家都退出了,A还没有点击【退出】:
    注:其他玩家退出同玩时,房主侧不强制退出同玩状态。可以A自己点击退出时,调用结束同玩接口EndMatchGame() (不指定座位号),返回单播。
【示例代码】:结束同玩: (可对照参考 Demo 工程代码的 Scripts/PK/UIGameResult.cs ):
private ICloudSeat _seat; /// 显示结算 public void ShowGameResult(GameResult result, ICloudSeat seat) { _seat = view.Seat; } // 当点击【退出】 private async void OnClickExit() { // 要传递的结束信息 var resultJson = JsonUtility.ToJson(_result); // 结束同玩 var result = await EndMatchGame(_seat.IsHost(), _seat.Index, resultJson); if (!result.IsSuccess) { // 结束同玩 result 失败 OnEndMatchFail(result); return; } // 结束同玩 result 成功 OnConfirmEnd(); } // 区分是否房主,结束同玩 private async Task<IEndResult> EndMatchGame(bool isHost, SeatIndex seatIndex, string endInfo) { Debug.Log($"GameResult EndMatchGame isHost: {isHost}, seatIndex: {seatIndex}"); IEndResult result; if (isHost) { // 若是房主A点击【退出】,调用EndMatchGame 且不指定座位号,并传入结束信息 // 逻辑其实是踢所有人,这时我们需要把结算结果透传给另一个主播 // 让另一个主播在回到原实例后仍可继续看结算结果(收到OnEndMatchEvent事件) result = await ICloudSync.Instance.MatchManager.EndMatchGame(endInfo); } else { // 若是玩家B点击【退出】,调用EndMatchGame 且指定座位号 index1,并传入结束信息 // [可选] endInfo 传入自定义结束信息,因为他是自己主动退了, 他已经看到了结算信息,所以可按需要自定义透传什么结束信息。 result = await ICloudSync.Instance.MatchManager.EndMatchGame(seatIndex, endInfo); } return result; } // 示例:结束同玩失败时,做相关提示 private void OnEndMatchFail(IEndResult result) { var debugMsg = $"code: {result.Code} ({(int)result.Code}), msg: {result.Message}"; // 根据返回码:`result.Code`枚举判断 switch (result.Code) { case EndResultCode.Success: break; case EndResultCode.Timeout: Debug.LogWarning($"EndMatchGame 结束同玩超时 {debugMsg}"); ShowNote("结束同玩超时"); break; case EndResultCode.Error: default: Debug.LogError($"EndMatchGame 结束同玩失败 {debugMsg}"); ShowNote($"结束同玩失败! {debugMsg}"); break; } }

2.事件:结束同玩了

调用结束同玩 ICloudMatchManager.EndMatchGame 后:
    加入了的玩家,回流到自己实例后,会收到事件:OnEndMatchEvent
    如果房主结束同玩,相关操作结束后,房主自己也会收到事件:OnEndMatchEvent
【使用举例】:
详见上一小节”#结束同玩 EndMatchGame“ 中的使用举例。已详细说明。
注:结束同玩成功后,在房主侧、回流的玩家侧,分别要做什么不同表现,这部分开发者可以自定义实现。
【示例代码】:收到结束同玩了事件时,检查事件类型、做相应展示:(可对照参考 Demo 工程代码的 Scripts/PK/Main.cs ):
// 监听事件 MatchManager.OnEndMatchEvent += OnEndMatchEvent; // 收到事件:结束同玩了(返回单播) private void OnEndMatchEvent(IEndEvent endEvent) { var endType = endEvent.EndType; var info = endEvent.EndInfo; Debug.Log($"OnEndMatchEvent, endType: {endType}, has info: {endEvent.HasEndInfo()}"); // 重置本地游戏状态、界面,允许后续操作。 ViewA.Clean(); SetState(State.Ready); switch (endType) { case EndEventType.EndMatchGame: // 玩家B回流到自己实例 ShowHostNote("回流了"); try { // 回流的玩家,收到结算信息 var gameResult = JsonUtility.FromJson<GameResult>(info); if (gameResult != null) ViewA.ShowGameResult(gameResult, false); } catch (Exception) { ShowHostAlert("endInfo", info); } break; case EndEventType.EndHostState: // 房主自己结束同玩了 ShowHostNote("结束同玩了"); break; case EndEventType.ExitForJoinError: case EndEventType.ExitForGameExpired: default: ShowHostNote($"退出同玩了 ({endType})"); break; } }

3.事件:玩家离开

如果同玩状态下,玩家离开,可以做相应展示。
如果离开后,没有其他玩家了,要做什么表现,这部分开发者可以自定义实现。
举例:如果同玩已正式开始一局,可以将本局提前结束。
【示例代码】
private void OnPlayerLeaving(ICloudSeat seat) { var playerText = seat.IsHost() ? "主播" : $"玩家{seat.IntIndex + 1}"; ViewA.ShowNote($"{playerText}{seat.PlayerInfo?.NickName} 离开了游戏"); if (State != GameState.Started) return; CheckGameEnd(); } private void CheckGameEnd() { if (State != GameState.Started) return; if (AreOtherSeatsDisconnected()) { // 其他玩家都已退出同玩。房主侧不强制退出,开发者可以在做好合适的展示后,自己调用`结束同玩`接口 Debug.Log("其他玩家都已退出同玩"); EndGame(); return; } if (_liveBlockCount == 0) { Debug.Log("游戏关卡完成"); EndGame(); } }
  

附:变更记录

版本 2.5.0 :
    变更:结束同玩 EndMatchGame :
    现在支持异步等待,且会返回 result 。
    相关示例代码,增加了处理结束同玩 result 成功失败。
    变更:事件:结束同玩了 :
    新事件名:OnEndMatchEvent
    新事件的参数: IEndEvent endEvent 结束同玩事件,包含多个成员属性,如:枚举 EndType 结束事件类型、string EndInfo 结束信息、等。
    废弃旧事件名:OnEndMatchGame
    相关示例代码,增加了处理 endEvent 的事件类型、结束信息。