내일배움캠프/프로젝트

팀 프로젝트 마무리 정리 - 로비

서보훈 2025. 1. 14. 21:00

팀 프로젝트 종료 기한이 다가옴에 따라 최종적으로 작성한 코드들을 정리하는 시간을 가져보고자 합니다.

 

가장 먼저 멀티플레이를 구현하면서 만들어준 로비 관련 시스템 입니다.

 

먼저 로비 시스템의 구조는 이러합니다

 

UGS 인증 (Authenticate) - 로비 생성 or 참가 - 로비 내 준비 완료 - 릴레이를 통한 포트 연결

 

이후 NetworkManager를 통해 클라이언트와 호스트가 연결되고, 호스트의 씬 변경 요청을 통해 게임씬으로 넘어가게 됩니다.

 


UGS 인증 - NetworkAuthenticate 클래스

UGS 에 익명으로 로그인한 뒤, Vivox 에도 익명 로그인을 실행하는 클래스입니다.

public class NetworkAuthenticate : MonoSingleton<NetworkAuthenticate>
{
    //로그인 성공시 호출
    public event Action OnAuthenticateSuccessEvent;

    private readonly string loginText = "Connecting";

    protected async override void Awake()
    {
        //싱글톤 관련 생략...

        await UnityServices.InitializeAsync();
    }

    //인증 메서드 - UGS에 연결
    public async void Authenticate(string playerName)
    {
        LoadingSceneController.Instance.ActiveNetworkLoadingUI(loginText);

        try
        {
            AuthenticationService.Instance.SwitchProfile(playerName);
            //인증 시도
            await AuthenticationService.Instance.SignInAnonymouslyAsync();

            await AuthenticationService.Instance.UpdatePlayerNameAsync(playerName);
        }
        catch (Exception ex)
        {
            Debug.Log(ex);
            Debug.Log("UGS 연결 실패");
            LoadingSceneController.Instance.CloseNetworkLoadingUI();
            return;
        }

        try
        {
            await VivoxAuthenticate();
        }
        catch (Exception ex)
        {
            Debug.Log(ex);
            Debug.Log("Vivox 로그인 실패");
            LoadingSceneController.Instance.CloseNetworkLoadingUI();
            return;
        }

        OnAuthenticateSuccessEvent?.Invoke();
    }

    public async Task VivoxAuthenticate()
    {
        await VivoxService.Instance.InitializeAsync();

        LoginOptions options = new LoginOptions();

        options.DisplayName = AuthenticationService.Instance.PlayerName;

        await VivoxService.Instance.LoginAsync(options);
    }
}

 

 

인증을 하기 위해 UnityService 를 초기화하는 과정을 이 클래스 Awake 문에서 진행하며

확인 버튼에 Authenticate 메서드를 연동하여 메서드를 호축하게 됩니다.

public void LoginButton()
{
    PlayButtonSFX();
    NetworkAuthenticate.Instance.Authenticate(nameInput.text);
}

AuthenticateService 를 통해 익명 인증이 완료되면 바로 다음 작업으로 Vivox 에 로그인을 시도하며,

Vivox 익명 로그인을 성공하면 로비를 찾는 UI 로 넘어가게 됩니다.

 


로비 관리 - LobbyManager

로비와 관련된 모든 작업을 하는 매니저 클래스 입니다.

UGS 로비 특성상 25초마다 하트비트 신호를 보내도록 하였으며, 1초 마다 로비 정보를 업데이트 하도록 만들어주었습니다.

더보기

로비 전체 코드 입니다.

public class LobbyManager : MonoSingleton<LobbyManager>
{
    //UI관리용 이벤트
    //로비에 참여하면 호출
    public event Action<Lobby> OnJoinLobbyEvent;
    public event Action OnJoinSuccessEvent;
    //로비에서 나가면 호출
    public event Action OnLeaveLobbyEvent;
    //강퇴 당할경우 호출
    public event Action OnKickedFromLobbyEvent;
    //플레이어 준비 완료 여부를 확인할 때 호출
    public event Action<bool> OnPlayerReadyEvent;
    //게임 시작시 호출 - TODO : Relay 연동하기
    public event Action OnGameStartEvent;

    //현재 플레이어가 참가한 로비 정보
    private Lobby nowLobby;

    //인증용 플레이어의 이름
    private string playerName;

    //로비 관리용 타이머
    private float lobbyMaintainTimer = 0;
    private float lobbyInfoUpdateTimer = 0;

