티스토리 뷰

비공개 방을 만들기로 했다.

 

처음에는 Room CustomProperties에 비공개(isPrivate), 비번(pw)를 아예 넣어서 로비 리스트에서 읽도록 하려고 했다. 근데 CustomPropertiesForLobby에 pw를 넣으면 보안에 안 좋을 것 같기에… (혹여나 유저가 네트워크 패킷 열어보면?? 그냥 보이니까)

 

그래서 로비에는 isPrivate 만 보내고, 일단 pw는 보내지 않도록.

JoinRoom을 일단 하고, 들어온 직후 방장이 비번 검증해서 틀리면 그냥 퇴장되도록 만들기로 했다.

그러기 위해선 가장 간단하게는 OnJoinedRoom에서 방장에게 비번 검증 요청하는 RemoteProcedureCall을 하면 된다. 또 RPC로 방장이 검증해서 허용하거나 강퇴시키거나. 총 RPC 4개 함수가 필요한 셈이다.

 

하지만 문제가 생겼다.

현재 OnJoinedRoom() 함수는 NetworkManager가 갖고 있다. 근데 이 NetworkManager는 photon view를 갖고 있지 않다. RPC는 Photon View 컴포넌트를 가져야만 사용할 수 있다. 그래서 한번 NetworkManager에 Photon View를 붙여봤다. view ID가 1이 된다(계속 다시 시도해도). 근데 이건 로비에 있는 매니저의 photon view id와 중복된다. 다른 id로 바꾼다 해도 안 겹치리란 보장이 없을 것 같았다.

 

그래서 지피티에게 이 rpc를 쓰는 방법 말고 다른 건 없는지 물어봤다.

만능의 Event 알림-구독 시스템이 또 있다고 한다. Raise Event가 뭔지 모르겠어서 구글링을 해보니

https://red-tiger.tistory.com/71

다행히도 실제로 사용해본 사람이 있었다.

 

Raise Event는 포톤에서 제공하는 통신 방식이라고 한다. RPC가 특정 함수를 등록하고 호출하는 방식이면, Raise Event는 이벤트를 방출하고 수신자들이 이를 가로채는 방식이라고 한다.

 

제미나이에게 물어보니, RPC 대신 Raise Event를 쓰는 이유는 다음과 같다고 한다.

  • PhotonView가 없는 오브젝트에서도 데이터를 주고받을 수 있기 때문.
  • RPC보단 오버헤드가 적어서 빈번하게 발생하는 통신에 유리하다고 한다.
  • 주의사항으로는 얘도 역시 Event니까 OnEnable, OnDisable에서 콜백 등록/해제해줘야 한다. 특히 해제를 잘해야 메모리 누수나 null 에러가 안뜬다.

Raise Event을 사용하려면 세 단계가 필요하다.

  1. 이벤트 식별 위한 고유 번호(Byte) 정하기
  2. 이벤트 송신 (Raise Event) : 데이터 실어서 네트워크로 보냄. 이때 옵션(RaiseEventOptions)으로 누구에게 보낼지 등을 정해야한다.
  3. 이벤트 수신 (OnEvent) : IOnEventCallback 인터페이스를 상속받아서 서버로부터 전달된 이벤트 처리.

 

또 구성요소로는 다음의 것들이 있다.

  • Event Code : byte 타입 ID
  • Content : object 타입(배열/리스트 등등)으로 된 보낼 데이터
  • RaiseEventOptions : 대상 설정
  • SendOptions : TCP처럼 순서 보장할지에 관한 Reliability 여부 설정

 

그래서 결국 비공개방 비번 검증은 Raise Event를 사용하기로 했다.

  • 이벤트 코드 :
    • EVT_PW_CHECK_REQUEST : 참가자 → 방장 비번 검증 요청
    • EVT_PW_CHECK_RESULT : 방장 → 특정 참가자 검증 결과 응답
  • 요청하는 데이터(payload) : [requesterActorNumber, inputPassword]
  • 응답하는 데이터(payload) : [ok(bool)]
  • 타겟팅:
    • 요청은 Receivers = ReceiverGroup.MasterClient 해서 방장한테만 전송
    • 응답은 TargetActors = requesterActorNumber 로 요청자에게만 전송
    • SendOptions.SendReliable 해야함. 중요하니까.

 

