[20260506] Player2 이 망할녀석아

SmartBear·2026년 5월 6일
post-thumbnail

아래 내용은 AI 를 통한 정리가 이루어져 있음을 미리 공지 합니다.

🚀 Unity MPPM 환경에서 겪은 삽질기: "왜 플레이어 2가 들어오면 플레이어 1이 터질까?"

Unity에서 멀티플레이어 게임을 개발하다 보면 한 PC에서 여러 클라이언트를 띄워 테스트해야 하는 상황이 반드시 옵니다. 최근 유니티가 제공하는 MPPM(Multiplayer Play Mode)은 매우 편리하지만, 환경 설정을 제대로 하지 않으면 원인 모를 NullReferenceException과 크래시에 시달리게 됩니다. 제가 겪은 주요 이슈와 해결책을 정리합니다.


1. 🤯 이슈: 플레이어 2 진입 시 플레이어 1의 Lobby 서비스 파괴

현상

플레이어 1(Host)이 방을 만들고 대기 중인데, 플레이어 2(Client)가 로그인을 시도하는 순간 플레이어 1의 콘솔에 다음과 같은 에러가 끝없이 찍힙니다.
NullReferenceException: Object reference not set to an instance of an object (Unity.Services.Lobbies.Internal.WrappedLobbyService.TryCatchRequest)

원인: 세션 캐시(Session Cache) 충돌

유니티 서비스(UGS)는 기본적으로 "한 기기 = 한 명의 유저"를 가정합니다.

  • 별도의 설정을 하지 않으면 두 플레이어 프로세스가 로컬의 동일한 세션 파일을 공유하려 합니다.
  • 플레이어 2가 로그인하는 순간, 플레이어 1의 인증 토큰이 덮어씌워지거나 무효화됩니다.
  • 인증이 풀린 것을 감지한 SDK 내부 엔진이 객체를 파괴(Dispose)해버리고, 이때 기존에 돌고 있던 Update 루프에서 Null 참조가 발생한 것입니다.

해결책: InitializationOptions.SetProfile 활용

초기화 단계에서 각 플레이어마다 독립된 프로필(저장 경로)을 지정해줘야 합니다.

var options = new InitializationOptions();
#if UNITY_EDITOR
    // 실행 시마다 고유한 프로필명을 부여하여 세션 경로 분리
    options.SetProfile($"Player_{Guid.NewGuid().ToString().Substring(0, 8)}");
#endif
await UnityServices.InitializeAsync(options);

2. 🚦 이슈: 비동기 이벤트 폭주로 인한 Race Condition

현상

세션을 분리했는데도 간헐적으로 같은 NullReferenceException이 발생했습니다. 특히 플레이어 참여/탈퇴 시점에 집중되었습니다.

원인: 재귀적 API 호출

로비 이벤트(LobbyChanged, PlayerJoined, ...)를 구독하고 있었는데, 이벤트가 발생할 때마다 GetLobbyAsync를 수동으로 다시 호출하고 있었습니다.

  • 플레이어 2가 진입하면 여러 이벤트가 동시에 터집니다.
  • 각 핸들러가 동시에 GetLobbyAsync를 호출하면서 SDK 내부 엔진에 과부하가 걸리고, 이전 요청이 처리되기 전 다음 요청이 들어오며 내부 객체가 꼬이게 됩니다.
private bool _isUpdating = false;

private async Task UpdateDataHandler() {
    if (_isUpdating) return; // 이미 실행 중이면 무시
    _isUpdating = true;
    try {
        var lobby = await LobbyService.Instance.GetLobbyAsync(currentLobbyId);
        // 처리 로직...
    } finally {
        _isUpdating = false;
    }
}

3. 🔥 이슈: Firebase Persistence 초기화 실패 및 크래시

현상

ExecutionEngineException: String conversion error와 함께 유니티가 강제 종료되는 현상이 발생했습니다. 로그에는 Could not initialize persistence라는 메시지가 남았습니다.

원인: 로컬 DB 파일 점유권 싸움

Firebase Realtime Database는 오프라인 지원을 위해 로컬에 데이터를 캐싱(Persistence)합니다.

  • MPPM 환경의 두 프로세스가 동일한 SQLite 캐시 파일에 동시 접근을 시도합니다.
  • 파일 잠금(File Lock) 충돌이 발생하고, 이 과정에서 네이티브(C++) 레이어의 에러 메시지가 C#으로 넘어오다 인코딩 문제로 크래시가 터진 것입니다.

해결책: 테스트 환경에서 Persistence 비활성화

한 기기에서 여러 클라이언트를 돌릴 때는 로컬 캐싱 기능을 꺼야 합니다.

// 초기화 시점에 호출
FirebaseDatabase.DefaultInstance.SetPersistenceEnabled(false);

📝 마치며

멀티플레이어 환경에서 "공유 자원(파일, 세션, 메모리)"은 언제나 폭탄이 될 수 있습니다.

  • UGS를 쓴다면 반드시 SetProfile로 세션 경로를 찢어줄 것.
  • Firebase를 쓴다면 Persistence 설정을 체크할 것.
  • 비동기 통신은 반드시 중복 호출 방지 로직을 넣을 것.

이 세 가지만 지켜도 MPPM 환경에서의 의문사 90%는 예방할 수 있습니다. 킹받는 에러였지만 덕분에 내부 동작 원리를 깊게 판 좋은 경험이었습니다! 🛠️

profile
Python Dev with Infra -> Game Programmer

0개의 댓글