    //로비의 최대 플레이어 수
    private readonly int maxPlayer = 2;
    //로비 재활성화 시간
    private readonly float lobbyMaintainTime = 25f;
    //로비 정보 확인 시간
    private readonly float lobbyInfoUpdateTime = 1f;

    public bool IsPlaying { get; set; } = false;

    private string vivoxChannelName;

    private NetworkStringKey networkKey = new NetworkStringKey();

    private void Start()
    {
        Application.targetFrameRate = 120;

        NetworkAuthenticate.Instance.OnAuthenticateSuccessEvent += SetPlayerName;

        if(AuthenticationService.Instance.IsSignedIn)
        {
            playerName = AuthenticationService.Instance.PlayerName;
        }

        CreateLobbyUI();
    }

    protected override void Update()
    {
        base.Update();

        MaintainLobbyAlive();

        if (nowLobby != null && IsPlaying == false)
        {
            RefreshLobbyInfo();
        }
    }

    public void SetPlayerName()
    {
        playerName = AuthenticationService.Instance.PlayerName;
    }

    //로비 생성, 일단 무조건 비밀방으로
    public async void CreateLobby(string lobbyName)
    {
        CreateLobbyOptions createOption = new CreateLobbyOptions
        {
            Player = GetPlayer(true),
            IsPrivate = true,

            //참여자에게 게임 시작을 알리는 키 생성
            Data = new Dictionary<string, DataObject>
            {
                {networkKey.GameStartKey, new DataObject(DataObject.VisibilityOptions.Member, networkKey.ReadyKey) }
            }
        };

        Lobby lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayer, createOption);

        nowLobby = lobby;

