Unity Multiplayer Services — Lobby, Relay, Netcode 연동
Unity Gaming Services(UGS)는 별도 서버 없이 멀티플레이어를 구현할 수 있는 클라우드 서비스 묶음입니다. Lobby(방 목록/매칭), Relay(NAT 우회 P2P), Netcode for GameObjects(동기화)를 조합해 완전한 멀티플레이어 흐름을 만들 수 있습니다.
1. 패키지 설치
섹션 제목: “1. 패키지 설치”Package Manager:- com.unity.services.lobby- com.unity.services.relay- com.unity.netcode.gameobjects- com.unity.services.authentication2. UGS 초기화 및 인증
섹션 제목: “2. UGS 초기화 및 인증”using Unity.Services.Core;using Unity.Services.Authentication;using UnityEngine;
public class UGSInitializer : MonoBehaviour{ async void Start() { await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn) await AuthenticationService.Instance.SignInAnonymouslyAsync();
Debug.Log($"플레이어 ID: {AuthenticationService.Instance.PlayerId}"); }}3. Lobby — 방 생성 및 참가
섹션 제목: “3. Lobby — 방 생성 및 참가”using Unity.Services.Lobbies;using Unity.Services.Lobbies.Models;
public class LobbyManager : MonoBehaviour{ private Lobby _currentLobby;
// 방 생성 public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers) { var options = new CreateLobbyOptions { IsPrivate = false, Data = new Dictionary<string, DataObject> { ["map"] = new DataObject( DataObject.VisibilityOptions.Public, "Forest") } };
_currentLobby = await LobbyService.Instance .CreateLobbyAsync(lobbyName, maxPlayers, options);
// 호스트는 하트비트로 로비 유지 StartCoroutine(HeartbeatLobby(_currentLobby.Id)); return _currentLobby; }
// 방 목록 조회 및 참가 public async Task JoinPublicLobbyAsync() { var lobbies = await LobbyService.Instance.QueryLobbiesAsync();
if (lobbies.Results.Count == 0) return;
_currentLobby = await LobbyService.Instance .JoinLobbyByIdAsync(lobbies.Results[0].Id); }
IEnumerator HeartbeatLobby(string lobbyId) { while (true) { yield return new WaitForSeconds(15f); LobbyService.Instance.SendHeartbeatPingAsync(lobbyId); } }}4. Relay — NAT 우회 연결
섹션 제목: “4. Relay — NAT 우회 연결”using Unity.Services.Relay;using Unity.Services.Relay.Models;using Unity.Netcode;using Unity.Netcode.Transports.UTP;
public class RelayManager : MonoBehaviour{ // 호스트: Relay 할당 생성 public async Task<string> StartHostWithRelayAsync(int maxConnections = 4) { var allocation = await RelayService.Instance .CreateAllocationAsync(maxConnections);
string joinCode = await RelayService.Instance .GetJoinCodeAsync(allocation.AllocationId);
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>(); transport.SetHostRelayData( allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData);
NetworkManager.Singleton.StartHost(); return joinCode; }
// 클라이언트: Join Code로 연결 public async Task JoinWithRelayAsync(string joinCode) { var allocation = await RelayService.Instance .JoinAllocationAsync(joinCode);
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>(); transport.SetClientRelayData( allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, allocation.HostConnectionData);
NetworkManager.Singleton.StartClient(); }}5. Lobby + Relay 통합 흐름
섹션 제목: “5. Lobby + Relay 통합 흐름”public class MultiplayerManager : MonoBehaviour{ public async Task HostGameAsync() { // 1. Relay 할당 → Join Code 획득 string joinCode = await relayManager.StartHostWithRelayAsync();
// 2. Lobby에 Join Code 저장 var updateOptions = new UpdateLobbyOptions { Data = new Dictionary<string, DataObject> { ["joinCode"] = new DataObject( DataObject.VisibilityOptions.Member, joinCode) } }; await LobbyService.Instance.UpdateLobbyAsync( _currentLobby.Id, updateOptions); }
public async Task JoinGameAsync(string lobbyId) { // 1. Lobby 참가 var lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
// 2. Lobby에서 Join Code 추출 string joinCode = lobby.Data["joinCode"].Value;
// 3. Relay로 연결 await relayManager.JoinWithRelayAsync(joinCode); }}6. NetworkVariable로 상태 동기화
섹션 제목: “6. NetworkVariable로 상태 동기화”using Unity.Netcode;
public class PlayerHealth : NetworkBehaviour{ private NetworkVariable<int> _health = new(100, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
public override void OnNetworkSpawn() { _health.OnValueChanged += (oldVal, newVal) => UpdateHealthUI(newVal); }
[ServerRpc] public void TakeDamageServerRpc(int damage) { _health.Value = Mathf.Max(0, _health.Value - damage); }}7. Lobby 폴링 — 실시간 참가자 목록 갱신
섹션 제목: “7. Lobby 폴링 — 실시간 참가자 목록 갱신”public class LobbyPolling : MonoBehaviour{ [SerializeField] private float _pollInterval = 1.5f; private string _lobbyId; private Coroutine _pollingCoroutine;
public void StartPolling(string lobbyId) { _lobbyId = lobbyId; _pollingCoroutine ??= StartCoroutine(PollLoop()); }
public void StopPolling() { if (_pollingCoroutine != null) { StopCoroutine(_pollingCoroutine); _pollingCoroutine = null; } }
private IEnumerator PollLoop() { while (true) { yield return new WaitForSeconds(_pollInterval);
// Lobby 상태를 서버에서 가져옴 var task = LobbyService.Instance.GetLobbyAsync(_lobbyId); yield return new WaitUntil(() => task.IsCompleted);
if (!task.IsFaulted) OnLobbyUpdated(task.Result); } }
private void OnLobbyUpdated(Lobby lobby) { Debug.Log($"참가자: {lobby.Players.Count}/{lobby.MaxPlayers}"); foreach (var player in lobby.Players) Debug.Log($" - {player.Id}"); }}8. Connection Approval — 접속 검증
섹션 제목: “8. Connection Approval — 접속 검증”using Unity.Netcode;
public class ConnectionApprovalHandler : MonoBehaviour{ void Start() { NetworkManager.Singleton.ConnectionApprovalCallback = ApproveConnection; }
private void ApproveConnection( NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) { // 클라이언트가 보낸 페이로드 검증 string payload = System.Text.Encoding.UTF8 .GetString(request.Payload);
bool approved = payload == "SECRET_TOKEN";
response.Approved = approved; response.Reason = approved ? "" : "잘못된 토큰";
// 스폰 위치 지정 (승인된 경우) if (approved) { response.CreatePlayerObject = true; response.Position = GetSpawnPoint(); response.Rotation = Quaternion.identity; } }
private Vector3 GetSpawnPoint() => new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));}Lobby로 방을 관리하고 Relay로 NAT 우회 P2P 연결을 수립한 뒤 Netcode for GameObjects로 상태를 동기화하는 것이 UGS 멀티플레이어의 표준 흐름입니다. Join Code를 Lobby 데이터에 저장해 두 서비스를 연결하는 패턴이 핵심입니다.