搭配 UTP 使用 Relay
Build multiplayer games using Relay with the Unity Transport Package for networking.
阅读时间11 分钟最后更新于 4 天前
Relay SDK 非常适合搭配 Unity Transport Package (UTP) 使用。UTP 是专为 Unity 游戏引擎构建的现代网络库,用于网络抽象化。借助 UTP,开发者可以专注于游戏本身,而非底层的协议和网络框架。UTP 与网络代码无关,这意味着它可以用于各种高层网络代码的抽象化,支持所有 Unity 网络代码解决方案,并可与其他网络代码库配合使用。
配置 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()public async void OnSignIn(){ await AuthenticationService.Instance.SignInAnonymouslyAsync(); playerId = AuthenticationService.Instance.PlayerId; Debug.Log($"Signed in. Player ID: {playerId}");}
主机玩家
以主机玩家身份启动的游戏客户端必须能够创建分配、请求加入代码、配置连接类型、创建用于绑定到 Relay 服务器的NetworkDriver主机玩家更新循环
开始执行分配流程的第一步之前,需要设置主机玩家的更新循环,用于处理传入的连接和消息。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; } } }}
创建分配
以下代码片段中包含函数OnAllocatepublic 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);}
绑定到 Relay server 并侦听连接
以下代码片段中包含函数OnBindHostpublic 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"); } }}
请求加入代码
以下代码片段中包含函数OnJoinCodepublic 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玩家更新循环
如同主机玩家一样,加入玩家在加入主机玩家的游戏会话之前,您也需要设置其更新循环,用于处理传入的消息。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; } }}
加入分配
以下代码片段中包含函数OnJoinpublic 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); }}
绑定到 Relay 服务器并连接到主机玩家
以下代码片段中包含函数OnBindPlayerpublic 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 服务器并连接到主机玩家
以下代码片段中包含函数OnConnectPlayerhostDriver.Accept()public void OnConnectPlayer(){ Debug.Log("Player - Connecting to Host's client."); // Sends a connection request to the Host Player. clientConnection = playerDriver.Connect();}
发送消息
主机玩家和加入玩家建立连接后,即可通过发送消息进行通信。以主机玩家身份发送消息
以下代码片段中包含函数OnHostSendMessagepublic 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); } }}
以加入玩家身份发送消息
以下代码片段中包含函数OnPlayerSendMessagepublic 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); }}
断开连接
加入玩家结束游戏后,可以断开与主机玩家的连接。主机玩家也可以强制断开与加入玩家的连接。以主机玩家身份断开连接
以下代码片段中包含函数OnDisconnectPlayerspublic 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); }}
以加入玩家身份断开连接
以下代码片段中包含函数OnDisconnectpublic 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);}