        //로비 참가시 UI작동용
        OnJoinLobbyEvent?.Invoke(nowLobby);
        OnPlayerReadyEvent?.Invoke(false);
    }

    //플레이어 정보 받아오기
    private Player GetPlayer(bool isCreate)
    {
        string readyOption = isCreate.ToString();

        return new Player(
            id: AuthenticationService.Instance.PlayerId,
            data: new Dictionary<string, PlayerDataObject>()
            {
                {
                    networkKey.PlayerNameKey, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, playerName)
                },
                {
                    networkKey.ReadyKey, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, readyOption)
                }

            });
    }

    //30초가 되기 전 로비를 업데이트
    public async void MaintainLobbyAlive()
    {
        //호스트가 아니면 작동하지 않음
        if (!IsLobbyhost()) return;

        lobbyMaintainTimer += Time.deltaTime;
        if (lobbyMaintainTimer < lobbyMaintainTime) return;

        lobbyMaintainTimer = 0;

        await LobbyService.Instance.SendHeartbeatPingAsync(nowLobby.Id);
    }

    //로비 참가(코드로)
    public async void JoinLobbyByCode(string code)
    {
        var joinOption = new JoinLobbyByCodeOptions { Player = GetPlayer(false) };

        try
        {
            nowLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(code, joinOption);
        }
        catch (LobbyServiceException ex)
        {
            if(ex.Reason == LobbyExceptionReason.InvalidJoinCode)
            {
                Debug.Log("코드에 일치하는 방이 없음");
            }
            Debug.Log(ex.Reason);
            return;
        }

        OnJoinSuccessEvent?.Invoke();
        OnJoinLobbyEvent?.Invoke(nowLobby);
    }

    //로비 정보 업데이트
    public async void RefreshLobbyInfo()
    {
        if(nowLobby == null) return;

        lobbyInfoUpdateTimer += Time.deltaTime;
        if(lobbyInfoUpdateTimer < lobbyInfoUpdateTime) return;

        lobbyInfoUpdateTimer = 0;

        bool isReady = true;

        try
        {
            Lobby lobby = await LobbyService.Instance.GetLobbyAsync(nowLobby.Id);
            nowLobby = lobby;

            if (nowLobby.Players.Count > 1)
            {
                for (int i = 0; i < nowLobby.Players.Count; i++)
                {
                    if (!bool.Parse(nowLobby.Players[i].Data[networkKey.ReadyKey].Value))
                    {
                        isReady = false;
                        break;
                    }
                }
            }
            else
            {
                isReady = false;
            }

            OnPlayerReadyEvent?.Invoke(isReady);

            //로비의 GameStartKey 가 자신과 다르면 키 내용으로 릴레이에 참가
            if (nowLobby.Data[networkKey.GameStartKey].Value != networkKey.ReadyKey)
            {
                if (!IsLobbyhost())
                {
                    //호스트가 아니면, 키 내용으로 릴레이 참가
                    RelayManager.Instance.JoinRelay(nowLobby.Data[networkKey.GameStartKey].Value);

                    vivoxChannelName = nowLobby.Name;
                    NetworkSceneChanger.Instance.VoiceChannelName = vivoxChannelName;

                    SetReady(false);

                    //게임 시작 이벤트 호출
                    IsPlaying = true;
                    NetworkSceneChanger.Instance.ChangeScene("PlayScene");
                    OnGameStartEvent?.Invoke();
                    return;
                }
            }
        }
        catch (LobbyServiceException ex)
        {
            //로비정보 업데이트중, 403 오류 발생시 강퇴 or 로비가 사라졌음 처리
            if(ex.Reason == LobbyExceptionReason.Forbidden)
            {
                nowLobby = null;
                OnKickedFromLobbyEvent?.Invoke();
                return;
            }
        }

        if(!IsPlayerInLobby())
        {
            nowLobby = null;
            return;
        }

        //정보 갱신
        OnJoinLobbyEvent?.Invoke(nowLobby);
    }

    //호스트 여부를 알려주는 메서드
    public bool IsLobbyhost() => nowLobby != null && nowLobby.HostId == AuthenticationService.Instance.PlayerId;
    
    //
    private bool IsPlayerInLobby()
    {
        if(nowLobby == null || nowLobby.Players == null) return false;

        foreach(var player in nowLobby.Players)
        {
            if (player.Id != AuthenticationService.Instance.PlayerId) continue;
            return true;
        }

        return false;
    }

    //로비 떠나기
    public async void LeaveLobby()
    {
        if (nowLobby == null) return;

        //호스트일경우, 로비 자체가 사라지도록
        if(IsLobbyhost())
        {
            for (int i = 1; i < nowLobby.Players.Count; i++)
            {
                if (nowLobby.Players[i].Id == AuthenticationService.Instance.PlayerId) continue;

                await LobbyService.Instance.RemovePlayerAsync(nowLobby.Id, nowLobby.Players[i].Id);
            }
        }

        try
        {
            await LobbyService.Instance.RemovePlayerAsync(nowLobby.Id, AuthenticationService.Instance.PlayerId);
            nowLobby = null;
            OnLeaveLobbyEvent?.Invoke();
        }
        catch(Exception ex)
        {
            Debug.Log(ex);
            nowLobby = null;
            OnLeaveLobbyEvent?.Invoke();
        }
    }

    public async void KickPlayer(string id)
    {
        if (!IsLobbyhost()) return;
        if (id == AuthenticationService.Instance.PlayerId) return;

        try
        {
            await LobbyService.Instance.RemovePlayerAsync(nowLobby.Id, id);
        }
        catch (LobbyServiceException ex)
        {
            Debug.Log(ex);
        }
    }

    public async void GameStart()
    {
        if(!IsLobbyhost() || IsPlaying == true) return;

        try
        {
            //릴레이 코드 가져오기
            string relayCode = await RelayManager.Instance.CreateRelay();

            Lobby lobby = await Lobbies.Instance.UpdateLobbyAsync(nowLobby.Id, new UpdateLobbyOptions
            {
                //로비의 GameStartKey 를 릴레이 코드로 변경
                Data = new Dictionary<string, DataObject>
                {
                    {networkKey.GameStartKey, new DataObject(DataObject.VisibilityOptions.Member, relayCode) }
                }
            });

            vivoxChannelName = nowLobby.Name;
            nowLobby = lobby;

            NetworkSceneChanger.Instance.VoiceChannelName = vivoxChannelName;

            IsPlaying = true;
            OnGameStartEvent?.Invoke();
            ChangeGameScene();
        }
        catch (LobbyServiceException e)
        {
            Debug.Log(e);
        }
    }

    //씬 변경
    public void ChangeGameScene()
    {
        //임시로 테스트씬 하나 추가
        NetworkSceneChanger.Instance.ChangeScene("PlayScene");
    }

    public Lobby GetNowLobby()
    {
        return nowLobby;
    }

    //호스트가 아니면 준비버튼 사용 가능
    public async void SetReady( bool isReady)
    {
        if (nowLobby == null) return;

        string readyOption = isReady.ToString();
        string playerId = AuthenticationService.Instance.PlayerId;

        UpdatePlayerOptions playerOptions = new UpdatePlayerOptions();
        playerOptions.Data = new Dictionary<string, PlayerDataObject>()
                {
                    {networkKey.PlayerNameKey, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, playerName)},
                    {networkKey.ReadyKey, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, readyOption) }
                };

        try
        {
            var lobby = await LobbyService.Instance.UpdatePlayerAsync(nowLobby.Id, playerId, playerOptions);
            nowLobby = lobby;

            RefreshLobbyInfo();
        }
        catch (LobbyServiceException e)
        {
            Debug.Log(e);
            LeaveLobby();
        }
    }

    //게임 종료후, 로비 재참여
    public async void BackToLobby()
    {
        if (!IsLobbyhost()) return;
        try
        {
            Lobby lobby = await Lobbies.Instance.UpdateLobbyAsync(nowLobby.Id, new UpdateLobbyOptions
            {
                //로비의 GameStartKey 를 릴레이 코드로 변경
                Data = new Dictionary<string, DataObject>
                {
                    {networkKey.GameStartKey, new DataObject(DataObject.VisibilityOptions.Member, networkKey.ReadyKey) }
                }
            });

            nowLobby = lobby;
            RefreshLobbyInfo();
        }
        catch (Exception e)
        {
            Debug.Log(e);
            LeaveLobby();
        }
    }

    public void CreateLobbyUI()
    {
        PlayLobbyBGM();

        LobbyUI ui = UIManager.Instance.CreateUI<LobbyUI>();
        ui.Show();
    }

    public void PlayLobbyBGM()
    {
        SoundManager.Instance.ChangeBGM(eBGMType.LobbyBGM);
    }
}

