내일배움캠프/프로젝트

팀 프로젝트 진행 - 프로토타입 로비 제작

서보훈 2024. 11. 28. 20:39

UGS의 Lobby 기능을 사용하여 로비를 생성하고, 방 코드를 입력하여 생성된 로비에 입장할 수 있도록 만들어주었습니다.

 

먼저 로비기능을 관리하는 클래스입니다.

제네릭 싱글톤이 현재 씬을 넘어가는 형태로만 제작되어있기 떄문에, Awake를 재정의하여 씬을 넘어가지 않도록 만들어주었습니다.

public class LobbyManager : MonoSingleton<LobbyManager>
{
    public TMP_InputField nameInput;
    public TMP_InputField roomCodeInput;

    //UI관리용 이벤트
    //로비에 참여하면 호출
    public event Action<Lobby> OnJoinLobbyEvent;
    //로비에서 나가면 호출
    public event Action<Lobby> OnLeaveLobbyEvent;
    //강퇴 당할경우 호출
    public event Action OnKickedFromLobbyEvent;
    //로그인 성공시 호출
    public event Action OnAuthenticateSuccessEvent;

    //게임 시작시 호출 - 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;

    //싱글톤, 해당 오브젝트가 씬을 넘어가지 않도록 재정의
    protected override void Awake()
    {
        Create();

        if (Instance != this)
        {
            Destroy(gameObject);
        }
    }
}

 

현재 기획상 플레이 인원은 2인이기 때문에 readonly 를 사용하여 로비 인원을 제한해주고 있습니다.

또한, 지금은 이 스크립트에서 익명 로그인을 같이 진행중입니다.

이 내용을 차후에 별도로 빼주는것을 고려해야할것 같습니다.

 

인증 메서드 입니다.

//인증 메서드 - UGS에 연결
public async void Authenticate()
{
    //플레이어 프로필 설정
    playerName = nameInput.text;
    InitializationOptions initOptions = new InitializationOptions();
    initOptions.SetProfile(playerName);

    //인증 시도
    await UnityServices.InitializeAsync(initOptions);
    await AuthenticationService.Instance.SignInAnonymouslyAsync();

    OnAuthenticateSuccessEvent?.Invoke();
}

TMP_InputField 에 사용할 이름을 입력한 후, 버튼을 누르면 이 메서드가 작동하도록 제작하였습니다.

 

InitializationOptions.SetProfile 을 사용하여, 현재는 영문, 숫자, '-', '_'  만을 이름에 사용할 수 있습니다.

※ 이외의 내용이 포함되면 SetProfile 에서 메서드가 종료됩니다.

 

이후 설정한 프로필 이름으로 UGS 를 초기화 한 뒤, 익명으로 인증을 시도합니다.

인증에 성공할경우 이벤트를 발생시켜 UI를 변경합니다.

 

로비를 생성하는 메서드입니다.

현재는 비밀방만 구성되어있으며, 방 코드를 입력하여 로비에 참가하는 방법만을 지원하고 있습니다.

2인의 협동이 중요한 게임이기에 우선적으로 이 방법을 선택하였고, 공개방 기능은 이후 추가 구현을 할 시간이 생기면 구현해볼 예정입니다.

