Relay SDK for NetCode 使用教程
Relay SDK for NetCode 使用教程
使用Sync Relay & Netcode轻松构建联机游戏数据同步指南
1.安装配置 UOS Launcher
新建一个 Unity 项目工程,在本教程中学习使用 Netcode for GameObjects 资源包结合 UOS 的 Sync Relay 服务来实现高效、稳定的联机游戏的数据同步。
参考 Launcher 教程,安装 Launcher 后,关联 UOS APP, 开启 Sync Relay 服务并安装 Sync Relay SDK。
注意: 请务必确认在进行后续教程之前,已经成功完成了 Launcher 教程的安装步骤,否则可能导致后续接入无法顺利进行。
2.操作步骤
2.1.安装 Netcode for GameObjects 资源包
在 Unity 编辑器中,点击 Window -> Package Manager 打开包管理器。在包管理器窗口中点击 「+」 按钮,选择 「Add package by name」,填入包名 com.unity.netcode.gameobjects 和版本号 1.3.1,点击 「Add」。
注意:推荐安装 1.3.1 及以上的 Netcode for GameObjects 版本
Netcode for GameObjects 的使用流程参考 官方教程
2.2.导入项目素材资源包
在 UOS Launcher 中点击「import sample」按钮,即可导入项目示例工程资源包。
2.3.打开 Sample 场景
导入资源包后,在 Assets/Samples/UOS Sync Relay/X.Y.Z/Sync Relay Netcode Demo/Scenes/SampleScene.unity 路径下(其中 X.Y.Z 为已安装的 Sync - Relay SDK 的版本号),可以看到示例的 SampleScene 场景并打开。
打开场景后,在弹出的窗口中,点击「Import TMP Essentials」来导入 TextMeshPro 的字体资源。
2.4.配置信息
1. 选择 RelayTransportNetcode 作为 Netcode 的传输协议
在 Network Manager 的 Network Transport 处,选择 RelayTransportNetcode 作为传输协议。
2. 填入 UOS 相关配置信息
这里需要我们从 UOS 网页端 Sync Relay 的配置一栏,找到「房间配置ID」并复制。
将刚才复制的「房间配置ID」粘贴到 Relay Transport(Netcode).cs 组件的 Room Profile UUID 变量中。
2.5.运行项目
我们选择 一个在编辑器中 Play 模式下测试,另一个提前 build 成 exe 文件 的方式来测试网络同步功能。
在编辑器中点击 Create Host 的按钮,由于当前编辑器既是作为服务器又是作为客户端启动的,所以会克隆生成一个之前设置好的 PlayerCube 预制物体对象。

通过 W/S 按键实现前后移动物体, A/D 按键实现左右旋转物体。
打开客户端可执行 exe 文件,点击 Join Game as Client,以客户端的身份加入游戏中。

这时候可以看到两个窗口中的两个 Capsule 对象的位置、旋转角度的数据都是一样的,说明我们已经实现游戏对象的网络同步了。