검증만 이벤트 메시지로 처리하고, OK 가 나면 방장은 이미 로비 안에 있으니까 참가자 혼자 로비로 넘어가는 건 참가자가 LoadLevel하면 되겠지? 하고 쉽게 생각했다. 근데 참가자 혼자 다른 방으로 가거나 에러 나거나 문제가 생겼다.

 

도대체 왤까 싶어서 지피티한테도, 제미나이한테도 물어보니 이게 바로 race condition이라고 한다…. 운영체제에서 배웠던.

  • 유니티 엔진이 LoadLevel 실행시키고 씬 로딩 끝내면, 바로 ReadyManager의 Start() 함수를 실행시킨다.
  • 이때 포톤 서버는 거의 동시에 이 유저가 방에 들어왔다는 데이터를 서버에 등록하고 승인 신호를 보낸다.

이 두 가지 상태가 동시에 엎치락 뒤치락 진행되면 서버가 입장완료 신호를 주기도 전에 유니티가(입장하는 유저가) Start() 안에 있는 SetCustomProperties 를 실행해 버리는 상황이 발생한다.

포톤 서버 입장에서는 방에 들어오지도 않은 애(아직 승인 신호 안 보냈으니)가 자기 정보 초기화시킨다고? 말도 안돼. 하며 에러를 내는 것이었다.

 

이 레이스 컨디션을 해결하기 위해 결국 수동으로 순서를 조절하기로 했다. 그래서 Unity의 코루틴을 써보기로 했다.

  • 기존 방식: 씬 로드 → Start 실행 → SetCustomProperties 호출 (서버: 누구세요? 이슈) → 에러
  • 새 방식: 씬 로드 → Start 실행 → InRoom 체크 → (아직 방 입장 전이면) 코루틴 가동 → WaitUntil로 대기 → 입장 완료 확인 → 호출 성공

이렇게 바꾸기로 했다.

그리하여 ReadyManager.cs 의 Start() 함수에 다음과 같이 추가했다.

//정상적일 땐 즉시 프로퍼티 설정
if(PhotonNetwork.InRoom) PhotonNetwork.LocalPlayer.SetCustomProperties(props);
//아직 서버 신호 주기 전이면 코루틴으로 기다리기
else StartCoroutine(CoSetPropsWhenInRoom(props));

 

또 StartCoRoutine 함수도 추가.

private IEnumerator CoSetPropsWhenInRoom(Hashtable props)
{
	yield return new WaitUntil(() => PhotonNetwork.IsConnectedAndReady && PhotonNetwork.InRoom);
        PhotonNetwork.LocalPlayer.SetCustomProperties(props);
}

 

이외에는 강제로 퇴장 이벤트를 추가해서 방장이 비번 틀린 유저 쫓아내도록 만들었다. EVT_FORCE_TO_LOBBY 이벤트를 방장이 쏘고, 이걸 받은 참가자는 PhotonNetwork.LeaveRoom() 을 실행해 로비 씬으로 돌아가게 된다.

 

최종 코드 변화는 다음과 같다.

  • 네트워크 매니저 IOnEventCallback 인터페이스 implements (RPC 말고 OnEvent(EventDate e) 로 이벤트 수신하기 위해)
  • 이벤트 3개 등록
private const byte EVT_PW_CHECK_REQUEST = 10; // 참가자 -> 방장 (비번 검증 요청)
private const byte EVT_PW_CHECK_RESULT  = 11; // 방장 -> 참가자 (검증 결과)
private const byte EVT_FORCE_TO_LOBBY   = 12; // 방장 -> 참가자 (로비로 이동 지시)
  • OnEvent 이벤트 수신 처리 함수 추가