//로비 생성, 일단 무조건 비밀방으로
public async void CreateLobby()
{
    string lobbyName = roomCodeInput.text;

    CreateLobbyOptions createOption = new CreateLobbyOptions
    {
        Player = GetPlayer(),
        IsPrivate = true
    };

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

    nowLobby = lobby;

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

//플레이어 정보 받아오기
private Player GetPlayer()
{
    return new Player(
        id: AuthenticationService.Instance.PlayerId,
        data: new Dictionary<string, PlayerDataObject>()
        {
            {
                "PlayerName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, playerName)
            }
        });
}

방을 생성할 때 역시, TMP_InputField 에 방 이름을 지어주고 생성하는 방식으로 구현해주었습니다.

다만, 현재로써는 방 이름이 의미가 없기 때문에 이를 변경하거나, 방 코드가 아닌 이름으로 입장하는 방식을 고려해보아야 할 것 같습니다.

 

생성시 옵션으로 플레이어의 정보와, 비밀방 여부를 지정해줍니다. 이전에 언급했듯 모든 방을 비밀방으로 제작해주고 있기 때문에 IsPrivate 는 true 인 상태입니다.

 

비밀번호나 추가 데이터는 사용하지 않습니다.

추가데이터의 경우, 현재 게임모드에 대한 기획이 없기 때문에 추가 할 이유가 없는 상황입니다.

 

이후 실제 로비를 생성합니다.

CreateLobbyAsync 에는 3개의 매개변수가 들어가는데 로비의 이름, 최대 플레이어 수, 생성 옵션입니다.

이름은 플레이어가 입력한 내용을, 최대 플레이어수는 2인 고정, 생성 옵션은 위에서 만들어준 플레이어 정보와 비밀방 여부를 넣어주었습니다.

 

이후 이 스크립트에 현재 로비정보를 저장하고, 로비 내용을 변경해주기위해 참가 이벤트를 작동시켜줍니다.

 

GetPlayer의 경우,방을 생성하기 위해 인증서비스를 통해 받은 ID와, 플레이어가 입력한 이름을 반환

 

 

UGS Lobby 기능을 통해 만들어준 로비는, 30초동안 업데이트, 하트비트 요청이 없을경우 비활성화 됩니다.

이를 막아주기 위해 30초가 되기 전 하트비트 요청을 보내는 코드를 작성하고, Update 에 추가해줍니다.

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

    MaintainLobbyAlive();
    RefreshLobbyInfo();
}

//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 RefreshLobbyInfo()
{
    if(nowLobby == null) return;

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

    lobbyInfoUpdateTimer = 0;

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

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

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

RefreshLobbyInfo() 메서드의 경우, 다른 플레이어가 로비에 참가했을때 이를 알고 로비UI를 변경해줄 방법이 없기때문에, 1초마다 로비정보를 다시 확인하고 UI를 재생성하도록 이벤트를 발생시키는 메서드입니다.

 

로비 참가 메서드입니다.

코드를 TMP_InputField 에 입력하고 버튼을 누르면 해당 코드를 가진 로비로 입장하게 됩니다.

이때 역시 JoinOption 으로 플레이어 정보와 함께 로비에 참가 요청을 보냅니다.

//로비 참가(코드로)
public async void JoinLobbyByCode()
{
    string code = roomCodeInput.text;
    var joinOption = new JoinLobbyByCodeOptions { Player = GetPlayer() };
    nowLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(code, joinOption);

    OnJoinLobbyEvent?.Invoke(nowLobby);
}

참가에 성공할경우 로비 UI를 보여주기위해 이벤트를 발생시킵니다.

 

호스트 여부를 알려주는 메서드와, 자신이 로비 내에 있는지를 알려주는 코드입니다.

//호스트 여부를 알려주는 메서드
private 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;
}

호스트 여부의 경우, 지금은 30초마다 하트비트 요청을 보낼때, 참가자는 요청을 보낼 필요가 없기 때문에 사용하는 코드입니다.

이후, 게임 시작이나 강퇴 기능을 만들면서 이 메서드를 사용하여 해당 버튼이 호스트에게만 노출되도록 만들어줄 예정입니다.

 

로비 내에 있는지 알려주는 코드의 경우, 로비 정보를 업데이트 하면서 자신이 로비에 속해있지 않을경우 다시 방을 찾는 UI를 보여주어야 하기 때문에 존재하는 코드입니다.

아직 이벤트를 연결하지 않았기 때문에, 자신의 로비를 null 로 만들어주는 기능만 하고있습니다.

 

 

로비관련 UI를 조작하기위해 사용되는 코드입니다.

