Post

2026-07-07 TIL (92일차)

2026-07-07 TIL (92일차)

[TIL] CrazyRoo 멀티플레이어 프레임워크: 세션 및 방장/준비 시스템 구축


1. 세션 서브시스템(CrazyRooSessionSubsystem) 설계

데디케이티드 서버 1개당 방 1개라는 전제로 세션 관리 시스템을 만들었습니다. 핵심은 클라이언트가 직접 방을 만들지 않고, 빈 데디케이티드 서버에 첫 번째로 입장하는 사람이 방장이 되는 구조입니다.

[Server] 세션 등록 (CreateDedicatedSession)

  • 서버가 부팅될 때 자동으로 호출됩니다.
  • Settings.bIsDedicated = true로 설정하여 이 세션이 데디서버임을 명시했습니다.
  • bUsesPresence = false: 데디케이티드 서버는 클라이언트가 아니므로 Presence(현재 활동 상태)를 사용할 수 없기 때문에 반드시 꺼야 합니다.
  • 향후 커스텀 데이터를 넣기 위해 세션 룸 이름(RoomNameKey)도 세팅해두었습니다.

[Client] 방 검색 및 참가 (FindRooms, JoinRoomByIndex, CreateRoom)

  • 방 참가 (FindRooms -> JoinRoomByIndex): SessionSearch 객체를 사용해 주변(LAN)의 세션을 20개까지 검색합니다. 검색된 정보는 블루프린트에서 UI에 뿌리기 쉽게 FCrazyRooRoomInfo라는 커스텀 구조체로 변환하여 델리게이트(OnFindRoomsComplete)로 넘겨줍니다. 사용자가 목록을 클릭하면 해당 인덱스로 JoinSession을 호출하고 접속 IP를 받아 ClientTravel로 이동합니다.
  • 방 생성 (CreateRoom): 일반적인 P2P(리슨 서버) 게임과 다르게, 우리 게임은 “비어있는 서버에 1빠로 들어가기”가 방 생성입니다. FindRooms를 돌린 후 bAutoJoinIfEmpty 플래그를 통해 검색 결과를 확인합니다. 방이 비어있으면 1등으로 자동 참가시키고, 방이 없으면 에러, 이미 누군가 있으면 참가 탭을 이용하라는 에러를 띄웁니다.

2. PlayerState 확장: 방장(Host)과 준비(Ready) 상태

로비의 핵심 기능인 “누가 방장인가?”와 “모두 준비했는가?”를 다루기 위해 PlayerState를 확장했습니다.

변수 동기화 및 보안 가드

1
2
UPROPERTY(Replicated, BlueprintReadOnly) bool bIsHost = false;
UPROPERTY(ReplicatedUsing=OnRep_IsReady, BlueprintReadOnly) bool bIsReady = false;
  • 위 두 변수는 클라이언트가 마음대로 조작(핵/치트)할 수 없도록, 값을 변경하는 SetIsHost, SetIsReady 함수 내부에 HasAuthority() 가드를 철저히 쳤습니다. 오직 서버 권한에서만 값을 바꿀 수 있습니다.
  • bIsReady는 값이 바뀔 때마다 로비 UI의 준비 완료/취소 애니메이션 등을 재생해야 하므로 OnRep_IsReady()를 바인딩해 두었습니다.

3. PlayerController & GameMode: 룰 검증

유저가 준비 버튼을 누르거나 방장이 시작 버튼을 눌렀을 때, 그 행동이 타당한지 서버(GameMode)에서 검증합니다.

준비 요청 (ServerSetReady)

PlayerController의 RPC를 통해 준비 신호가 오면, 서버는 해당 유저가 PlayerState에서 “역할(ECrazyRooPlayerRole)을 선택했는지” 먼저 확인합니다. 아무 역할도 고르지 않은 깍두기 상태에서는 준비를 허용하지 않습니다.

모든 유저 준비 확인 (AreAllPlayersReady)

GameMode에서 매치 시작 조건을 검사합니다.

  1. PlayerArray.Num() >= MinPlayersToStart: 현재 접속 인원이 최소 인원(테스트용 2명)을 넘었는지 확인.
  2. 루프를 돌며 방장을 제외한 모든 유저의 bIsReadytrue인지 확인합니다.

방장 이탈 승계 (Logout)

방장이 튕기거나 나가버렸을 때 방이 터지는 것을 막기 위해 Logout 이벤트에서 로직을 추가했습니다. 나가는 사람이 방장이면 남은 인원 중 가장 먼저 들어온 사람(JoinOrder 최솟값)을 찾아 SetIsHost(true)로 왕관(방장 권한)을 넘겨줍니다.


4. Seamless Travel 엣지 케이스 처리 (가장 중요한 버그 예방)

언리얼에서 심리스 트래블(Seamless Travel)을 타면 PlayerState 객체가 파괴되고 새로 생성됩니다.

  • ** 문제:** 로비에서 방장이었던 사람이 StageSelect 맵으로 트래블하면서 방장 권한(bIsHost)이 초기화되어, 다음 맵에서 아무 버튼도 못 누르는 게임 진행 불가 상태에 빠질 수 있습니다.
  • ** 해결:** HandleSeamlessTravelPlayer 단계에서 GameInstance에 백업해 둔 JoinOrder를 복원하는 코드를 확장했습니다.
  • ** 주의:** 단, bIsReady는 다음 맵이나 다음 게임에서 또다시 준비 단계를 거쳐야 하므로 일부러 복원하지 않고 리셋되게 내버려 둡니다. 이는 버그가 아닌 의도된 설계입니다.
1
2
3
4
if (SavedOrder > 0) {
    PS->SetJoinOrder(SavedOrder);
    PS->SetIsHost(SavedOrder == 1);  // 1번 입장자에게 다시 방장 부여!
}

느낀 점 및 다음 계획

오늘은 뼈대를 잡는 서버 인프라 작업을 주로 했습니다. 복잡한 OnlineSubsystem의 델리게이트들을 직접 다루고, 맵 이동 시 데이터가 증발하는 생명주기(Lifecycle) 문제를 해결하면서 네트워크 아키텍처에 대한 이해도가 크게 올랐습니다.

내일은 오늘 만든 C++ 기반 위에 텅 빈 로비와 스테이지 맵을 올리고, 위젯(UI)을 연결하여 직접 버튼을 누르며 눈으로 테스트를 진행할 예정입니다!

This post is licensed under CC BY 4.0 by the author.