//Photon 이벤트 수신 함수
    public void OnEvent(EventData photonEvent)
    {
        //비번 검증 요청 이벤트
        if(photonEvent.Code == EVT_PW_CHECK_REQUEST)
        {
            //방장만 처리
            if(!PhotonNetwork.IsMasterClient) return;
            HandlePwCheckRequest_AsMaster(photonEvent);
        }

        //비번 검증 결과 이벤트
        else if(photonEvent.Code == EVT_PW_CHECK_RESULT)
        {
            //참가자 결과 처리
            HandlePwCheckResult_AsClient(photonEvent);
        }

        else if(photonEvent.Code == EVT_FORCE_TO_LOBBY)
        {
            //참가자만 처리
            if(!PhotonNetwork.IsMasterClient && moveRoutine == null) moveRoutine = StartCoroutine(CoMoveToLobbyWhenReady());
        }
    }
  • 참가자 측 요청 보내는 함수 추가
//참가자->방장 비번 검증 요청 보냄
    private void SendPasswordCheckRequestToMaster(string inputPw)
    {
        inputPw ??= "";

        //payload: [요청자 ActorNumber, 입력 비번]
        object[] content = new object[] {PhotonNetwork.LocalPlayer.ActorNumber, inputPw};

        //이벤트 방장만 받음
        var opt = new RaiseEventOptions
        {
            Receivers = ReceiverGroup.MasterClient //방장에게만
        };

        //이벤트 전송
        //요청코드, 데이터, 누가 받을지 설정한 것, sendreliable
        PhotonNetwork.RaiseEvent(EVT_PW_CHECK_REQUEST, content, opt, SendOptions.SendReliable);
        Debug.Log("[PrivateRoom] PW check 요청을 방장에게 보냄");
    }
  • 방장 측 요청 처리 + 결과 전송 함수 추가
//방장이 비번 검증 요청 받을 시 실행
    private void HandlePwCheckRequest_AsMaster(EventData e)
    {
        //이벤트에서 데이터 꺼내기
        var data = (object[])e.CustomData;

        int requesterActor = (int)data[0];//요청자 ActorNumber
        string inputPw = data[1] as string ?? "";//요청자 입력 비번

        //현재 방 실제 비번
        string realPw = PhotonNetwork.CurrentRoom.CustomProperties["pw"] as string ?? "";

        //입력 비번과 실제 비번 비교
        bool ok = (inputPw == realPw);

        //요청자에게 보낼 비교 결과 데이터
        object[] result = new object[] {ok};

        //요청자에게만 보내기
        var opt = new RaiseEventOptions
        {
            TargetActors = new int[] {requesterActor}
        };

        //결과 이벤트 전송
        PhotonNetwork.RaiseEvent(EVT_PW_CHECK_RESULT, result, opt, SendOptions.SendReliable);
        Debug.Log($"[PrivateRoom] PW 체크 요청 -> actor={requesterActor} ok={ok}");

        if (ok)
        {
            var moveOpt = new RaiseEventOptions {TargetActors = new int[] {requesterActor}};
            PhotonNetwork.RaiseEvent(EVT_FORCE_TO_LOBBY, null, moveOpt, SendOptions.SendReliable);
        }
    }
  • 참가자 측 결과 처리 함수 추가
    • 직접 LoadLevel 하지 않는다.
    • 이동은 EVT_FOCE_TO_LOBBY 에서만 처리
    • 실패일 때만 LeaveRoom
//참가자가 비번 비교 결과 받았을 때 실행
    private void HandlePwCheckResult_AsClient(EventData e)
    {
        //이벤트 데이터 꺼내기
        var data = (object[])e.CustomData;
        bool ok = (bool)data[0];//true 통과 false 실패

        pendingPassword = "";//사용 후 초기화

        //결과 따라 처리
        if (ok)
        {
            Debug.Log("[PrivateRoom] PW OK -> LOAD LOBBY");
            //PhotonNetwork.LoadLevel("Scene_Lobby");
        }
        else
        {
            Debug.Log("[PrivateRoom] PW WRONG -> LEAVEROOM");
            PhotonNetwork.LeaveRoom();
        }
    }

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함