搭配 UTP 使用 Relay
Relay SDK 非常适合搭配 Unity Transport Package (UTP) 使用。UTP 是专为 Unity 游戏引擎构建的现代网络库,用于网络抽象化。借助 UTP,开发者可以专注于游戏本身,而非底层的协议和网络框架。UTP 与网络代码无关,这意味着它可以用于各种高层网络代码的抽象化,支持所有 Unity 网络代码解决方案,并可与其他网络代码库配合使用。
注意:建议最好搭配 NGO 使用 Relay。如果希望使用 NGO 和自定义网络代码等来同步游戏对象,可以搭配 UTP 使用 Relay 用于传输和 Relay 代理用途。
配置 Relay SDK
在使用 Relay 前,必须先配置 Unity 项目。要进行配置,请访问开始使用 Relay。如果已经启用 Relay 服务,请通过 Unity 编辑器链接到您的 Unity Project ID。
请查看 Simple Relay Sample (using UTP),了解搭配 UTP 使用 Relay 的基础知识。
Simple Relay Sample (using UTP)
该示例在 Simple Relay Sample 基础上进行了扩充,展示了搭配 UTP 使用 Relay 构建多人游戏的基本情况。从 Package Manager(包管理器)中的 Relay 包导入示例,然后按提供的脚本进行操作。
该示例至少需要两个单独的客户端,即一个主机玩家和一个或多个加入玩家。
在测试示例项目代码时,您可能希望在同一台机器上运行多个客户端。为此,您可以将项目构建到可执行的二进制文件中,也可以使用 ParrelSync 或 Multiplayer Play Mode 等解决方案,此类解决方案可支持运行同一项目的多个 Unity 编辑器实例。
如果项目构建到二进制文件中,该二进制文件可以与其克隆文件或打开的 Unity 编辑器同时使用。
播放场景时,可以主机玩家(游戏会话发起者)身份或加入玩家身份启动客户端。
必须至少具有一个主机玩家。必须要有主机玩家,加入玩家才能运行游戏。
主机玩家的操作流程如下所示:
登录
获取地区(可选)
选择地区(可选)
分配游戏会话
绑定到所选 Relay 服务器
获取加入代码
主机玩家与加入玩家共享加入代码。加入玩家加入并连接到主机玩家后,主机玩家可以:
向所有连接的玩家发送消息
断开与所有玩家的连接
加入玩家的操作流程如下所示:
登录
使用主机玩家的加入代码加入游戏会话
绑定到所选 Relay 服务器
请求与主机玩家进行连接
连接到主机玩家后,加入玩家可以:
向主机玩家发送消息
断开与主机玩家的连接
导入 Simple Relay Sample (using UTP) 项目
使用 Unity 编辑器(版本 2020.3)打开 Relay 项目。如果没有 Relay 项目,请访问设置 Relay 项目。
打开 Package Manager(包管理器),然后导航到 Relay 包。
展开 **Samples(示例)**部分。
选择 Import(导入),导入“Simple Relay sample (using UTP)(简单 Relay 示例(使用 UTP))”项目。
导入 Simple Relay Sample 项目后,即可将其作为场景打开。该项目位于 Assets/Samples/Relay/1.0.4/Simple Relay sample (using UTP) 下的当前项目中。
选择 File(文件)> Open Scene(打开场景)。
导航至“Simple Relay sample (using UTP)(简单 Relay 示例(使用 UTP))”场景。
初始化 Unity 服务
使用 Relay 等任何 Unity 服务之前,必须初始化 UnityServices 单例模式。请参阅 Services Core API。
await UnityServices.InitializeAsync();
对玩家进行身份验证
主机玩家和连接玩家都必须通过身份验证。对玩家进行身份验证的最简单方法是使用 Authentication 服务的 SignInAnonymouslyAsync()
方法。请访问如何使用匿名登录以及如何使用社交平台登录。
如需更多信息,请访问 Unity Authentication 简介和 Unity Authentication 用例。
public async void OnSignIn()
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
playerId = AuthenticationService.Instance.PlayerId;
Debug.Log($"Signed in. Player ID: {playerId}");
}
主机玩家
以主机玩家身份启动的游戏客户端必须能够创建分配、请求加入代码、配置连接类型、创建用于绑定到 Relay 服务器的 NetworkDriver
实例以及侦听来自加入玩家的连接请求。
主机玩家更新循环
开始执行分配流程的第一步之前,需要设置主机玩家的更新循环,用于处理传入的连接和消息。
注意:为简单起见,示例代码会在每个 Update()
帧上调用此循环。实践中,在单独的协同程序中处理主机玩家更新循环更为合理。
void UpdateHost()
{
// Skip update logic if the Host isn't yet bound.
if (!hostDriver.IsCreated || !hostDriver.Bound)
{
return;
}
// This keeps the binding to the Relay server alive,
// preventing it from timing out due to inactivity.
hostDriver.ScheduleUpdate().Complete();
// Clean up stale connections.
for (int i = 0; i < serverConnections.Length; i++)
{
if (!serverConnections[i].IsCreated)
{
Debug.Log("Stale connection removed");
serverConnections.RemoveAt(i);
--i;
}
}
// Accept incoming client connections.
NetworkConnection incomingConnection;
while ((incomingConnection = hostDriver.Accept()) != default(NetworkConnection))
{
// Adds the requesting Player to the serverConnections list.
// This also sends a Connect event back the requesting Player,
// as a means of acknowledging acceptance.
Debug.Log("Accepted an incoming connection.");
serverConnections.Add(incomingConnection);
}
// Process events from all connections.
for (int i = 0; i < serverConnections.Length; i++)
{
Assert.IsTrue(serverConnections[i].IsCreated);
// Resolve event queue.
NetworkEvent.Type eventType;
while ((eventType = hostDriver.PopEventForConnection(serverConnections[i], out var stream)) != NetworkEvent.Type.Empty)
{
switch (eventType)
{
// Handle Relay events.
case NetworkEvent.Type.Data:
FixedString32Bytes msg = stream.ReadFixedString32();
Debug.Log($"Server received msg: {msg}");
hostLatestMessageReceived = msg.ToString();
break;
// Handle Disconnect events.
case NetworkEvent.Type.Disconnect:
Debug.Log("Server received disconnect from client");
serverConnections[i] = default(NetworkConnection);
break;
}
}
}
}
创建分配
以下代码片段中包含函数 OnAllocate
,该函数展示了如何使用 Relay SDK 创建分配。
public async void OnAllocate()
{
Debug.Log("Host - Creating an allocation. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");
// Determine region to use (user-selected or auto-select/QoS)
string region = GetRegionOrQosDefault();
Debug.Log($"The chosen region is: {region ?? autoSelectRegionName}");
// Set max connections. Can be up to 150, but note the more players connected, the higher the bandwidth/latency impact.
int maxConnections = 4;
// Important: After the allocation is created, you have ten seconds to BIND, else the allocation times out.
hostAllocation = await RelayService.Instance.CreateAllocationAsync(maxConnections, region);
Debug.Log($"Host Allocation ID: {hostAllocation.AllocationId}, region: {hostAllocation.Region}");
// Initialize NetworkConnection list for the server (Host).
// This list object manages the NetworkConnections which represent connected players.
serverConnections = new NativeList<NetworkConnection>(maxConnections, Allocator.Persistent);
}
警告:如果通过 SetRelayServerData
/SetRelayClientData
提供的信息与从分配获得的信息不匹配,您将无法连接到 Relay 服务器。例如,如果 isSecure
参数不匹配,您将收到“无法连接服务器”错误消息。
要防止此类情况发生,最简单的方法之一就是直接从分配构建 RelayServerData
。不过,您必须使用 Netcode for GameObjects (NGO) 版本 1.1.0 或更高版本。
绑定到 Relay server 并侦听连接
以下代码片段中包含函数 OnBindHost
,该函数展示了如何使用 Relay SDK 将主机玩家绑定到 Relay 服务器并侦听来自加入玩家的连接请求。
public void OnBindHost()
{
Debug.Log("Host - Binding to the Relay server using UTP.");
// Extract the Relay server data from the Allocation response.
var relayServerData = new RelayServerData(hostAllocation, "udp");
// Create NetworkSettings using the Relay server data.
var settings = new NetworkSettings();
settings.WithRelayParameters(ref relayServerData);
// Create the Host's NetworkDriver from the NetworkSettings.
hostDriver = NetworkDriver.Create(settings);
// Bind to the Relay server.
if (hostDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
{
Debug.LogError("Host client failed to bind");
}
else
{
if (hostDriver.Listen() != 0)
{
Debug.LogError("Host client failed to listen");
}
else
{
Debug.Log("Host client bound to Relay server");
}
}
}
请求加入代码
以下代码片段中包含函数 OnJoinCode
,该函数展示了如何在成功请求分配后,请求加入代码。主机玩家会与加入玩家共享加入代码,以便加入玩家可以加入主机玩家的游戏会话。
public async void OnJoinCode()
{
Debug.Log("Host - Getting a join code for my allocation. I would share that join code with the other players so they can join my session.");
try
{
joinCode = await RelayService.Instance.GetJoinCodeAsync(hostAllocation.AllocationId);
Debug.Log("Host - Got join code: " + joinCode);
}
catch (RelayServiceException ex)
{
Debug.LogError(ex.Message + "\n" + ex.StackTrace);
}
}
加入玩家
以加入玩家身份启动的游戏客户端必须能够加入分配、配置连接类型、创建用于绑定到主机玩家的 Relay 服务器的 NetworkDriver
实例以及向主机玩家发送连接请求。
注意:默认情况下,用于创建 NetworkDriver
的 DefaultDriverBuilder.RegisterClientDriver
方法会在玩家加入 Relay 服务器时尽量避免使用套接字连接。不过,客户端会在以下情况下使用套接字连接到服务器:
NetworkSimulator
处于启用状态。ClientServerBootstrap.RequestPlayType
设置为Client
。ServerWorld
不存在(出于任何原因)。
玩家更新循环
如同主机玩家一样,加入玩家在加入主机玩家的游戏会话之前,您也需要设置其更新循环,用于处理传入的消息。
注意:为简单起见,示例代码会在每个 Update()
帧上调用此循环。不过,在实践中,在单独的协同程序中处理玩家更新循环更为合理。
void UpdatePlayer()
{
// Skip update logic if the Player isn't yet bound.
if (!playerDriver.IsCreated || !playerDriver.Bound)
{
return;
}
// This keeps the binding to the Relay server alive,
// preventing it from timing out due to inactivity.
playerDriver.ScheduleUpdate().Complete();
// Resolve event queue.
NetworkEvent.Type eventType;
while ((eventType = clientConnection.PopEvent(playerDriver, out var stream)) != NetworkEvent.Type.Empty)
{
switch (eventType)
{
// Handle Relay events.
case NetworkEvent.Type.Data:
FixedString32Bytes msg = stream.ReadFixedString32();
Debug.Log($"Player received msg: {msg}");
playerLatestMessageReceived = msg.ToString();
break;
// Handle Connect events.
case NetworkEvent.Type.Connect:
Debug.Log("Player connected to the Host");
break;
// Handle Disconnect events.
case NetworkEvent.Type.Disconnect:
Debug.Log("Player got disconnected from the Host");
clientConnection = default(NetworkConnection);
break;
}
}
}
加入分配
以下代码片段中包含函数 OnJoin
,该函数展示了如何使用 Relay SDK 借助加入代码加入分配,以及将连接类型配置为 UDP。
public async void OnJoin()
{
// Input join code in the respective input field first.
if (String.IsNullOrEmpty(JoinCodeInput.text))
{
Debug.LogError("Please input a join code.");
return;
}
Debug.Log("Player - Joining host allocation using join code. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");
try
{
playerAllocation = await RelayService.Instance.JoinAllocationAsync(JoinCodeInput.text);
Debug.Log("Player Allocation ID: " + playerAllocation.AllocationId);
}
catch (RelayServiceException ex)
{
Debug.LogError(ex.Message + "\n" + ex.StackTrace);
}
}
警告:如果通过 SetRelayServerData
/SetRelayClientData
提供的信息与从分配获得的信息不匹配,您将无法连接到 Relay 服务器。例如,如果 isSecure
参数不匹配,您将收到“无法连接服务器”错误消息。
要防止此类情况发生,最简单的方法之一就是直接从分配构建 RelayServerData
。不过,您必须使用 Netcode for GameObjects (NGO) 版本 1.1.0 或更高版本。
绑定到 Relay 服务器并连接到主机玩家
以下代码片段中包含函数 OnBindPlayer
,该函数展示了如何绑定到 Relay 服务器。
注意:该函数会将玩家绑定到 Relay 服务器。下一步是将玩家连接到主机玩家,以便双方可以开始交换消息。
public void OnBindPlayer()
{
Debug.Log("Player - Binding to the Relay server using UTP.");
// Extract the Relay server data from the Join Allocation response.
var relayServerData = new RelayServerData(playerAllocation, "udp");
// Create NetworkSettings using the Relay server data.
var settings = new NetworkSettings();
settings.WithRelayParameters(ref relayServerData);
// Create the Player's NetworkDriver from the NetworkSettings object.
playerDriver = NetworkDriver.Create(settings);
// Bind to the Relay server.
if (playerDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
{
Debug.LogError("Player client failed to bind");
}
else
{
Debug.Log("Player client bound to Relay server");
}
}
绑定到 Relay 服务器并连接到主机玩家
以下代码片段中包含函数 OnConnectPlayer
,该函数展示了如何向主机玩家发送连接请求。由于调用的是 hostDriver.Accept()
,主机玩家的更新循环中显示为传入连接。
public void OnConnectPlayer()
{
Debug.Log("Player - Connecting to Host's client.");
// Sends a connection request to the Host Player.
clientConnection = playerDriver.Connect();
}
发送消息
主机玩家和加入玩家建立连接后,即可通过发送消息进行通信。
以主机玩家身份发送消息
以下代码片段中包含函数 OnHostSendMessage
,该函数展示了主机玩家如何向与其连接的加入玩家发送消息。在该示例中,主机玩家将消息广播到所有连接的加入玩家。
public void OnHostSendMessage()
{
if (serverConnections.Length == 0)
{
Debug.LogError("No players connected to send messages to.");
return;
}
// Get message from the input field, or default to the placeholder text.
var msg = !String.IsNullOrEmpty(HostMessageInput.text) ? HostMessageInput.text : HostMessageInput.placeholder.GetComponent<Text>().text;
// In this sample, we will simply broadcast a message to all connected clients.
for (int i = 0; i < serverConnections.Length; i++)
{
if (hostDriver.BeginSend(serverConnections[i], out var writer) == 0)
{
// Send the message. Aside from FixedString32, many different types can be used.
writer.WriteFixedString32(msg);
hostDriver.EndSend(writer);
}
}
}
以加入玩家身份发送消息
以下代码片段中包含函数 OnPlayerSendMessage
,该函数展示了加入玩家如何向主机玩家发送消息。
public void OnPlayerSendMessage()
{
if (!clientConnection.IsCreated)
{
Debug.LogError("Player isn't connected. No Host client to send message to.");
return;
}
// Get message from the input field, or default to the placeholder text.
var msg = !String.IsNullOrEmpty(PlayerMessageInput.text) ? PlayerMessageInput.text : PlayerMessageInput.placeholder.GetComponent<Text>().text;
if (playerDriver.BeginSend(clientConnection, out var writer) == 0)
{
// Send the message. Aside from FixedString32, many different types can be used.
writer.WriteFixedString32(msg);
playerDriver.EndSend(writer);
}
}
断开连接
加入玩家结束游戏后,可以断开与主机玩家的连接。主机玩家也可以强制断开与加入玩家的连接。
以主机玩家身份断开连接
以下代码片段中包含函数 OnDisconnectPlayers
,该函数展示了主机玩家如何强制断开与加入玩家的连接。在该示例中,主机玩家断开与所有加入玩家的连接。
public void OnDisconnectPlayers()
{
if (serverConnections.Length == 0)
{
Debug.LogError("No players connected to disconnect.");
return;
}
// In this sample, we will simply disconnect all connected clients.
for (int i = 0; i < serverConnections.Length; i++)
{
// This sends a disconnect event to the destination client,
// letting them know they're disconnected from the Host.
hostDriver.Disconnect(serverConnections[i]);
// Here, we set the destination client's NetworkConnection to the default value.
// It will be recognized in the Host's Update loop as a stale connection, and be removed.
serverConnections[i] = default(NetworkConnection);
}
}
以加入玩家身份断开连接
以下代码片段中包含函数 OnDisconnect
,该函数展示了加入玩家如何断开与主机玩家的连接。
public void OnDisconnect()
{
// This sends a disconnect event to the Host client,
// letting them know they're disconnecting.
playerDriver.Disconnect(clientConnection);
// We remove the reference to the current connection by overriding it.
clientConnection = default(NetworkConnection);
}