UTP와 함께 Relay 사용
Build multiplayer games using Relay with the Unity Transport Package for networking.
읽는 시간 5분최근 업데이트: 5일 전
Relay SDK는 UTP(Unity Transport 패키지)를 지원합니다. UTP는 네트워킹을 추상화하기 위해 Unity 게임 엔진용으로 구축된 최신 네트워킹 라이브러리입니다. UTP를 사용하면 개발자가 세부적인 프로토콜과 네트워킹 프레임워크가 아닌 게임 자체에 더욱 집중할 수 있습니다. UTP는 넷코드에 구애받지 않으므로 다양한 하이레벨 네트워킹 코드 추상화와 함께 사용할 수 있고, 모든 Unity 넷코드 솔루션을 지원하며, 다른 넷코드 라이브러리와도 연동됩니다.
Relay SDK 설정
Relay를 사용하기 전에 Unity 프로젝트를 Unity용으로 설정해야 합니다. Unity용으로 프로젝트를 설정하려면 Relay 시작하기를 참조하십시오. Relay 서비스를 활성화한 다음, Unity 에디터를 통해 Unity 프로젝트 ID를 연결합니다. Simple Relay Sample (using UTP)을 확인하여 Relay와 UTP를 함께 사용하는 기본적인 방법을 알아보십시오.Simple Relay Sample (using UTP)
이 샘플은 Simple Relay 샘플을 확장한 것으로, Relay와 UTP를 함께 사용하여 멀티플레이어 게임을 구축하는 과정의 기본 사항을 보여 줍니다. 패키지 관리자에서 Relay 패키지의 샘플을 임포트하고, 샘플에 포함된 스크립트를 따르십시오. 이 샘플은 두 개 이상의 개별 클라이언트에서 작동합니다. 하나는 호스트 플레이어 클라이언트이고, 그 외 하나 이상의 클라이언트는 참여 플레이어 클라이언트입니다. 샘플 프로젝트 코드를 테스트하는 경우 동일한 머신에서 여러 클라이언트를 실행해야 할 수 있습니다. 하나의 머신에서 여러 클라이언트를 실행하려면 프로젝트를 실행 가능한 바이너리로 빌드하거나 동일한 프로젝트의 Unity 에디터 인스턴스를 여러 개 실행할 수 있는 멀티플레이어 플레이 모드 또는 ParrelSync 등의 솔루션을 사용하면 됩니다. 프로젝트를 바이너리로 빌드한 경우, 바이너리를 열린 Unity 에디터나 바이너리의 클론과 함께 사용하면 됩니다. 씬을 플레이할 때, 호스트 플레이어(게임 세션을 시작한 플레이어)나 참여 플레이어로 클라이언트를 시작할 수 있습니다. 호스트 플레이어 한 명은 반드시 있어야 합니다. 참여 플레이어가 게임을 실행하려면 호스트 플레이어가 필요합니다. 호스트 플레이어의 플로는 다음과 같습니다.- 로그인
- 지역 가져오기(선택사항)
- 지역 선택(선택사항)
- 게임 세션 할당
- 선택한 Relay 서버에 바인드
- 참여 코드 가져오기
- 모든 연결된 플레이어에게 메시지 전송
- 모든 연결된 플레이어의 연결 해제
- 로그인
- 호스트 플레이어의 참여 코드를 사용하여 게임 세션 참여
- 선택한 Relay 서버에 바인드
- 호스트 플레이어에게 연결 요청
- 호스트 플레이어에게 메시지 전송
- 호스트 플레이어에게서 연결 해제
Simple Relay Sample (using UTP) 프로젝트 임포트
- Unity 에디터(버전 2020.3)에서 Relay 프로젝트를 엽니다. Relay 프로젝트가 없다면 Relay 프로젝트 설정을 참조하십시오.
- Package Manager를 열고 Relay 패키지로 이동합니다.
- Samples 섹션을 펼칩니다.
- Import를 선택하여 Simple Relay Sample (using UTP) 프로젝트를 임포트합니다.
- Simple Relay 샘플 프로젝트를 임포트하고 나면 이 프로젝트를 씬으로 열 수 있습니다. 샘플 프로젝트는 현재 프로젝트의 Assets/Samples/Relay/1.0.4/Simple Relay Sample (using UTP)에 있습니다.
- File > Open Scene을 선택합니다.
- Simple Relay Sample (using 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}");}
호스트 플레이어
게임 클라이언트가 호스트 플레이어로 작동하는 경우, 게임 클라이언트는 할당을 생성하고, 참여 코드를 요청하며, 연결 유형을 설정하고,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 서버에 바인드하고 연결 수신
다음 코드 스니핏은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); }}
참여 플레이어
게임 클라이언트가 참여 플레이어로 작동하는 경우, 게임 클라이언트는 할당에 참여하고, 연결 유형을 설정하며,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);}