2026-07-01 TIL (88일차)
2026-07-01 TIL (88일차)
오늘의 TIL: CrazyRoo 프로젝트 트러블슈팅 및 프레임워크 설계
1. 트러블슈팅 및 아키텍처 개선
① GameState 기반 UI 시스템 재설계
- 증상: 맵을 넘어갈 때(Travel) 이전 UI가 사라지지 않거나, 새 맵의 UI가 뜨지 않는 동기화 문제 발생.
- 원인: UI의 상태를 결정하는
CurrentMapId가GameInstance에 존재했음.GameInstance는 리플리케이션(복제)되지 않는 로컬 객체이므로, 서버가 값을 변경해도 클라이언트는 이를 인지할 수 없음. - 해결 및 개선:
CurrentMapId를 네트워크 통신이 보장되는GameState로 이전하고Replicated처리.- 클라이언트는 해당 변수의
OnRep이벤트를 구독하여, 자신의 로컬 데이터 테이블(FCrazyRooStageInfo)을 조회해 메인 UI를 동적으로 생성/삭제하도록 구조 변경. - 맵 단위로 고정되던
ECrazyRooStageStateEnum 대신, ESC 메뉴 등 동적 환경에 대응할 수 있도록SetUIInputMode(bool)함수를 통한 명시적 토글 방식으로 유연성을 확보.
② Subsystem 초기화 타이밍 및 구독 버그 해결
- 증상:
UCrazyRooLocalPlayerSubsystem이 클라이언트 입장에서GameState를 구독하려 할 때 타이밍 불일치 발생. 클라이언트로GameState가 늦게 도착하여 구독이 보류되고 로직이 멈추는 현상. - 해결: 기존
ULocalPlayerSubsystem의Init부분에서FWorldDelegates::OnWorldInitializedActors.AddUObject를 통해 이벤트를 바인딩했던 방식은 맵 이동(Travel)이 잦은 멀티플레이 환경에서 리플리케이션 지연으로 인해 치명적인 타이밍 한계를 가질 수밖에 없습니다. 이를World->GameStateSetEvent를 직접 구독하는 방식으로 변경한 것은 엔진의 네트워크 생명주기를 정확히 꿰뚫어 본 최적의 해결책입니다.PlayerController에 의존하던 불필요한 안전망 코드를 삭제하고 진입점을 단일화하여 유지보수성을 극대화했습니다.
③ 접속 순번(JoinOrder) 및 PlayerState 디버그 시스템 구축
- 목표: 데디케이티드 서버 + 스탠드얼론 클라 환경에서 로그만으로 “누가” 접속했는지 식별. (단순
GetPIEInstanceID()는 한계가 있음) - 해결 및 Seamless Travel 대응:
- 서버의
PostLogin단계에서JoinOrder를 부여하고PlayerState를 통해 리플리케이트. - Seamless Travel 함정: 심리스 트래블 시
PlayerState(및 Role)는 파괴 후 재생성됩니다. 이를 해결하기 위해GameInstance에PlayerId기준으로 백업해두고HandleSeamlessTravelPlayer에서 복원하는 패턴을 적용했습니다. - 순서 보장: 복원 시
JoinOrder를 먼저 세팅하여, 초기값(0) 로그가 찍히는 버그를 수정.
- 서버의
- 네트워크 지연 분석:
[Player?]로그는 버그가 아니라 구조적 경쟁(Race Condition)에 의한 정상적인 지연 현상입니다. 오너십을 가진PlayerController가PlayerState의 개별 프로퍼티보다 클라이언트에 먼저 도착하기 때문에 발생하는 필연적 타이밍 간극을 확인했습니다.
2. 향후 작업 계획 (비대칭 공유 캐릭터 조작)
‘WASD 토리의 모험’과 같이 4인(P_Left, P_Right, P_Shooter, P_Acrobat)이 1개의 캐릭터를 공유하여 조작하는 구조를 설계합니다.
- 한계 극복: 언리얼 엔진은 기본적으로 1
Controller: 1Pawn빙의(Possess) 구조이므로, 다중 빙의 방식은 불가능합니다. - 설계 대안:
- 공유 캐릭터는 서버 권위(Authority)로만 제어.
- 4명의
PlayerController는 각각의 역할에 맞는 조작만 Server RPC로 전송. - 서버가 이 4개의 입력을 합산하여 하나의 캐릭터를 물리적으로 움직임.
- 카메라 처리: 빙의 없이
SetViewTargetWithBlend를 사용하여 4명의 화면을 공유 캐릭터에 고정.
3. 언리얼 멀티플레이 프레임워크 & 접속 생명주기(Lifecycle)
멀티플레이어 게임에서 핵심 클래스들이 어디에 존재하고, 어떻게 초기화되는지 정리한 필수 가이드입니다.
핵심 프레임워크 클래스 네트워크 속성
| 클래스 (Class) | 존재 위치 (Network Presence) | 수명 (Seamless Travel 시) | 역할 및 설명 |
|---|---|---|---|
| GameInstance | 로컬 프로세스 전용 (서버/클라 각자 가짐) | 유지됨 (파괴 안됨) | 게임이 켜져 있는 내내 유지되는 싱글톤 성격. 레벨 이동 간 데이터 백업(Player Role 등)에 필수적입니다. |
| GameMode | 서버에만 존재 (클라 없음) | 파괴 후 새 레벨에서 재생성 | 게임의 룰, 승패 판정, 유저 접속 허가(PreLogin, PostLogin)를 통제하는 심판. |
| GameState | 서버 + 모든 클라이언트 | 파괴 후 새 레벨에서 재생성 | 모두가 알아야 하는 현재 게임의 상태(타이머, 점수, 현재 맵 상태 등)를 동기화. |
| PlayerController | 서버 + 나의 클라이언트 (남의 PC는 모름) | 유지됨 (객체 생존) | 플레이어의 “두뇌”. 서버 RPC를 쏘고 입력을 받는 핵심 채널. (심리스 시 BeginPlay는 재호출됨) |
| PlayerState | 서버 + 모든 클라이언트 | 파괴 후 새 레벨에서 재생성 | 플레이어의 데이터(닉네임, JoinOrder, 역할, 핑 등). 다른 유저의 정보를 보여주기 위해 모두에게 복제됨. |
| Pawn / Character | 서버 + 모든 클라이언트 | 파괴 후 새 레벨에서 재생성 | 플레이어의 “육체”. 컨트롤러가 빙의(Possess)하여 조종하는 물리적 형태. |
접속 생명주기 (Login Flow & Initialization)
유저가 방에 들어와서 캐릭터가 움직이기까지의 서버 기준 핵심 순서입니다.
GameMode::PreLogin(서버)- 클라이언트가 접속을 요청합니다.
- 서버가 밴(Ban) 목록이나 정원 초과 여부를 검사하고 허가/거부를 결정합니다.
GameMode::Login(서버)- 접속이 승인되면, 서버에 해당 유저를 위한
PlayerController를 생성합니다.
- 접속이 승인되면, 서버에 해당 유저를 위한
GameMode::PostLogin(서버)- [가장 중요한 초기화 지점] 클라이언트 접속이 완전히 확정되었습니다.
- 이 시점부터 안전하게 RPC를 보낼 수 있으며, 여기서 접속 순번(
JoinOrder)을 매기거나 캐릭터(Pawn)를 스폰시켜 빙의(Possess)시킵니다.
- 리플리케이션(Replication) 시작 (클라이언트)
- 서버에 생성된
PlayerController,PlayerState,Pawn정보가 클라이언트의 컴퓨터로 복제되어 내려가기 시작합니다. (이때 속도 차이로 인해 로그에 딜레이가 발생할 수 있습니다.)
- 서버에 생성된
PostNetInit(클라이언트)- 클라이언트에 액터 스폰과 네트워크 변수 초기화가 막 완료된 시점입니다.
BeginPlay(모두)- 모든 세팅이 끝나고 본격적인 게임 로직(Tick 등)이 시작됩니다.
This post is licensed under CC BY 4.0 by the author.