현재는 로비 참가를 코드로만 진행중에 있으며, 커스텀 키 를 사용하여 플레이어 준비 상태를 관리, 게임 시작 상태 체크 등의 역할을 추가해주었습니다.

 

또한 게임이 정상적으로 종료시 로비로 다시 돌아오도록 설계를 변경하면서, 로비로 돌아올경우 UI를 다시 만들어주는 코드와 게임 시작 관련 커스텀 키 값을 변경해주는 코드를 추가해주었습니다.

 

현재는 코드를 통해 방에 참여하는 방식만을 사용중인데, 개인적으로는 공개방에 참가할 수 있도록 설계하는것 또한 고려했었습니다.

단, 제작이 늦춰지면서 멀티플레이쪽에 더이상 시간을 쓸 수 없게되어 코드 참가기능만 존재하는것이 아쉬울 따름입니다.

 

로비를 생성하거나 참가하게 되면, 해당 로비에 연동되는 UI들이 존재해야합니다.

로비 관련 UI는 두종류가 있는데, 하나는 로비 전체를 관리하는 UI,

다른 하나는 로비에 참여한 플레이어의 정보를 표기하는 UI 입니다.

더보기

로비 전체 관리 클래스

public class LobbyUI : UIPopup
{
    public TMP_InputField nameInput;
    public TMP_InputField roomCodeInput;

    //로그인, 방검색, 로비 UI본체
    public GameObject loginUI;
    public GameObject searchLobbyUI;
    public GameObject lobbyUI;

    //로비 UI에서 사용되는 필드
    public LobbyUIPlayerInfo infoPrefab;
    public Transform infoPerant;
    public TextMeshProUGUI roomCodeText;
    public Button StartButton;

    //로비 UI의 책 넘기는 효과를 발생시키기 위한 이벤트
    public event Action OnFlipRightEvent;
    public event Action OnFlipLeftEvent;
    public event Action<int> OnFlipToTargetEvent;

    private void Start()
    {
        NetworkAuthenticate.Instance.OnAuthenticateSuccessEvent += SuccessLogin;
        LobbyManager.Instance.OnJoinLobbyEvent += JoinLobby;
        LobbyManager.Instance.OnJoinSuccessEvent += JoinSuccess;
        LobbyManager.Instance.OnLeaveLobbyEvent += LeaveLobby;
        LobbyManager.Instance.OnKickedFromLobbyEvent += LeaveLobby;
        LobbyManager.Instance.OnPlayerReadyEvent += GameStartButtonControll;

        if (AuthenticationService.Instance.IsSignedIn == false)
        {
            loginUI.SetActive(true);
            searchLobbyUI.SetActive(false);
            lobbyUI.SetActive(false);
        }
        else
        {
            CloseAllUIs();

            if (LobbyManager.Instance.GetNowLobby() == null)
            {
                OnFlipToTargetEvent?.Invoke(2);
            }
            else
            {
                OnFlipToTargetEvent?.Invoke(4);
            }
        }
    }