public class LobbyUI : MonoBehaviour
{
    public GameObject loginUI;
    public GameObject searchLobbyUI;
    public GameObject lobbyUI;

    public LobbyUIPlayerInfo infoPrefab;

    public Transform infoParent;

    public TextMeshProUGUI roomCodeText;

    private void Start()
    {
        LobbyManager.Instance.OnAuthenticateSuccessEvent += SuccessLogin;
        LobbyManager.Instance.OnJoinLobbyEvent += JoinLobby;

        loginUI.SetActive(true);
        searchLobbyUI.SetActive(false);
        lobbyUI.SetActive(false);
    }

    private void SuccessLogin()
    {
        loginUI.SetActive(false);

        searchLobbyUI.SetActive(true);
    }

    private void JoinLobby(Lobby lobby)
    {
        if (lobbyUI.activeInHierarchy == false)
        {
            searchLobbyUI.SetActive(false);
            lobbyUI.SetActive(true);
        }

        roomCodeText.text = lobby.LobbyCode;

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

        for(int i = 0; i < lobby.Players.Count; i++)
        {
            LobbyUIPlayerInfo info = Instantiate(infoPrefab, infoParent);
            info.SetPlayerInfo(lobby.Players[i].Data["PlayerName"].Value, lobby.Players[i].Id == lobby.HostId);
        }
    }
}

인증 서비스를 통해 ID를 발급받는데 성공하면 방을 찾는 UI를 활성화하고, 방을 생성하거나 코드를 통해 참가하면 로비 UI를 보여주는 역할을 하는 스크립트 입니다.

 

로비UI의 경우, 플레이어 정보를 보여주는 UI가 매 업데이트마다 재생성 되도록 만들어준 상태입니다.

현재 플레이어가 2인으로 고정되어있는 상태이기 때문에, 2개의 UI를 생성한 뒤, 이를 활성화/ 비활성화 하도록 수정하는것을 고려하고 있습니다.

 


트러블 슈팅

  • await LobbyService.Instance.CreateLobbyAsync() 로 생성된 로비를 Lobby 클래스가 받지 못하는 현상
//LobbyService.Instance.CreateLobbyAsync로 생성된 로비를 Lobby 클래스가 받지 못하는 현상
Lobby lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayer, createOption);

 

기존 LobbyManager 스크립트의 클래스명이 Lobby 여서 발생했던 문제입니다.

개인적으로 이 스크립트에 Manager 이름을 붙이는게 맞는가 고민중이지만, 이름이 곂쳐서 인식하지 못한다는것을 깨닳고 일단 LobbyManager로 스크립트 명을 변경해주었습니다.

 

  • 게스트가 참가했을때, 게스트 UI가 계속 복제되는 현상

OnJoinLobbyEvent 가 발생할 때 기존 UI를 삭제하고 새로운 UI를 발생시키기 위해서 infoParent 의 자식오브젝트를 전부 삭제하고, 다시 그 자식으로 UI를 생성하는 방식을 선택했었습니다.

if (infoParent.childCount != 0)
{
    for (int i = 0; i < infoParent.childCount; i++)
    {
        //GetChild가 0여서 해당 상황 발생, 0번째 자식만 삭제하고 다음 자식을 삭제하지 않아서 복제됨
        Destroy(infoParent.GetChild(0).gameObject);
    }
}

이 때, GetChild(0) 으로 해당 오브젝트를 삭제하였는데, 첫번째 자식 오브젝트가 삭제되고 다음 자식오브젝트의 인덱스가 0번이 될것이라고 생각하여 이렇게 만들었으나 이와같이 동작하지 않았고, 모든 오브젝트를 삭제하기위해 GetChild(i) 를 사용하여 오류를 해결하였습니다

 


해야할 것 목록

  • 강퇴기능 만들기
  • 로비에서 나가기 기능 만들기
  • 게임 시작 버튼 준비해두기
  • Vivox, Relay 연결 준비하기
  • 로비에서 나갔을경우 행동 만들어주기