Relay SDK for Mirror 接入指南
Relay SDK for Mirror 接入指南
1.安装配置 UOS Launcher
参考 Launcher 教程,安装 Launcher 后,关联 UOS APP, 开启 Sync Relay 服务并安装 Sync Relay SDK。
注意: 请务必确认在进行后续教程之前,已经成功完成了 Launcher 教程的安装步骤,否则可能导致后续接入无法顺利进行。
2.操作步骤
使用整体流程可参考 Mirror官方教程 和 示例程序
配置信息
1. 选择 RelayTransportMirror 作为Mirror的传输协议
选中NetworkManager, 在Inspector页面点击「Add Component」,选择Scripts → Unity.Sync.Relay → Relay Transport Mirror, 然后将Relay Transport Mirror拖到NetworkManager的Transport处
2. 填入UOS相关配置信息
在Relay Transport Mirror处,填入 Room Profile UUID
注: 此处 RoomProfileUUID 如何获取
此处的 RoomProfileUUID 可在 UOS「Sync -> 配置」处的房间列表项内获取
3.相关接口
RelayRoom
RelayRoom 是描述房间信息的类
namespace Unity.Sync.Relay.Model
{
public class RelayRoom
{
public string Name;
public string NameSpace;
public string ID;
public ulong MasterClientID;
public Dictionary<uint, RelayPlayer> Players;
public LobbyRoomStatus Status;
public string IP;
public ushort Port;
public string JoinCode;
public Dictionary<string, string> CustomProperties;
public string RoomCode;
public LobbyRoomVisibility Visibility;
}
}RelayPlayer
RelayPlayer 是描述玩家信息的类
namespace Unity.Sync.Relay.Model
{
public class RelayPlayer
{
public string ID;
public string Name;
public uint TransportId;
public Dictionary<string, string> Properties;
}
}Lobby
Lobby 是 Sync Relay 为客户端提供的异步创建房间/异步查询房间列表/异步查询房间信息的类
namespace Unity.Sync.Relay.Lobby
{
public class CreateRoomRequest;
public class CreateRoomResponse;
public class ListRoomRequest;
public class ListRoomResponse;
public class QueryRoomResponse;
public class ChangeRoomStatusResponse;
public class LobbyService
{
// 异步创建房间
public static IEnumerator AsyncCreateRoom(CreateRoomRequest req, Action<CreateRoomResponse> callback);
// 异步查询房间列表
public static IEnumerator AsyncListRoom(ListRoomRequest request, Action<ListRoomResponse> callback);
// 异步查询房间信息
public static IEnumerator AsyncQueryRoom(String roomId, Action<QueryRoomResponse> callback);
// 改变房间状态,仅支持在Ready和Running之间切换
public static IEnumerator ChangeRoomStatus(String roomUuid, LobbyRoomStatus status, Action<ChangeRoomStatusResponse> callback);
// 快速加入房间
public static IEnumerator QuickJoinRoom(QuickJoinRequest request, Action<QuickJoinResponse> callback);
// 异步查询房间信息,满足房间处于可加入状态时返回结果
// 可加入状态是指,Host 获取到的房间状态为已分配,或者 Client 获取到的房间状态为已就绪
// 回调函数里的 bool 参数表示当前客户端是否为 Host
// 目前主要用于和 Matchmaking 服务对接,playerId 需要和 Matchmaking 服务里创建 ticket 所用的 playerId 一致, roomId 可以从匹配成功的 ticket 里获取
// timeout 表示轮询过程的最长持续时间,默认20s
public static IEnumerator AsyncQueryRoomUntilReady(String playerId, String roomId, Action<bool, QueryRoomResponse> callback, int timeout = 20);
// 根据 RoomCode 异步查询房间信息
public static IEnumerator AsyncQueryRoomByRoomCode(String roomCode, Action<QueryRoomResponse> callback);
// 更新房间属性,目前仅支持更新 Visibility、JoinCode、RoomName(房间名) 和 MaxPlayers (这个需要注意当前房间的人数)
public static IEnumerator AsyncUpdateRoom(string roomId, UpdateRoomRequest request, Action<UpdateRoomResponse> callback);
}
}RelayCallbacks
RelayCallbacks 是 Sync Relay 提供的用户可自定义的回调类
namespace Unity.Sync.Relay
{
// 目前支持的回调函数类型
public enum RelayCallback
{
ConnectToRelayServer = 0,
MasterClientMigrate,
PlayerInfoUpdate,
RoomInfoUpdate,
PlayerKicked,
PlayerEnterRoom,
PlayerLeaveRoom,
SetHeartbeat
}
public class RelayCallbacks
{
// 目前支持的回调函数接口定义
// uint code, 表示连接的结果,可参考RelayCode
// RelayRoom room, 连接成功返回当前房间信息,失败返回null
public Action<uint, RelayRoom> OnConnectToRelayServerCallback;
// uint newMasterClientID, 新的MasterClient的TransportId
// 在MasterClient在退出房间或掉线时会触发,(对于Netcode,如果未勾选DisableDisconnectRemoteClient,则仅会在被动掉线时触发)
// 注册该回调函数后,如果MasterClient离开,当前玩家不会断开连接,后续流程会由OnMasterClientMigrateCallback处理
public Action<uint> OnMasterClientMigrateCallback;
// RelayPlayer player, 表示更新的玩家信息
public Action<RelayPlayer> OnPlayerInfoUpdateCallback;
// RelayRoom room, 表示更新后的房间信息
public Action<RelayRoom> OnRoomInfoUpdateCallback;
// uint code, 表示玩家被踢掉的原因,可参考RelayCode
// string reason, 表示玩家被踢掉的原因
public Action<uint, string> OnPlayerKickedCallback;
// RelayPlayer player, 表示加入房间的玩家信息
public Action<RelayPlayer> OnPlayerEnterRoom;
// RelayPlayer player, 表示离开房间的玩家信息
public Action<RelayPlayer> OnPlayerLeaveRoom;
// 当客户端到Relay Server的心跳超时时会触发
public Action OnHeartbeatTimeout;
// 调用SetHeartbeat完成后触发
// uint code, 表示设置的结果,可参考RelayCode
// uint timeout, 心跳超时时间,单位为s
public Action<uint, uint> OnSetHeartbeat;
// 注册回调函数,重复调用会覆盖之前的记录(确保回调函数的类型和定义保持一致)
public void RegisterConnectToRelayServer(Action<uint, RelayRoom> callback);
public void RegisterMasterClientMigrate(Action<uint> callback);
public void RegisterPlayerInfoUpdate(Action<RelayPlayer> callback);
public void RegisterRoomInfoUpdate(Action<RelayRoom> callback);
public void RegisterPlayerKicked(Action<uint, string> callback);
public void RegisterPlayerEnterRoom(Action<RelayPlayer> callback);
public void RegisterPlayerLeaveRoom(Action<RelayPlayer> callback);
public void RegisterHeartbeatTimout(Action callback);
public void RegisterSetHeartbeat(Action<uint, uint> callback);
// 删除回调函数
public void Remove(RelayCallback code);
}
}MirrorTransport
RelayTransportMirror 是 Sync Relay 提供的实现Mirror Transport的类
namespace Unity.Sync.Relay.Transport.Mirror
{
public class RelayTransportMirror : Transport
{
// 设置房间信息
public void SetRoomData(CreateRoomResponse resp);
public void SetRoomData(QueryRoomResponse resp);
// 设置私有房间的Join Code
public void SetJoinCode(string joinCode);
// 设置玩家信息
public void SetPlayerData(string Id, string Name);
public void SetPlayerData(string Id, string Name, Dictionary<string, string> Properties);
// 设置回调函数
public void SetCallbacks(RelayCallbacks callbacks);
// 获取房间/玩家的信息
public RelayRoom GetRoomInfo();
public RelayPlayer GetPlayerInfo(uint transportId);
public RelayPlayer GetCurrentPlayer();
// 更新玩家信息/房间属性
// 玩家信息是根据 TransportId 更新 Name/Properties ( ID和TransportId不支持更新 )
public void UpdatePlayerInfo(RelayPlayer player);
public void UpdateRoomCustomProperties(Dictionary<string, string> properties);
// 获取客户端到Relay Server的往返时延
public ulong GetRelayServerRtt();
// reason可不填,默认为空
public void KickPlayer(uint transportId, string reason);
// 设置Relay Server的心跳超时时间,单位s,范围为5 - 600
// 如果不传入callback,可以通过RegisterSetHeartbeat注册回调函数接收结果
public void SetHeartbeat(uint seconds, Action<uint> callback);
public void SetHeartbeat(uint seconds);
// 暂停/重启传输层协议的心跳
// 如果有需要临时延长心跳超时时间,先调用SetHeartbeat(),再调用Pause()
public void Pause();
public void UnPause();
// 获取Relay Server的默认心跳超时时间,单位s
public uint GetDefaultHeartbeat();
}
}4.集成示例
示例代码
using System;
using Mirror;
using Unity.Sync.Relay;
using Unity.Sync.Relay.Lobby;
using Unity.Sync.Relay.Model;
using UnityEngine;
namespace Unity.Sync.Relay
{
public class RelayNetworkManagerHUD : MonoBehaviour
{
NetworkManager manager;
private string playerUuid;
private string playerName;
void Awake()
{
manager = GetComponent<NetworkManager>();
}
private void Start()
{
// 需要在创建或加入房间之前,设置好玩家信息
// PlayerUuid是Unique ID,用于表明用户的身份
// PlayerName是用户名
playerUuid = Guid.NewGuid().ToString();
playerName = "Player-" + playerUuid;
manager.GetComponent<RelayTransportMirror>().SetPlayerData(playerUuid, playerName);
}
// 以Server身份加入游戏
private void StartServer()
{
// 异步创建房间
StartCoroutine(LobbyService.AsyncCreateRoom(new CreateRoomRequest()
{
Name = "Demo",
// Namespace = "Unity", // 选填项,可用于房间按照NameSpace过滤
MaxPlayers = 4, // 选填项,默认值为0,表示不设上限
OwnerId = playerUuid, // 必填项,如果当前客户端为房主,需要保证OwnerId的值与SetPlayerData方法中的playerUuid一致
},(resp) =>
{
if ( resp.Code == (uint)RelayCode.OK )
{
Debug.Log("Create Room succeed.");
if (resp.Status == LobbyRoomStatus.ServerAllocated)
{
// 需要在连接到Relay服务器之前,设置好房间信息
manager.GetComponent<RelayTransportMirror>().SetRoomData(resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// manager.GetComponent<RelayTransportMirror>().SetJoinCode(JoinCode);
manager.StartServer();
}
else
{
Debug.Log("Room Status Exception : " + resp.Status.ToString());
}
}
else
{
Debug.Log("Create Room Fail By Lobby Service");
}
}));
}
// 以Host身份加入游戏
private void StartHost()
{
// 异步创建房间
StartCoroutine(LobbyService.AsyncCreateRoom(new CreateRoomRequest()
{
Name = "Demo",
// Namespace = "Unity", // 选填项,可用于房间按照NameSpace过滤
MaxPlayers = 4, // 选填项,默认值为0,表示不设上限
OwnerId = playerUuid, // 必填项,如果当前客户端为房主,需要保证OwnerId的值与SetPlayerData方法中的playerUuid一致
Visibility = LobbyRoomVisibility.Public, // 选填项,默认值为Public,如果选择Private,则必须带上JoinCode
// JoinCode = "U", // 选填项,仅在Visibility值为Private时带上
},(resp) =>
{
if ( resp.Code == (uint)RelayCode.OK )
{
Debug.Log("Create Room succeed.");
if (resp.Status == LobbyRoomStatus.ServerAllocated)
{
// 需要在连接到Relay服务器之前,设置好房间信息
manager.GetComponent<RelayTransportMirror>().SetRoomData(resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// manager.GetComponent<RelayTransportMirror>().SetJoinCode(JoinCode);
manager.StartHost();
}
else
{
Debug.Log("Room Status Exception : " + resp.Status.ToString());
}
}
else
{
Debug.Log("Create Room Fail By Lobby Service");
}
}));
}
// 以Client身份加入游戏
private void StartClient()
{
// 异步查询房间列表
StartCoroutine(LobbyService.AsyncListRoom(new ListRoomRequest()
{
Start = 0,
Count = 10,
// Name = "Demo", // 选填项,可用于房间名的模糊搜索
// Namespace = "Unity", // 选填项,可用于列出指定Namespace的房间
Statuses = new List<LobbyRoomStatus>() { LobbyRoomStatus.Ready, LobbyRoomStatus.Running } // 选填项,不填会默认返回Ready状态的房间
}, (resp) =>
{
if (resp.Code == (uint)RelayCode.OK)
{
Debug.Log("List Room succeed.");
if (resp.Items.Count > 0)
{
foreach (var item in resp.Items)
{
if (item.Status == LobbyRoomStatus.Ready)
{
// 异步查询房间信息
StartCoroutine(LobbyService.AsyncQueryRoom(item.RoomUuid,
( _resp) =>
{
if (_resp.Code == (uint)RelayCode.OK)
{
// 需要在连接到Relay服务器之前,设置好房间信息
manager.GetComponent<RelayTransportMirror>().SetRoomData(_resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// manager.GetComponent<RelayTransportMirror>().SetJoinCode(JoinCode);
manager.StartClient();
}
else
{
Debug.Log("Query Room Fail By Lobby Service");
}
}));
break;
}
}
}
}
else
{
Debug.Log("List Room Fail By Lobby Service");
}
}));
}
}
}Matchmaking 集成示例
// Relay模式下,由于主机玩家(Host)要处理游戏逻辑,需要保证最先加入房间,因此在集成Matchmaking时会进行一些额外的操作,流程如下
public async void MatchmakingAndJoin()
{
// 初始化玩家信息
// Matchmaking在为Relay模式匹配玩家时,会在系统内部选择一个PlayerId作为主机,后续玩家连接到Relay服务器时,会据此判断自己是否为主机玩家
// 所以需要确保创建Ticket时和加入房间时传入的PlayerId一致
var manager = GetComponent<NetworkManager>();
var playerId = Guid.NewGuid().ToString(); // playerId 是 Unique ID,用于表明用户的身份,请填入真实值
var playerName = "Player-" + playerId; // playerName 是用户名,请填入真实值
manager.GetComponent<RelayTransportMirror>().SetPlayerData(playerId, playerName);
var configId = "xx"; // configId 请从 uos 官网上的 matchmaking 页面获取
// 以下为Matchmaking流程,使用Matchmaking前请先参考Matchmaking文档初始化MatchmakingSDK。
// 创建Ticket并等待匹配成功
var player = new Player { id = playerId };
Ticket ticket = new Ticket();
var ticketId = await MatchmakingSDK.Instance.CreateTicketAsync(configId, new List<Player> { player });
for (var i = 0; i < 60; i++)
{
ticket = await MatchmakingSDK.Instance.GetTicketAsync(ticketId);
if (ticket.status == MatchmakingSDK.TicketStatusMatched)
{
break;
}
// 使用Thread.Sleep延时1s再轮询,实际使用哪种延时方法可结合你的游戏场景自行决定
Thread.Sleep(1000);
}
if (ticket.status != MatchmakingSDK.TicketStatusMatched)
{
// 匹配失败
await MatchmakingSDK.Instance.DeleteTicketAsync(ticketId);
return;
}
// 匹配成功后,从Ticket里获取房间ID,然后通过LobbyService.AsyncQueryRoomUntilReady方法来检测房间状态,并根据当前客户端的身份选择连接服务器的方式
// 主机玩家会优先返回Response,副机玩家(Client)会在主机玩家成功加入后收到Response
var roomUuid = ticket.assignment.roomId;
StartCoroutine(LobbyService.AsyncQueryRoomUntilReady(playerId, roomUuid, (isHost, resp) =>
{
if (resp.Code == (uint)RelayCode.OK)
{
if (isHost)
{
manager.GetComponent<RelayTransportMirror>().SetRoomData(resp);
manager.StartHost();
}
else
{
manager.GetComponent<RelayTransportMirror>().SetRoomData(resp);
manager.StartClient();
}
}
else
{
Debug.LogFormat("Query Room Status Failed with reason - {0}", resp.ErrorMessage);
}
}));
}