    private void Update()
    {
        if (AuthenticationService.Instance.IsSignedIn == false) return;
        if(VivoxService.Instance.IsLoggedIn == false) return;

        //로비 오류 문제 수정용
        if(LobbyManager.Instance.GetNowLobby() == null)
        {
            lobbyUI.SetActive(false);
            searchLobbyUI.SetActive(true);
            OnFlipToTargetEvent?.Invoke(2);
        }
    }

    public void CloseAllUIs()
    {
        loginUI.SetActive(false);
        searchLobbyUI.SetActive(false);
        lobbyUI.SetActive(false);
    }

    public void SuccessLogin()
    {
        loginUI.SetActive(false);
    }

    private void JoinLobby(Lobby lobby)
    {
        bool isHost = false;

        if (lobbyUI.activeInHierarchy == false)
        {
            searchLobbyUI.SetActive(false);
        }

        roomCodeText.text = lobby.LobbyCode;

        if(AuthenticationService.Instance.PlayerId == lobby.HostId)
        {
            StartButton.gameObject.SetActive(true);
            isHost = true;
        }
        else
        {
            StartButton.gameObject.SetActive(false);
        }

        if (infoPerant.childCount != 0)
        {
            for (int i = 0; i < infoPerant.childCount; i++)
            {
                Destroy(infoPerant.GetChild(i).gameObject);
            }
        }

        for(int i = 0; i < lobby.Players.Count; i++)
        {
            LobbyUIPlayerInfo info = Instantiate(infoPrefab, infoPerant);
            info.SetPlayerInfo(lobby.Players[i], lobby.Players[i].Id == lobby.HostId);

            if(!isHost)
            {
                info.ActiveKickButton(false);

                info.ActiveReadyButton(lobby.Players[i].Id == AuthenticationService.Instance.PlayerId);
            }
            else
            {
                info.ActiveReadyButton(false);
            }
        }
    }

    private void LeaveLobby()
    {
        lobbyUI.SetActive(false);
        OnFlipLeftEvent?.Invoke();
    }

    public void GameStartButtonControll(bool isActive)
    {
        if (LobbyManager.Instance.IsLobbyhost() == false) return;
        StartButton.interactable = isActive;
    }

    public void CreateRoomButton()
    {
        PlayButtonSFX();
        LobbyManager.Instance.CreateLobby(roomCodeInput.text);
        searchLobbyUI.SetActive(false);
        OnFlipRightEvent?.Invoke();
    }

    public void JoinRoomButton()
    {
        PlayButtonSFX();
        LobbyManager.Instance.JoinLobbyByCode(roomCodeInput.text);
    }

    public void JoinSuccess()
    {
        searchLobbyUI.SetActive(false);
        OnFlipRightEvent?.Invoke();
    }

    public void LoginButton()
    {
        PlayButtonSFX();
        NetworkAuthenticate.Instance.Authenticate(nameInput.text);
    }

    public void GameStartButton()
    {
        PlayButtonSFX();
        LobbyManager.Instance.GameStart();
    }

    public void LeaveLobbyButton()
    {
        PlayButtonSFX();
        LobbyManager.Instance.LeaveLobby();
    }

    private void OnDestroy()
    {
        NetworkAuthenticate.Instance.OnAuthenticateSuccessEvent -= SuccessLogin;
        LobbyManager.Instance.OnJoinLobbyEvent -= JoinLobby;
        LobbyManager.Instance.OnJoinSuccessEvent -= JoinSuccess;
        LobbyManager.Instance.OnLeaveLobbyEvent -= LeaveLobby;
        LobbyManager.Instance.OnKickedFromLobbyEvent -= LeaveLobby;
        LobbyManager.Instance.OnPlayerReadyEvent -= GameStartButtonControll;
    }

    public void QuitGame()
    {
        PlayButtonSFX();
        Application.Quit();
    }

    public void ControllInfoButton()
    {
        PlayButtonSFX();
        UIControllInfo ui = UIManager.Instance.CreateUI<UIControllInfo>();
        ui.Show();
    }

    public void PlayButtonSFX()
    {
        SoundManager.Instance.PlayOneShotSFX(eSFXType.LobbyButtonSFX);
    }
}

 

플레이어 정보 관리 UI

public class LobbyUIPlayerInfo : MonoBehaviour
{
    public TextMeshProUGUI hostText;
    public TextMeshProUGUI playerNameText;
    public GameObject kickButton;
    public GameObject readyButton;

    public GameObject readyImage;

