실제 인터넷 환경에서는 NAT(Network Address Translation, 네트워크 주소 변환) 와 방화벽이 직접 연결을 막기 때문에 중간에서 트래픽을 중계해 주는 서버가 필요.
Unity는 이를 해결하기 위한 Relay 서비스를 UGS의 일부로 제공
Host가 Relay 서버에 자리를 잡고 Join Code를 발급받으면, Client는 그 Join Code로 같은 자리에 참가할 수 있음.
사전 개념
| 키워드 | 역할 |
|---|---|
| async | "이 메서드 안에서 await를 사용하겠다" 는 선언 |
| await | "이 작업이 끝날 때까지 기다렸다가 다음 줄로 넘어간다" |
| Task | 비동기 작업 자체를 나타내는 타입. 반환값이 있으면 Task |
| try-catch | 실행 중 발생하는 에러를 잡아서 처리하는 구문 (예외 발생 시 비용이 큼) |
try-catchtry-catch 로 감싸는 습관을 들이는 것이 좋음Relay 쓰는 구조에서는 Auth(인증) → Relay(방 생성 or 참가) → Network 시작 순서 필수로 지켜야 함.
using System;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
using UnityEngine;
public class AuthService : MonoBehaviour
{
public static AuthService Instance { get; private set; }
private async void Awake()
{
SetSingleton();
await InitializeAsync();
}
private void SetSingleton()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
// IEnumerator
public async Task InitializeAsync()
{
try
{ // yield return
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
Debug.Log($"[Auth] 로그인 완료: {AuthenticationService.Instance.PlayerId}");
}
catch (Exception e)
{
Debug.LogError($"[Auth] 초기화 실패: {e.Message}");
throw;
}
}
}
Join Code 를 발급받고, Client는 그 Join Code 로 같은 Allocation에 참가using System;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Networking.Transport.Relay;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
public class RelayNetworkService : MonoBehaviour
{
public static RelayNetworkService Instance { get; private set; }
private void Awake() => SetSingleton();
// maxConnections에는 Host 자신을 포함하지 않음. 4인 게임이면 3을 전달
public async Task<string> StartHostWithRelayAsync(int maxConnections = 3)
{
try
{
// Relay 서버에 공간 할당
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections);
// 다른 플레이어가 접속할 Join Code 생성
string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
// UnityTransport 에 Relay 서버 정보 주입
RelayServerData serverData = AllocationUtils.ToRelayServerData(allocation, "dtls");
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(serverData);
// Host 시작
NetworkManager.Singleton.StartHost();
return joinCode;
}
catch (Exception e)
{
Debug.LogError($"[Relay] Host 시작 실패: {e.Message}");
throw;
}
}
public async Task StartClientWithRelayAsync(string joinCode)
{
try
{
// Join Code 로 Allocation 참가
JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
// UnityTransport 에 Relay 서버 정보 주입
RelayServerData serverData = AllocationUtils.ToRelayServerData(joinAllocation, "dtls");
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(serverData);
// Client 시작
NetworkManager.Singleton.StartClient();
}
catch (Exception e)
{
Debug.LogError($"[Relay] Client 접속 실패: {e.Message}");
throw;
}
}
private void SetSingleton()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
SetRelayServerData → StartHost / StartClient 순서 중요connectionType("dtls") 을 사용해야 함using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unity.Netcode;
public class NetworkBootstrap : MonoBehaviour
{
[SerializeField] private Button _startHostButton;
[SerializeField] private Button _startClientButton;
[SerializeField] private Button _disconnectButton;
// Join Code 를 표시 · 입력하는 공용 InputField
[SerializeField] private TMP_InputField _joinCodeInputField;
// Join Code 를 클립보드에 복사하는 버튼
[SerializeField] private Button _copyButton;
private bool _isCallbacksBound;
private void OnEnable()
{
BindNetworkCallbacks();
BindButtonEvents();
}
private void OnDisable()
{
UnbindNetworkCallbacks();
UnbindButtonEvents();
}
// Copy 버튼 이벤트 바인딩 추가, Host/Client 핸들러가 async 버전으로 변경됨
private void BindButtonEvents()
{
_startHostButton.onClick.AddListener(OnStartHostClicked);
_startClientButton.onClick.AddListener(OnStartClientClicked);
_disconnectButton.onClick.AddListener(OnDisconnectClicked);
_copyButton.onClick.AddListener(OnCopyClicked);
}
// Copy 버튼 이벤트 해제 추가
private void UnbindButtonEvents()
{
_startHostButton.onClick.RemoveListener(OnStartHostClicked);
_startClientButton.onClick.RemoveListener(OnStartClientClicked);
_disconnectButton.onClick.RemoveListener(OnDisconnectClicked);
_copyButton.onClick.RemoveListener(OnCopyClicked);
}
private void BindNetworkCallbacks()
{
if (_isCallbacksBound) return;
if (NetworkManager.Singleton == null) return;
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
_isCallbacksBound = true;
}
private void UnbindNetworkCallbacks()
{
if (!_isCallbacksBound) return;
if (NetworkManager.Singleton == null) return;
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect;
NetworkManager.Singleton.OnServerStarted -= OnServerStarted;
_isCallbacksBound = false;
}
// Relay 연동으로 변경: StartHost 직접 호출 대신 Relay Allocation 생성 후 Join Code 를 InputField 에 표시
private async void OnStartHostClicked()
{
try
{
string joinCode = await RelayNetworkService.Instance.StartHostWithRelayAsync();
_joinCodeInputField.text = joinCode;
}
catch (Exception e)
{
Debug.LogError($"[Bootstrap] Host 시작 오류: {e.Message}");
}
}
// Relay 연동으로 변경: InputField 의 Join Code 를 읽어 Relay 에 접속
private async void OnStartClientClicked()
{
string joinCode = _joinCodeInputField.text.Trim();
if (string.IsNullOrEmpty(joinCode)) return;
try
{
await RelayNetworkService.Instance.StartClientWithRelayAsync(joinCode);
}
catch (Exception e)
{
Debug.LogError($"[Bootstrap] Client 접속 오류: {e.Message}");
}
}
private void OnDisconnectClicked() => NetworkManager.Singleton.Shutdown();
// 추가: Join Code 를 클립보드에 복사
private void OnCopyClicked()
{
GUIUtility.systemCopyBuffer = _joinCodeInputField.text;
}
private void OnClientConnected(ulong clientId) => Debug.Log($"<color=green>[Network] 접속: {clientId}</color>");
private void OnClientDisconnect(ulong clientId) => Debug.Log($"<color=red>[Network] 해제: {clientId}</color>");
private void OnServerStarted() => Debug.Log("<color=green>[Network] 서버 시작</color>");
}