2.6.自定义创建新的房间配置
可以在 Sync Relay 的「配置」页面,根据游戏房间的规模、存在时长、负载与性能,选择房间模板并创建自己的房间配置。 点击「创建房间配置」按钮,自己创建一个房间配置如下:自行选择微型、小型还是中型的房间规模。
概念说明:
房间配置的超时时长(分钟)
指一个房间从创建到关闭的最长存续时间。无论房间内是否有玩家,在到期时房间都会自动关闭。
空房间的超时时长(秒)
没有玩家连接的空房间可以存续的最大时间。设置为 0 时表示不检查空房间超时。
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);
}
}NetcodeTransport
RelayTransportNetcode 是 Sync Relay 提供的实现 Netcode Transport的类
namespace Unity.Sync.Relay.Transport.Netcode
{
public class RelayTransportNetcode : NetworkTransport
{
// 默认为false
// 设为true将会禁用掉Netcode里NetworkTransport的DisconnectRemoteClient()方法
public bool DisableDisconnectRemoteClient = false;
// 设置房间信息
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);
// 踢掉玩家
// reason可不填,默认为空
public void KickPlayer(uint transportId, string reason);
// 获取客户端到Relay Server的往返时延
public ulong GetRelayServerRtt();
// 设置心跳区间时长
public void SetHeartbeat(uint seconds, Action<uint> callback = null);
}
}4.集成示例
示例代码
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Sync.Relay;
using Unity.Sync.Relay.Lobby;
using Unity.Sync.Relay.Model;
using Unity.Sync.Relay.Transport.Netcode;
public class Demo : MonoBehaviour
{
private string playerUuid;
private string playerName;
// Start()会在第一帧Update()之前被调用
private void Start()
{
// 需要在创建或加入房间之前,设置好玩家信息
// PlayerUuid是Unique ID,用于表明用户的身份
// PlayerName是用户名
playerUuid = Guid.NewGuid().ToString();
playerName = "Player-" + playerUuid;
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetPlayerData(playerUuid, playerName);
// 需要在创建或加入房间之前,配置好回调函数
var callbacks = new RelayCallbacks();
callbacks.RegisterConnectToRelayServer(OnConnectToRelayServer);
callbacks.RegisterSetHeartbeat(OnSetHeartBeat);
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetCallbacks(callbacks);
}
public void OnConnectToRelayServer(uint code, RelayRoom room)
{
Debug.Log("OnConnectToRelayServer Called");
if (code == (uint)RelayCode.OK)
{
Debug.LogFormat("Connect To Relay Server Succeed. ( Room : {0} )", room.Name);
}
else
{
Debug.LogFormat("Connect To Relay Server Failed with Code {0}.", code);
}
}
public void OnSetHeartBeat(uint code, uint timeout)
{
Debug.Log("OnSetHeartBeat Called");
Debug.LogFormat("Code {0} Timeout {1}", code, timeout);
}
// 以Server身份加入游戏
public void OnStartServerButton()
{
// 异步创建房间
StartCoroutine(LobbyService.AsyncCreateRoom(new CreateRoomRequest()
{
Name = "Demo",
Namespace = "Unity",
MaxPlayers = 4, // 选填项,默认值为0,表示不设上限
OwnerId = playerUuid, // 必填项
},( resp) =>
{
if ( resp.Code == (uint)RelayCode.OK )
{
Debug.Log("Create Room succeed.");
if (resp.Status == LobbyRoomStatus.ServerAllocated)
{
// 需要在连接到Relay服务器之前,设置好房间信息
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetRoomData(resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetJoinCode(JoinCode);
StartServer();
}
else
{
Debug.Log("Room Status Exception : " + resp.Status.ToString());
}
}
else
{
Debug.Log("Create Room Fail By Lobby Service");
}
}));
}
private void StartServer()
{
NetworkManager.Singleton.StartServer();
}
// 以Host身份加入游戏
public void OnStartHostButton()
{
// 异步创建房间
StartCoroutine(LobbyService.AsyncCreateRoom(new CreateRoomRequest()
{
Name = "Demo",
Namespace = "Unity",
MaxPlayers = 4, // 选填项,默认值为0,表示不设上限
OwnerId = 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服务器之前,设置好房间信息
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetRoomData(resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetJoinCode(JoinCode);
StartHost();
}
else
{
Debug.Log("Room Status Exception : " + resp.Status.ToString());
}
}
else
{
Debug.Log("Create Room Fail By Lobby Service");
}
}));
}
private void StartHost()
{
NetworkManager.Singleton.StartHost();
}
// 以Client身份加入游戏
public void OnStartClientButton()
{
// 异步查询房间列表
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服务器之前,设置好房间信息
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>()
.SetRoomData(_resp);
// 如果是Private类型的房间,需要开发者自行获取JoinCode,并调用以下方法设置好
// NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetJoinCode(JoinCode);
StartClient();
}
else
{
Debug.Log("Query Room Fail By Lobby Service");
}
}));
break;
}
}
}
}
else
{
Debug.Log("List Room Fail By Lobby Service");
}
}));
}
private void StartClient()
{
NetworkManager.Singleton.StartClient();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.P))
{
Debug.Log("Update Player Info");
var p = NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().GetCurrentPlayer();
p.Properties.Add("logo", "unity");
// 更新玩家信息
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().UpdatePlayerInfo(p);
}
if (Input.GetKeyDown(KeyCode.R))
{
Debug.Log("Update Room Custom Properties");
var p = new Dictionary<string, string>();
p.Add("logo", "unity");
// 更新房间属性
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().UpdateRoomCustomProperties(p);
}
}
}Sync - Relay SDK 的使用流程可参考以下示例程序: 示例程序
Matchmaking 集成示例
// Relay模式下,由于主机玩家(Host)要处理游戏逻辑,需要保证最先加入房间,因此在集成Matchmaking时会进行一些额外的操作,流程如下
public async void MatchmakingAndJoin()
{
// 初始化玩家信息
// Matchmaking在为Relay模式匹配玩家时,会在系统内部选择一个PlayerId作为主机,后续玩家连接到Relay服务器时,会据此判断自己是否为主机玩家
// 所以需要确保创建Ticket时和加入房间时传入的PlayerId一致
var playerId = Guid.NewGuid().ToString(); // playerId 是 Unique ID,用于表明用户的身份,请填入真实值
var playerName = "Player-" + playerId; // playerName 是用户名,请填入真实值
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().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)
{
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetRoomData(resp);
NetworkManager.Singleton.StartHost();
}
else
{
NetworkManager.Singleton.GetComponent<RelayTransportNetcode>().SetRoomData(resp);
NetworkManager.Singleton.StartClient();
}
}
else
{
Debug.LogFormat("Query Room Status Failed with reason - {0}", resp.ErrorMessage);
}
}));
}