    public string playerId;
    private bool isReady = false;
    
    public void SetPlayerInfo(Player player, bool isHostId)
    {
        if (isHostId)
        {
            hostText.text = "Host";
            kickButton.SetActive(false);
        }
        else
        {
            hostText.text = "Guest";
            kickButton.SetActive(true);
        }

        playerNameText.text = player.Data["PlayerName"].Value;
        playerId = player.Id;

        if (bool.Parse(player.Data["IsPlayerReady"].Value) && isHostId == false)
        {
            isReady = true;
            readyImage.SetActive(true);
        }
    }

    //킥 버튼 관리 (호스트에게만 보이도록)
    public void ActiveKickButton(bool isActive)
    {
        kickButton.SetActive(isActive);
    }

    public void ActiveReadyButton(bool isActive)
    {
        readyButton.SetActive(isActive);
    }

    public void KickButtonClick()
    {
        LobbyManager.Instance.KickPlayer(playerId);
    }

    public void ReadyButtonClick()
    {
        if(isReady)
        {
            isReady = false;
            readyImage.SetActive(false);
            LobbyManager.Instance.SetReady(isReady);
        }
        else
        {
            isReady = true;
            readyImage.SetActive(true);
            LobbyManager.Instance.SetReady(isReady);
        }
    }
}

로비의 경우, LobbyManager에서 발급된 이벤트를 통해 내용이 관리되며, 이를통해 생성될 때 이벤트에 구독하고 파괴될때 해제 하면서 정보를 관리할 수 있도록 만들어주었습니다.

 

또한 LobbyManager에서 1초마다 로비 정보를 갱신할 때, 플레이어 정보 UI도 갱신하는 방식으로 로비 참여자 정보를 관리하고 있습니다.

 


호스트 - 클라이언트 간 연결 : RelayManager

Relay는 포트포워딩 등의 작업을 대신해준다고 생각할 수 있습니다.

RelayManager는 호스트의 참여 키를 만들어주는 메서드와 

클라이언트의 참여 키를 통해 서버에 참가하는 메서드 2개의 기능이 있습니다.

public class RelayManager : MonoSingleton<RelayManager>
{
    //씬을 넘어가지 않도록 Awake 재정의
    protected override void Awake()
    {
        //...싱글톤 관련 생략
    }

    //릴레이서버를 만드는 메서드
    public async Task<string> CreateRelay()
    {
        try
        {
            var playerCount = LobbyManager.Instance.GetNowLobby().MaxPlayers - 1;
            Allocation allocation = await RelayService.Instance.CreateAllocationAsync(playerCount);

            string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);

            //TODO : NetworkManager 연동
            NetworkManager.Singleton.GetComponent<UnityTransport>().SetHostRelayData
                (
                allocation.RelayServer.IpV4,
                (ushort)allocation.RelayServer.Port,
                allocation.AllocationIdBytes,
                allocation.Key,
                allocation.ConnectionData
                );

            NetworkManager.Singleton.StartHost();

            return joinCode;
        }
        catch(RelayServiceException e)
        {
            Debug.Log(e);
            return null;
        }
    }

    //호스트에게 참여하는 메서드
    public async void JoinRelay(string joinCode)
    {
        try
        {
            JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);

            //TODO : NetworkManager 연동
            NetworkManager.Singleton.GetComponent<UnityTransport>().SetClientRelayData
                (
                joinAllocation.RelayServer.IpV4,
                (ushort)joinAllocation.RelayServer.Port,
                joinAllocation.AllocationIdBytes,
                joinAllocation.Key,
                joinAllocation.ConnectionData,
                joinAllocation.HostConnectionData
                );

            NetworkManager.Singleton.StartClient();
        }
        catch (RelayServiceException e)
        {
            Debug.Log(e);
        }
    }
}

 


여기까지가 로비에서 사용되는 클래스들 입니다.

 

이후 차근차근 실제 멀티플레이중에 사용되는 클래스, Vivox 관련 클래스, 씬 변경에 관련된 클래스를 정리해 줄 예정입니다.

 

개인적인 평가로는 작성할 때는 몰랐지만 예외처리 부분에서 많은 문제가 보이기 시작했습니다.

 

이번 작업을 기반으로, 네트워크 관련 예외처리를 진행할 경우, 문제 상황을 보여주는 UI 와의 연동과, 문제 발생 후 작동 코드를 추가로 작성하는 내용에 대해 더 생각해볼 수 있는 기회가 된 것 같습니다.