-
유니티 게이밍 서비스 로비 마스터하기 / Master Unity Gaming Services Lobby UGS유니티 2024. 12. 28. 02:33
유니티 게이밍 서비스 로비(UGS Lobby)란?
유니티 게이밍 서비스 중 플레이어를 관리하고 매칭시켜주는 로비 서버입니다.
유니티의 서버를 사용하기에 월 무료구간 이후 과금제로 운영됩니다.
https://unity.com/kr/products/gaming-services/pricing
에서 가격을 확인할 수 있습니다.
https://docs.unity.com/ugs/ko-kr/manual/lobby/manual/unity-lobby-service
에서 공식문서를 확인할 수 있습니다.
로비의 특징으로는
- 로비의 생성, 참여, 수정, 삭제를 간편하게 할 수 있습니다.
- 로비 목록을 필터링을 걸어 확인할 수 있습니다.
- 로비와 플레이어의 데이터를 편집할 수 있습니다.
- 비밀번호로 참여, 참여코드로 참여, 빠른 참여가 가능합니다.
설치
Window - Package Manager - Multiplayer Services를 설치합니다.
구 버전에는 Lobby를 따로 설치했으나 인증, 로비, 릴레이 등 UGS를 통합한 멀티 서비스입니다.
Edit - Project Settings... - Services에서 유니티 클라우드와 프로젝트를 연결합니다.
연결이 되었으면 Dashboard를 눌러 유니티 클라우드로 이동합니다.
제품에 Lobby를 검색해 실행합니다.
관리 - 서비스 사용량에서 사용량을 알 수 있습니다.
로비 매니저 (LobbyManager)
사용하기 편리하도록 로비 관련 함수들을 담은 래핑 클래스로 만들었습니다.
LobbyManager.cs
using System; using UnityEngine; using System.Collections.Generic; using System.Collections.Concurrent; using Unity.Services.Authentication; using Unity.Services.Core; using Unity.Services.Lobbies; using Unity.Services.Lobbies.Models; namespace Unity.Services.Lobbies.Models { // User Custom enums public enum EPlayerOptionKey { PlayerName } public enum ELobbyOptionKey { GameMode, Map } } public class LobbyManager : MonoBehaviour { public string CurPlayerId { get => curPlayerId; } public Lobby CurLobby { get => curLobby; } string curPlayerId; Lobby curLobby; Player optionPlayer; float heartbeatTimer; float heartbeatTimerMax = 15; ConcurrentQueue<string> createdLobbyIds = new(); LobbyEventCallbacks lobbyEvents = new(); public async Awaitable<(bool success, string playerId)> SignIn() { try { await UnityServices.InitializeAsync(); if (!AuthenticationService.Instance.IsSignedIn) { await AuthenticationService.Instance.SignInAnonymouslyAsync(); } curPlayerId = AuthenticationService.Instance.PlayerId; return (true, curPlayerId); } catch (Exception e) { Debug.Log($"SignIn failed. {e}"); return (false, string.Empty); } } /// <summary> Sign in and initialize required. </summary> public void SetOptionCurPlayer(Dictionary<EPlayerOptionKey, string> playerDic) { Dictionary<string, PlayerDataObject> optionPlayerDic = new(); foreach (var (key, value) in playerDic) { optionPlayerDic[key.ToString()] = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, value); } optionPlayer = new Player(id: curPlayerId, data: optionPlayerDic); } // <summary> isPublic is true, password is 8 ~ 64 characters. </summary> public async Awaitable<(bool success, Lobby lobby)> CreateLobby(string lobbyName, int maxPlayers, bool isPublic, string password, Dictionary<ELobbyOptionKey, string> dataDic) { try { CreateLobbyOptions options = new() { IsPrivate = !isPublic, Player = optionPlayer, Data = GetLobbyOptionDic(dataDic) }; if (!isPublic && !string.IsNullOrEmpty(password) && password.Length >= 8) { options.Password = password; } curLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, options); await SubscribeLobbyEvent(curLobby.Id); createdLobbyIds.Enqueue(curLobby.Id); return (true, curLobby); } catch (Exception e) { Debug.Log($"CreateLobby failed. {e}"); return (false, null); } } public async Awaitable<(bool success, Lobby lobby)> JoinLobbyById(string lobbyId, string password) { try { JoinLobbyByIdOptions options = new() { Player = optionPlayer, }; if (!string.IsNullOrEmpty(password) && password.Length >= 8) { options.Password = password; } curLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, options); await SubscribeLobbyEvent(curLobby.Id); return (true, curLobby); } catch (Exception e) { Debug.Log($"JoinLobbyById failed. {e}"); return (false, null); } } public async Awaitable<(bool success, Lobby lobby)> JoinLobbyByCode(string lobbyCode, string password) { try { JoinLobbyByCodeOptions options = new() { Player = optionPlayer }; if (!string.IsNullOrEmpty(password) && password.Length >= 8) { options.Password = password; } curLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, options); await SubscribeLobbyEvent(curLobby.Id); return (true, curLobby); } catch (Exception e) { Debug.Log($"JoinLobbyByCode failed. {e}"); return (false, null); } } public async Awaitable<(bool success, Lobby lobby)> QuickJoinLobby(params string[] filters) { try { QuickJoinLobbyOptions options = new() { Player = optionPlayer, Filter = GetQueryFilters(filters) }; curLobby = await LobbyService.Instance.QuickJoinLobbyAsync(options); await SubscribeLobbyEvent(curLobby.Id); return (true, curLobby); } catch (Exception e) { Debug.Log($"QuickJoinLobby failed. {e}"); return (false, null); } } public async Awaitable<(bool success, List<Lobby> lobbies)> GetListLobbies(int skip, int count, params string[] filters) { try { QueryLobbiesOptions options = new() { Skip = skip, Count = count, Filters = GetQueryFilters(filters), Order = new List<QueryOrder> { new QueryOrder(false, QueryOrder.FieldOptions.Created) } }; QueryResponse queryResponse = await LobbyService.Instance.QueryLobbiesAsync(options); List<Lobby> lobbies = queryResponse.Results; return (true, lobbies); } catch (Exception e) { Debug.Log($"GetListLobbies failed. {e}"); return (false, null); } } public async Awaitable<(bool success, Lobby lobby)> UpdateLobby(string lobbyName, int maxPlayers, bool isPublic, string password, Dictionary<ELobbyOptionKey, string> dataDic) { try { UpdateLobbyOptions options = new() { Name = lobbyName, MaxPlayers = maxPlayers, IsPrivate = !isPublic, Data = GetLobbyOptionDic(dataDic) }; if (!isPublic && !string.IsNullOrEmpty(password) && password.Length >= 8) { options.Password = password; } curLobby = await LobbyService.Instance.UpdateLobbyAsync(curLobby.Id, options); return (true, curLobby); } catch (Exception e) { Debug.Log($"UpdateLobby failed. {e}"); return (false, null); } } public async Awaitable<(bool success, Lobby lobby)> UpdatePlayer(string playerId, Dictionary<EPlayerOptionKey, string> playerDic) { try { UpdatePlayerOptions options = new() { Data = GetPlayerOptionDic(playerDic) }; SetOptionCurPlayer(playerDic); curLobby = await LobbyService.Instance.UpdatePlayerAsync(curLobby.Id, playerId, options); return (true, curLobby); } catch (Exception e) { Debug.Log($"UpdatePlayer failed. {e}"); return (false, null); } } public async Awaitable<bool> DeleteLobby(string lobbyId) { try { await LobbyService.Instance.DeleteLobbyAsync(lobbyId); curLobby = null; lobbyEvents = new LobbyEventCallbacks(); return true; } catch (Exception e) { Debug.Log($"DeleteLobby failed. {e}"); return false; } } public async Awaitable<bool> KickPlayer(string playerId) { try { await LobbyService.Instance.RemovePlayerAsync(curLobby.Id, playerId); return true; } catch (Exception e) { Debug.Log($"KickPlayer failed. {e}"); return false; } } public async Awaitable<(bool success, Lobby lobby)> GetLobby(string lobbyId) { try { Lobby lobby = await LobbyService.Instance.GetLobbyAsync(lobbyId); return (true, lobby); } catch (Exception e) { Debug.Log($"GetLobby failed. {e}"); return (false, null); } } public async Awaitable<(bool success, List<string> lobbyIds)> GetJoinedLobbyIds() { try { List<string> lobbyIds = await LobbyService.Instance.GetJoinedLobbiesAsync(); return (true, lobbyIds); } catch (Exception e) { Debug.Log($"GetJoinedLobbyIds failed. {e}"); return (false, null); } } public async Awaitable<bool> ReconnectLobby() { try { await LobbyService.Instance.ReconnectToLobbyAsync(curLobby.Id); return true; } catch (Exception e) { Debug.Log($"ReconnectLobby failed. {e}"); return false; } } public void PrintLobbyInfo() { if (curLobby == null) { Debug.Log($"curLobby is null"); return; } Debug.Log("------Lobby detault------"); Debug.Log($"LobbyId: {curLobby.Id}"); Debug.Log($"LobbyName: {curLobby.Name}"); Debug.Log($"LobbyCode: {curLobby.LobbyCode}"); Debug.Log($"LobbyPlayerCount: {curLobby.Players.Count}"); Debug.Log($"LobbyMaxPlayers: {curLobby.MaxPlayers}"); Debug.Log($"LobbyIsPrivate: {curLobby.IsPrivate}"); Debug.Log($"LobbyHasPassword: {curLobby.HasPassword}"); Debug.Log($"LobbyIsLocked: {curLobby.IsLocked}"); Debug.Log($"LobbyHostId: {curLobby.HostId}"); Debug.Log("------Lobby data------"); foreach (var (key, dataObject) in curLobby.Data) { Debug.Log($"{key}: {dataObject.Value}"); } Debug.Log("------Lobby player data------"); foreach (Player player in curLobby.Players) { Debug.Log($"PlayerId: {player.Id}"); foreach (var (key, playerDataObject) in player.Data) { Debug.Log($"{key}: {playerDataObject.Value}"); } } } Dictionary<string, DataObject> GetLobbyOptionDic(Dictionary<ELobbyOptionKey, string> dataDic) { Dictionary<string, DataObject> resultDic = new(); foreach (var (key, value) in dataDic) { resultDic[key.ToString()] = new DataObject(DataObject.VisibilityOptions.Public, value); } return resultDic; } Dictionary<string, PlayerDataObject> GetPlayerOptionDic(Dictionary<EPlayerOptionKey, string> playerDic) { Dictionary<string, PlayerDataObject> resultDic = new(); foreach (var (key, value) in playerDic) { resultDic[key.ToString()] = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Public, value); } return resultDic; } List<QueryFilter> GetQueryFilters(params string[] filters) { List<QueryFilter> resultFilters = new() { new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT) }; if (filters != null && filters.Length > 0) { for (int i = 0; i < filters.Length; i++) { resultFilters.Add(new QueryFilter(QueryFilter.FieldOptions.S1, filters[i], QueryFilter.OpOptions.EQ)); } } return resultFilters; } async void HandleLobbyHeartbeat() { if (curLobby != null && curLobby.HostId == curPlayerId) { heartbeatTimer -= Time.deltaTime; if (heartbeatTimer < 0) { heartbeatTimer = heartbeatTimerMax; await LobbyService.Instance.SendHeartbeatPingAsync(curLobby.Id); } } } async void HandleCreatedLobbyDelete() { while (createdLobbyIds.TryDequeue(out string lobbyId)) { await DeleteLobby(lobbyId); } } void OnKickedFromLobby() { Debug.Log($"Kicked from lobby"); curLobby = null; lobbyEvents = new LobbyEventCallbacks(); } void OnLobbyChanged(ILobbyChanges changes) { if (changes.LobbyDeleted) { Debug.Log($"Lobby deleted"); curLobby = null; lobbyEvents = new LobbyEventCallbacks(); } else { Debug.Log($"Lobby changed"); changes.ApplyToLobby(curLobby); } } void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState state) { Debug.Log($"LobbyEventConnectionStateChanged: {state}"); } async Awaitable<bool> SubscribeLobbyEvent(string lobbyId) { try { lobbyEvents.LobbyChanged += OnLobbyChanged; lobbyEvents.KickedFromLobby += OnKickedFromLobby; lobbyEvents.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged; await LobbyService.Instance.SubscribeToLobbyEventsAsync(lobbyId, lobbyEvents); Debug.Log($"SubscribeLobbyEvent success"); return true; } catch (Exception e) { Debug.Log($"SubscribeLobbyEvent failed. {e}"); return false; } } void Update() { HandleLobbyHeartbeat(); } void OnApplicationQuit() { lobbyEvents = null; HandleCreatedLobbyDelete(); } }
맨 위에 User Custom enums가 있는데 이는 사용자가 플레이어와 로비에 커스텀 데이터를 정하는 키 값입니다.
편집해 쓰시면 됩니다.
EPlayerOptionKey로는 PlayerName이 있어 플레이어의 이름을 담습니다.
ELobbyOptionKey로는 GameMode (Easy, Normal, Hard), Map (Pasture, Desert) 같이
여러분이 키와 값을 설계하는 부분입니다.프로퍼티
- string CurPlayerId: 로그인 된 아이디입니다.
- Lobby CurLobby: 참여하고 있는 로비입니다.
함수
- Awaitable<(bool success, string playerId)> SignIn()
다른 로그인 인증을 사용해도 되지만 간단히 익명로그인으로 로그인을 합니다. - void SetOptionCurPlayer(Dictionary<EPlayerOptionKey, string> playerDic)
로그인 후 바로 호출해야 하며 플레이어 데이터 커스텀 딕셔너리로 아이디와 함께 플레이어를 초기화합니다. - Awaitable<(bool success, Lobby lobby)> CreateLobby(string lobbyName,
int maxPlayers, bool isPublic, string password, Dictionary<ELobbyOptionKey, string> dataDic)
로비를 생성합니다.
로비이름, 최대 플레이어 수, 보이는지 (false라면 로비 리스트에 보이지 않습니다),
비밀번호 (8~64의 문자), 로비 데이터 커스텀 딕셔너리 매개변수에 따라 생성합니다.
비밀로비를 만들때는 비밀번호가 8자 미만이면 비밀번호가 안들어갑니다. - Awaitable<(bool success, Lobby lobby)> JoinLobbyById(string lobbyId, string password)
로비 아이디, (비밀번호가 있는경우 비밀번호도 포함)로 참여합니다. - Awaitable<(bool success, Lobby lobby)> JoinLobbyByCode(string lobbyCode, string password)
로비 코드 (짧음), (비밀번호가 있는경우 비밀번호도 포함)로 참여합니다. - Awaitable<(bool success, Lobby lobby)> QuickJoinLobby(params string[] filters)
로비 생성 순으로 (필터가 있는경우 필터 포함) 빠른 참여를 합니다. - Awaitable<(bool success, List<Lobby> lobbies)> GetListLobbies(int skip, int count, params string[] filters)
skip 인덱스부터 count개수만큼 (필터가 있는경우 필터 포함)하여 로비리스트를 가져옵니다.
로비에 이미 들어와 있는 경우에는 리스트를 볼 수 없습니다. - Awaitable<(bool success, Lobby lobby)> UpdateLobby(string lobbyName,
int maxPlayers, bool isPublic, string password, Dictionary<ELobbyOptionKey, string> dataDic)
로비 호스트는 로비의 설정을 변경할 수 있습니다.
로비 이름, 최대 플레이어 수, 보이는지, 비밀번호, 로비 데이터 커스텀 딕셔너리를 바꿉니다. - Awaitable<(bool success, Lobby lobby)> UpdatePlayer(string playerId,
Dictionary<EPlayerOptionKey, string> playerDic)
플레이어 데이터 커스텀 딕셔너리를 바꿉니다. - Awaitable<bool> DeleteLobby(string lobbyId)
로비 호스트는 로비를 제거하고 자신을 포함 모두 로비에서 탈퇴시킵니다. - Awaitable<bool> KickPlayer(string playerId)
로비 호스트는 플레이어 아이디로 플레이어를 탈퇴시킵니다.
자신을 선택하면 다른 사람이 호스트가 되며 자신은 탈퇴됩니다. - Awaitable<(bool success, Lobby lobby)> GetLobby(string lobbyId)
로비 아이디로 로비를 얻어 올 수 있습니다. - Awaitable<(bool success, List<string> lobbyIds)> GetJoinedLobbyIds()
참여하고 있는 로비 아이디들을 얻어옵니다. 기본적으로 하나입니다. - Awaitable<bool> ReconnectLobby()
연결이 잠깐 끊어진 경우 마지막 접속한 로비로 재연결을 시도합니다. - void PrintLobbyInfo()
현재 로비의 자세한 정보와 로비 커스텀 데이터, 플레이어 커스텀 데이터들을 한눈에 봅니다.
Test.cs
using System.Collections.Generic; using UnityEngine; using Unity.Services.Lobbies.Models; using QuickCmd; public class Test : MonoBehaviour { [SerializeField] LobbyManager lobbyManager; [Command] async void SignIn(string playerName) { (bool success, string playerId) = await lobbyManager.SignIn(); if (success) { Dictionary<EPlayerOptionKey, string> playerDic = new() { { EPlayerOptionKey.PlayerName, playerName }, }; lobbyManager.SetOptionCurPlayer(playerDic); Debug.Log($"SignIn success! PlayerId:{playerId}, PlayerName:{playerName}"); } } [Command] async void CreateLobby(string lobbyName, int maxPlayers, bool isPublic, string password, string gameMode, string map) { Dictionary<ELobbyOptionKey, string> dataDic = new() { { ELobbyOptionKey.GameMode, gameMode }, { ELobbyOptionKey.Map, map }, }; (bool success, Lobby lobby) = await lobbyManager.CreateLobby(lobbyName, maxPlayers, isPublic, password, dataDic); if (success) { Debug.Log($"Lobby created! Name:{lobby.Name}, LobbyCode:{lobby.LobbyCode}"); } } [Command] void FastCreatePublicLobby() { CreateLobby("TestLobby", 4, true, string.Empty, "Easy", "Pasture"); } [Command] async void JoinLobbyById(string lobbyId, string password) { (bool success, Lobby lobby) = await lobbyManager.JoinLobbyById(lobbyId, password); if (success) { Debug.Log($"Lobby joined! Name:{lobby.Name}"); } } [Command] async void JoinLobbyByCode(string lobbyCode, string password) { (bool success, Lobby lobby) = await lobbyManager.JoinLobbyByCode(lobbyCode, password); if (success) { Debug.Log($"Lobby joined! Name:{lobby.Name}"); } } [Command] async void QuickJoinLobby() { (bool success, Lobby lobby) = await lobbyManager.QuickJoinLobby(); if (success) { Debug.Log($"Lobby joined! Name:{lobby.Name}"); } } [Command] async void GetListLobbies(int skip, int count) { (bool success, List<Lobby> lobbies) = await lobbyManager.GetListLobbies(skip, count); if (success) { Debug.Log($"Lobbies:{lobbies.Count}"); } } [Command] async void UpdateLobby(string lobbyName, int maxPlayers, bool isPublic, string password, string gameMode, string map) { Dictionary<ELobbyOptionKey, string> dataDic = new() { { ELobbyOptionKey.GameMode, gameMode }, { ELobbyOptionKey.Map, map }, }; (bool success, Lobby lobby) = await lobbyManager.UpdateLobby(lobbyName, maxPlayers, isPublic, password, dataDic); if (success) { lobbyManager.PrintLobbyInfo(); } } [Command] async void UpdatePlayer(string playerId, string playerName) { Dictionary<EPlayerOptionKey, string> playerDic = new() { { EPlayerOptionKey.PlayerName, playerName }, }; (bool success, Lobby lobby) = await lobbyManager.UpdatePlayer(playerId, playerDic); if (success) { lobbyManager.PrintLobbyInfo(); } } [Command] async void DeleteLobby() { bool success = await lobbyManager.DeleteLobby(lobbyManager.CurLobby.Id); if (success) { Debug.Log($"Lobby deleted!"); } } [Command] async void DeleteLobbyWithId(string lobbyId) { bool success = await lobbyManager.DeleteLobby(lobbyId); if (success) { Debug.Log($"Lobby deleted!"); } } [Command] async void KickPlayer(string playerId) { bool success = await lobbyManager.KickPlayer(playerId); if (success) { Debug.Log($"Player kicked! {playerId}"); } } [Command] async void GetLobby(string lobbyId) { (bool success, Lobby lobby) = await lobbyManager.GetLobby(lobbyId); if (success) { lobbyManager.PrintLobbyInfo(); } } [Command] async void GetJoinedLobbyIds() { (bool success, List<string> lobbyIds) = await lobbyManager.GetJoinedLobbyIds(); if (success) { Debug.Log($"GetJoinedLobbyIds count:{lobbyIds.Count}"); foreach (var lobbyId in lobbyIds) { Debug.Log($"LobbyId:{lobbyId}"); } } } [Command] async void ReconnectLobby() { bool success = await lobbyManager.ReconnectLobby(); if (success) { Debug.Log($"Lobby reconnected! {lobbyManager.CurLobby.Id}"); } } [Command] void PrintLobbyInfo() { lobbyManager.PrintLobbyInfo(); } }
QuickCommand (https://goranitv.tistory.com/33) 에셋으로 명령어 처리를 할 수 있습니다.
테스트 코드위에 [Command] 어트리뷰트를 넣었습니다.
'유니티' 카테고리의 다른 글