TIL

[Unreal Engine 멀티플레이] 멀티플레이 핵심 개념 통합 복습

think95592 2026. 8. 10. 20:44


0. 가장 먼저 기억할 한 문장

클라이언트는 행동의 의도를 보내고, Ownership이 요청 경로를 정하며, 서버가 권위 있는 원본을 검증·변경한다. 지속 상태는 Property Replication으로, 특정 대상의 순간 피드백은 RPC로 전달하고, 각 클라이언트는 받은 결과를 UI와 로그로 표현한다.

숫자 야구의 타자 추측 흐름으로 쓰면 다음과 같다.

Client A 입력
→ Client A가 소유한 PlayerController의 Server RPC
→ 서버 PlayerController
→ 서버 GameMode 검증·판정
→ 서버 원본 상태 변경
├─ GameState Property Replication → Client A/B OnRep → 공용 UI·로그
└─ PlayerController Client RPC → 요청자인 Client A → 개인 피드백

이 흐름을 외우는 것이 목표는 아니다. 아래 질문을 순서대로 답하면 같은 흐름을 새 기능에서도 스스로 만들 수 있어야 한다.

  1. 행동은 어느 PC에서 시작되는가?
  2. 클라이언트가 소유한 네트워크 액터는 무엇인가?
  3. 최종 판정에 사용하는 원본 데이터는 서버의 어느 객체에 있는가?
  4. 서버는 클라이언트 요청에서 무엇을 다시 검증해야 하는가?
  5. 결과는 지속 상태인가, 순간 사건인가?
  6. 누가 결과를 알아야 하며 늦게 접속해도 알아야 하는가?
  7. Client A/B의 UI와 로그에서 무엇이 보여야 하는가?

1. 네 개의 판단 축

1.1 실행 PC

함수 이름이나 클래스 이름만 보고 실행 위치를 추측하지 않는다. 어느 인스턴스에서 누가 호출했는지를 함께 본다.

  • 일반 함수는 호출한 현재 PC에서 실행된다.
  • Client A가 로컬 UI 함수를 호출하면 Client A에서 실행된다.
  • Client A가 유효한 Server RPC를 호출하면 RPC 본문은 서버에서 실행된다.
  • 서버가 A 소유 PlayerController의 Client RPC를 호출하면 Client A에서 실행된다.
  • 서버가 복제 프로퍼티를 변경하면 그 값은 관련 클라이언트로 전송되고 클라이언트의 OnRep가 실행된다.

HasAuthority()는 이 액터의 현재 인스턴스가 권위 있는 서버 인스턴스인지 묻는다. IsLocalController()는 이 PlayerController가 현재 컴퓨터의 로컬 플레이어 Controller인지 묻는다. 둘은 서로 바꿔 쓰는 조건이 아니다.

1.2 원본 데이터

원본 데이터는 최종 게임 결과를 결정할 때 신뢰하는 값이다. 클라이언트에도 같은 이름의 복제 변수가 있을 수 있지만, 그 값은 서버 원본의 복사본이다.

예를 들어 Client A의 GameState에 CurrentTryCount == 1이 보이더라도 Client A가 그 값을 2로 바꿔 서버의 시도 횟수를 증가시킬 수 있는 것은 아니다. 서버의 GameMode와 서버 GameState가 권위 있는 값을 변경해야 한다.

1.3 전달 방법

전달 수단은 데이터의 의미로 고른다.

의미 기본 수단 판단 근거
클라이언트의 행동 요청 Server RPC 클라이언트에서 서버로 의도를 전달한다.
현재도 유효한 공용 상태 Property Replication 현재 접속자와 늦은 접속자가 최신 값을 알아야 한다.
특정 플레이어의 순간 피드백 Client RPC 소유자 한 명에게 지금 한 번 전달하면 된다.
현재 접속자에게만 필요한 순간 표현 상황에 맞는 RPC 과거 사건을 늦은 접속자에게 재생할 필요가 없다.
로컬에서만 필요한 표시·임시값 로컬 함수·변수 네트워크 전송 자체가 필요 없다.

한 기능 안에서도 RPC와 Property Replication을 함께 사용할 수 있다. 요청, 판정, 상태, 표현은 서로 다른 문제이기 때문이다.

1.4 서버 검증

Server RPC가 서버에서 실행된다는 사실만으로 안전해지지 않는다. Server RPC의 인자는 클라이언트가 만든 요청이므로 서버가 다시 검사해야 한다.

검증은 보통 다음 범주로 나눈다.

  • 요청자: 실제 연결된 유효한 플레이어인가?
  • 역할·권한: 현재 이 행동을 할 수 있는 플레이어인가?
  • 단계: 현재 게임 상태에서 허용되는 행동인가?
  • 값: 개수, 범위, 형식, 중복이 올바른가?
  • 자원: 남은 횟수, 마나, 아이템, 탄약 등이 충분한가?
  • 시간: 서버 기준 마감과 쿨다운을 지켰는가?
  • 중복·경쟁: 같은 요청이 두 번 적용되거나 오래된 요청이 다음 단계에 적용되지 않는가?
  • 수명주기: 이미 종료·이탈·리셋 중인 객체와 라운드에 대한 요청은 아닌가?

2. 프레임워크 클래스의 존재 범위와 책임

Dedicated Server 1개와 Client A/B를 기준으로 본다.

객체 서버 소유 Client A 다른 Client B 적합한 책임
GameMode 있음 없음 없음 비밀 데이터, 규칙, 승패, 서버 상태 전이
GameState 있음 있음 있음 모두가 알아야 하는 현재 경기 상태
A의 PlayerState 있음 있음 있음 A의 역할·팀·점수 같은 플레이어 상태
A의 PlayerController 있음 있음 보통 없음 A의 Server RPC 입구, A 전용 Client RPC
Pawn/Character 서버와 관련 클라이언트에 복제 가능 있음 있음 월드 안 플레이어 행동과 표현
UMG UI 없음 A의 로컬 UI B의 로컬 UI 입력 수집과 복제 결과 표현

GameMode

  • 서버에만 존재한다.
  • 클라이언트가 직접 접근할 수 없다.
  • 비밀 구역과 판정 규칙을 두기에 적합하다.
  • UI에서 읽어야 하는 공개 상태는 GameState나 PlayerState로 옮겨야 한다.

GameState

  • 서버와 모든 클라이언트에 존재한다.
  • 공용 현재 상태를 복제하기에 적합하다.
  • 모든 클라이언트에 보인다고 해서 모든 클라이언트가 소유하는 Server RPC 창구인 것은 아니다.

PlayerState

  • 해당 플레이어뿐 아니라 다른 클라이언트에서도 필요할 수 있는 플레이어별 상태에 적합하다.
  • 역할, 팀, 공개 점수, 관전용 정보 등에 사용한다.
  • 개인에게만 보여야 하는 비밀 데이터를 무조건 PlayerState에 복제하면 안 된다.

PlayerController

  • 서버에는 각 플레이어의 PlayerController가 있다.
  • 소유 클라이언트에는 자신의 PlayerController가 있다.
  • 다른 클라이언트의 PlayerController는 보통 없다.
  • 그래서 Client A의 요청 입구와 Client A 전용 응답 출구로 적합하다.

UI

  • 각 클라이언트의 로컬 표현 계층이다.
  • 입력을 모으고 버튼을 제어할 수 있지만 게임 결과의 권위자가 아니다.
  • Dedicated Server에는 UI를 생성하지 않는다.

3. MYBaseball 타자 추측을 처음부터 끝까지 추적하기

3.1 로컬 입력

위젯의 SubmitSelectedZones()는 로컬에서 선택된 타일을 확인하고 현재 복제된 역할·단계에 따라 요청 함수를 선택한다.

타일 OnClicked
→ 위젯의 로컬 SelectedZones 변경
→ 선택 완료 버튼
→ SubmitSelectedZones()
→ PlayerController::SubmitGuessZones()

이때 SelectedZones는 아직 승인된 게임 상태가 아니다. 서버에 보내고 싶은 입력 초안이다.

클라이언트의 역할·단계 분기는 잘못된 버튼 사용을 줄이는 UX 방어다. 조작된 클라이언트가 RPC를 직접 호출할 수 있으므로 최종 권한 판정에는 사용할 수 없다.

관련 코드:

  • MYBaseball/Source/MYBaseball/UI/MYBaseballGameWidget.cpp
  • UMYBaseballGameWidget::SubmitSelectedZones()

3.2 소유한 PlayerController에서 Server RPC 호출

AMYBaseballPlayerController::SubmitGuessZones()IsLocalController()를 확인한 뒤 ServerSubmitGuessZones()를 호출한다.

서버 요청에는 다음이 포함된다.

  • 선택한 구역 배열
  • 클라이언트가 현재 보고 있는 시도 번호 ExpectedTryCount

ExpectedTryCount는 클라이언트가 시도 횟수를 결정한다는 뜻이 아니다. 서버의 현재 시도 번호와 비교해 오래된 요청을 거부하기 위한 표식이다.

관련 코드:

  • MYBaseball/Source/MYBaseball/MYBaseballPlayerController.h
  • AMYBaseballPlayerController::ServerSubmitGuessZones
  • MYBaseball/Source/MYBaseball/MYBaseballPlayerController.cpp
  • AMYBaseballPlayerController::SubmitGuessZones()

3.3 서버 PlayerController에서 RPC 본문 실행

Client A가 호출했지만 _Implementation() 본문은 서버에 있는 A의 PlayerController에서 실행된다.

Client A: ServerSubmitGuessZones(...) 호출
                   ↓ 네트워크
Server: A의 PlayerController에서 ServerSubmitGuessZones_Implementation(...) 실행

서버 PlayerController는 GetAuthGameMode<AMYBaseballGameMode>()로 서버 전용 GameMode를 얻어 TrySubmitGuessZones()에 요청을 전달한다.

GameMode를 얻지 못했을 때는 서버가 요청자에게 개인 시스템 오류를 돌려준다. 클라이언트가 직접 GameMode를 찾는 구조가 아니다.

3.4 서버 GameMode 검증

현재 TrySubmitGuessZones()가 검사하는 핵심 조건은 다음과 같다.

  1. 서버 권위에서 실행되는가?
  2. 요청 PlayerController가 유효한가?
  3. 현재 단계가 BatterGuessing인가?
  4. ExpectedTryCount가 서버 CurrentBatterTryCount와 같은가?
  5. 서버 기준 제한시간이 남아 있는가?
  6. 요청자의 서버 PlayerState 역할이 Batter인가?
  7. 서버의 비밀 구역이 준비되어 있는가?
  8. 구역이 정확히 세 개인가?
  9. 각 구역이 1~9 범위인가?
  10. 중복 구역이 없는가?
  11. 서버 기준 시도 횟수가 세 번 미만인가?

검증에 성공한 뒤에만 타이머를 정리하고 시도를 증가시키며 적중 구역과 결과를 계산한다. 잘못된 입력이 시도를 소비하지 않게 하려면 검증 완료 전에는 권위 상태를 바꾸지 않는 순서가 중요하다.

관련 코드:

  • MYBaseball/Source/MYBaseball/MYBaseballGameMode.cpp
  • AMYBaseballGameMode::TrySubmitGuessZones()

3.5 서버 원본 상태 변경

현재 기능의 데이터 위치는 다음처럼 나뉜다.

데이터 권위 위치 공개 방식
SecretZones 서버 GameMode 복제하지 않음
판정용 CurrentBatterTryCount 서버 GameMode 화면용 현재값을 GameState에 반영
현재 시도·적중 수·맞힌 구역 서버 GameState Property Replication
결과·아웃 서버 GameState Property Replication
이닝 단계 서버 GameState Property Replication
점수·주자·경기 결과 서버 GameState Property Replication
역할·고정 팀 서버 PlayerState Property Replication
공용 게임 로그·채팅 서버 GameState 제한된 배열로 Property Replication
개인 거부 메시지 서버에서 생성 요청자 PlayerController의 Client RPC

비밀 구역은 클라이언트 UI에 필요하지 않으며 공개하면 게임 규칙이 깨진다. 반면 현재 단계와 점수는 모든 참가자와 늦은 접속자가 알아야 하므로 GameState에 둔다.

3.6 Property Replication으로 Client A/B 갱신

GameState의 공개 상태는 ReplicatedUsing으로 선언되어 있고 GetLifetimeReplicatedProps()에서 DOREPLIFETIME으로 등록된다.

서버 GameState 값 변경
→ 복제 시스템이 관련 클라이언트에 최신 값 전송
→ Client A GameState 복사본 변경
→ Client A OnRep
→ Client B GameState 복사본 변경
→ Client B OnRep

Property Replication은 RPC처럼 프로퍼티 변경 줄에서 즉시 원격 함수를 호출하는 구조가 아니다. 네트워크 업데이트 시점에 최신 상태가 전달되며, 중간 값이 합쳐질 수도 있다. 중요한 것은 과거의 모든 변경 과정이 아니라 수신자가 최종적으로 현재 상태에 수렴하는 것이다.

관련 코드:

  • MYBaseball/Source/MYBaseball/Game/MYBaseballGameState.h
  • MYBaseball/Source/MYBaseball/Game/MYBaseballGameState.cpp
  • AMYBaseballGameState::GetLifetimeReplicatedProps()

3.7 Client RPC로 요청자 개인 피드백 전달

요청 거부 이유는 요청한 플레이어에게만 필요하다.

서버 GameMode 검증 실패
→ 서버 A PlayerController::SendPrivateSystemLog()
→ ClientReceivePrivateSystemLog() Client RPC
→ Client A의 로컬 로그 UI

Client B와 관전자에게는 전달하지 않는다. 과거의 개인 오류를 늦게 접속한 플레이어가 받을 필요도 없으므로 Property Replication보다 Client RPC가 적합하다.

반대로 승인된 공용 판정 상태는 A만의 피드백으로 끝내면 안 된다. Client B와 늦은 접속자도 현재 상태를 알아야 하므로 GameState 복제를 함께 사용한다.


4. RPC와 Property Replication을 구분하는 기준

Server RPC가 적합한 것

  • 추측 제출 요청
  • 채팅 전송 요청
  • 재경기 동의 요청
  • 공격 시작 요청
  • 문 열기 요청

공통점은 클라이언트가 서버에 “이 행동을 하고 싶다”고 알리는 순간 요청이라는 것이다.

Property Replication이 적합한 것

  • 현재 경기 단계
  • 현재 점수와 아웃
  • 현재 체력과 사망 상태
  • 문이 현재 열려 있는가
  • 현재 라운드와 남은 기회
  • 늦게 접속한 플레이어가 즉시 알아야 하는 정보

공통점은 과거에 무슨 일이 있었는지보다 지금 값이 무엇인지가 중요하다는 것이다.

Client RPC가 적합한 것

  • 요청자만 보는 입력 거부 이유
  • 해당 플레이어의 개인 결과 화면 열기
  • 소유자에게만 필요한 순간 안내

Multicast가 적합할 수 있는 것

  • 현재 접속자에게 한 번 보여주는 폭발 이펙트
  • 짧은 피격 효과음
  • 과거 사건을 늦은 접속자에게 재생할 필요가 없는 순간 표현

Multicast는 지속 상태의 대체물이 아니다. 늦게 접속한 클라이언트는 과거 Multicast를 받지 못한다.

로컬 처리가 적합한 것

  • 타일 선택 강조
  • 마우스 오버
  • 입력창 임시 문자열
  • 복제된 마감 시각으로 남은 초 계산
  • 서버 판정에 영향을 주지 않는 UI 애니메이션

5. OnRep와 UI 수명주기

OnRep의 역할

OnRep는 클라이언트의 복제값이 갱신되었을 때 표현 계층에 변화를 알리는 연결점이다. 현재 GameState는 OnRep에서 델리게이트를 Broadcast하고 위젯이 그 델리게이트를 받아 다시 그린다.

서버 Setter
→ 서버 값 변경
→ 서버에서는 Notify 직접 호출
→ Property Replication
→ 클라이언트 OnRep
→ 클라이언트 Notify/Delegate
→ 로컬 위젯 Refresh

서버가 프로퍼티를 직접 변경했을 때 서버의 OnRep 자동 호출에 의존하지 않는다. 현재 코드는 서버 Setter에서 Notify를 직접 호출하고 클라이언트 OnRep에서도 같은 Notify를 호출한다.

UI가 늦게 만들어질 수 있다

다음 순서가 가능하다.

복제값 도착
→ OnRep 실행
→ 위젯이 아직 없음
→ 위젯 생성

OnRep 이벤트만 구독하면 초기 화면이 비어 있을 수 있다. 안전한 패턴은 두 가지를 함께 사용하는 것이다.

  1. 델리게이트를 구독해 이후 변경을 받는다.
  2. 위젯 생성·바인딩 직후 Getter로 현재 상태를 읽어 즉시 한 번 갱신한다.

현재 PlayerController는 BeginPlay()OnRep_PlayerState()에서 위젯 생성 및 상태 바인딩을 시도한다. 개인 Client RPC가 위젯보다 먼저 도착하는 경우를 위해 최대 20개의 개인 시스템 로그 대기 큐도 사용한다.

흔한 OnRep 오해

  • OnRep가 게임 규칙을 판정해야 한다 → OnRep는 클라이언트 표현 갱신에 집중한다.
  • OnRep는 모든 중간 값을 한 번씩 보장한다 → Property Replication은 현재 상태 수렴이 목적이다.
  • OnRep가 실행되면 UI가 반드시 존재한다 → 객체 생성·복제·자산 로딩 순서는 다를 수 있다.
  • 서버와 클라이언트의 OnRep 동작을 완전히 같다고 가정한다 → 서버는 Setter에서 필요한 Notify를 명시적으로 호출하는 편이 안전하다.

6. 접속과 이탈을 같은 체계로 보기

접속

서버 GameMode의 PostLogin()에서 새 참가자를 등록하고 역할과 팀을 배정한다.

클라이언트 접속
→ 서버 PlayerController/PlayerState 생성
→ 서버 GameMode::PostLogin()
→ 서버 PlayerState 역할·팀 변경
→ PlayerState Property Replication
→ 클라이언트 OnRep
→ 로컬 UI 갱신

늦게 접속한 클라이언트에게 필요한 현재 상태는 Property Replication으로 복구한다.

  • 역할과 팀
  • 이닝과 공수
  • 점수와 주자
  • 현재 경기 단계
  • 현재 타석 상태
  • 최근 공용 게임 로그와 채팅

늦게 접속한 클라이언트에게 필요하지 않은 것은 다시 보내지 않는다.

  • 과거 버튼 클릭
  • 다른 플레이어의 개인 오류 메시지
  • 이미 끝난 짧은 이펙트
  • 투수의 비밀 구역

이탈

서버 GameMode의 Logout()에서 참가자와 역할을 정리하고 경기 조건을 다시 평가한다.

UI에서 플레이어를 숨기는 것만으로 서버 참가자 목록이 정리되는 것은 아니다. 이탈한 플레이어가 서버의 생존자·턴·준비 목록에 남으면 다음과 같은 버그가 생긴다.

  • 모든 플레이어가 끝났는데 경기가 종료되지 않음
  • 존재하지 않는 플레이어의 턴을 기다림
  • 재경기 동의가 영원히 완료되지 않음
  • 역할이 비어 있는데 다음 참가자에게 재배정되지 않음

접속·이탈 정책도 원본은 서버가 가져야 한다.


7. 복제 비용과 정확성

정확히 동작한다고 모든 값을 매 프레임 복제하면 안 된다. 무엇을 얼마나 자주, 누구에게 보낼지 판단한다.

현재 프로젝트의 좋은 사례: 마감 시각 복제

남은 시간을 매초 복제하지 않는다.

  1. 서버가 서버 기준 마감 시각을 한 번 정한다.
  2. GameState가 그 마감 시각을 복제한다.
  3. 각 클라이언트는 서버 시간과 마감 시각의 차이를 로컬 UI에 표시한다.
  4. 실제 시간 초과 판정은 서버가 한다.

표시는 로컬에서 부드럽게 계산하면서 판정 권위는 서버에 남고, 매초 복제 트래픽도 줄어든다.

현재 프로젝트의 공용 로그와 채팅

  • 게임 로그는 최근 30개를 GameState 배열로 복제한다.
  • 채팅은 최근 50개를 GameState 배열로 복제한다.
  • 늦은 접속자가 현재 기록을 받아야 하므로 순간 Multicast보다 상태 복제가 맞다.
  • 규모가 커지면 배열 전체 갱신 비용을 측정하고 증분 복제를 검토한다.

ForceNetUpdate()의 의미

ForceNetUpdate()는 원격 UI를 현재 코드 줄에서 동기식으로 즉시 바꾸는 함수가 아니다. 다음 네트워크 업데이트를 앞당기도록 요청한다. 지연과 패킷 전달, 여러 프로퍼티의 갱신 순서는 여전히 존재한다.

따라서 UI는 여러 OnRep의 고정 순서를 가정하기보다 갱신 신호를 받았을 때 필요한 현재 상태 묶음을 다시 읽는 편이 안전하다.

Relevancy, Priority, Dormancy의 위치

  • Relevancy는 이 연결에 이 액터를 보낼 필요가 있는지 판단한다.
  • Priority는 대역폭이 부족할 때 어떤 관련 액터를 먼저 보낼지 조절한다.
  • Dormancy는 한동안 변하지 않는 액터의 반복 확인 비용을 줄인다.
  • FlushNetDormancy()는 휴면 중 상태가 바뀐 액터를 다시 전송 대상으로 깨운다.

Priority를 높인다고 관련성이 없는 액터가 보이거나 휴면 상태가 자동 해제되는 것은 아니다.


8. 패키징과 Dedicated Server가 최종 검증인 이유

PIE에서는 서버와 여러 클라이언트가 한 에디터 프로세스 안에 섞여 보여 잘못된 실행 위치가 가려질 수 있다. 패키징된 Dedicated Server와 Client는 프로세스가 분리되므로 구조적 오류가 더 명확하게 드러난다.

패키징 환경에서 드러나는 문제

  • 클라이언트 코드가 GameMode에 접근함
  • Dedicated Server가 UI를 만들려고 함
  • UI Blueprint가 cook에서 빠져 위젯 로드 실패
  • 클라이언트와 서버가 서로 다른 기본 맵을 실행함
  • 주소·포트·방화벽 문제로 연결 자체가 실패함
  • Client/Server Target 또는 엔진 빌드 환경이 맞지 않음
  • 로그에 NetMode와 요청자가 없어 실행 위치를 구분할 수 없음

현재 MYBaseball의 배포 결정

  • Lvl_UIOnly를 Game/Server/Editor 기본 맵과 cook 대상에 사용한다.
  • /Game/UI를 항상 cook하여 WBP_BaseballGame 누락을 방지한다.
  • 렌더링이 필요 없는 Dedicated Server도 같은 빈 게임 월드를 사용할 수 있다.
  • 현재 기록상 런처 엔진은 Client/Server Target 제약이 있었다.
  • 한글 프로젝트 경로 때문에 MSVC 중간 파일 생성 실패가 있었으므로 영문 경로의 소스 빌드 환경 검증이 남아 있다.

패키징 성공과 멀티플레이 성공은 별개다. 다음 세 단계를 각각 확인한다.

  1. Build/Cook/Stage/Pak/Archive가 성공했는가?
  2. Dedicated Server가 올바른 맵과 GameMode로 실행되는가?
  3. Client A/B가 접속해 실제 RPC·복제·UI 흐름을 통과하는가?

9. 로그로 멀티플레이 흐름 진단하기

화면이 맞아 보이는 것만으로 실행 위치를 증명할 수 없다. 기능 한 번에 같은 식별 정보를 서버와 클라이언트 로그에 남긴다.

권장 로그 문맥

기능명 | NetMode | 요청자 | 함수명 | 단계 | 요청 번호 | 서버 상태 | 승인/거부 이유

타자 추측 한 번의 기대 로그는 다음과 같다.

[Guess][ClientA][Client] SubmitGuessZones ExpectedTry=0
[Guess][PlayerA][DedicatedServer] ServerSubmitGuessZones ExpectedTry=0
[Guess][PlayerA][DedicatedServer] Accepted Try=1 Hit=2 Result=None
[Guess][ClientA][Client] OnRep_AtBatStatus Try=1 Hit=2
[Guess][ClientB][Client] OnRep_AtBatStatus Try=1 Hit=2
[Guess][ClientA][Client] PrivateFeedback Accepted

PC별 기대 결과

단계 Dedicated Server Client A Client B
타일 입력 없음 있음 없음
로컬 Submit 함수 없음 있음 없음
Server RPC 본문 있음 없음 없음
GameMode 판정 있음 GameMode 없음 GameMode 없음
GameState 권위 변경 있음 없음 없음
GameState OnRep 서버 Setter에서 별도 Notify 있음 있음
A 전용 Client RPC 송신 수신 없음
UI 갱신 UI 없음 공용+개인 공용만

증상에서 원인 좁히기

버튼을 눌러도 서버 로그가 없다

확인 순서:

  1. 로컬 버튼 함수가 실행됐는가?
  2. IsLocalController() 조건을 통과했는가?
  3. Server RPC를 PlayerController 같은 소유 액터에서 호출했는가?
  4. 해당 액터와 연결이 유효한가?
  5. 서버 프로세스 로그를 보고 있는가?

서버 판정 로그는 있는데 Client A/B UI가 안 바뀐다

확인 순서:

  1. 서버가 실제 GameState 프로퍼티를 변경했는가?
  2. 프로퍼티에 ReplicatedUsing이 있는가?
  3. DOREPLIFETIME 등록이 있는가?
  4. 액터가 복제되는가?
  5. OnRep가 실행됐는가?
  6. 위젯이 델리게이트를 구독했는가?
  7. 위젯 생성 직후 Getter 초기화를 했는가?

Client A만 바뀌고 Client B는 안 바뀐다

  • 공용 상태를 Client RPC로만 보낸 것은 아닌가?
  • GameState 대신 요청자 로컬 UI만 직접 바꾼 것은 아닌가?
  • B에게 해당 액터가 관련성이 있는가?
  • B의 OnRep와 바인딩 로그가 있는가?

늦게 접속한 Client C가 현재 상태를 모른다

  • 현재 상태를 과거 Multicast에만 의존한 것은 아닌가?
  • 공개 상태가 GameState/PlayerState의 복제 프로퍼티인가?
  • 초기 UI가 현재 Getter 값을 읽는가?
  • 휴면 액터라면 변경 시 휴면을 해제했는가?

제한시간 직전 요청이 다음 시도를 소비한다

  • 서버 기준 마감 시각으로 검사하는가?
  • 클라이언트가 보낸 시도 식별자를 서버 현재 시도와 비교하는가?
  • 시간 초과 처리와 RPC 처리가 같은 서버 원본 상태를 기준으로 한 번만 전이되는가?

이탈 후 경기가 끝나지 않는다

  • Logout()에서 서버 참가자 목록을 제거했는가?
  • 제거 후 종료 조건을 다시 평가했는가?
  • 이탈한 PlayerState나 Controller 포인터를 목록에 유지하고 있지 않은가?

10. 자주 틀리는 정신 모형 교정

잘못된 생각 올바른 판단
Server RPC면 클라이언트 값을 신뢰해도 된다. Server RPC 인자는 요청일 뿐이며 서버가 모두 재검증한다.
Replicated 액터면 아무 클라이언트나 Server RPC를 호출할 수 있다. 클라이언트→서버 RPC에는 Ownership 경로가 필요하다.
GameState는 모두에게 있으므로 클라이언트 요청 창구다. GameState의 가시성과 클라이언트 소유권은 다르다.
UI에서 버튼을 막았으니 서버 검증은 필요 없다. 조작된 클라이언트는 UI를 우회할 수 있다.
Client RPC로 현재 상태를 보내면 복제와 같다. 늦은 접속자는 과거 Client RPC를 받지 못한다.
Multicast를 Reliable로 하면 영구 상태가 된다. Reliable도 과거 사건을 늦은 접속자에게 재생하지 않는다.
OnRep는 서버와 클라이언트에서 항상 같은 방식으로 호출된다. 서버는 Setter에서 로컬 Notify가 별도로 필요할 수 있다.
OnRep가 실행되면 위젯이 준비되어 있다. UI 생성 순서가 늦을 수 있어 구독과 현재값 초기화가 모두 필요하다.
복제 프로퍼티를 클라이언트에서 바꾸면 서버도 바뀐다. 서버 원본에는 영향이 없고 다음 복제에서 덮일 수 있다.
ForceNetUpdate()는 즉시 동기식 UI 갱신이다. 네트워크 업데이트를 앞당길 뿐 지연과 전달 순서는 남는다.
PIE에서 되면 Dedicated Server 패키지도 된다. 맵, cook, Target, 연결, 방화벽, 프로세스 분리가 별도 변수다.

11. 현재 MYBaseball의 확인 상태

코드에서 확인된 것

  • 위젯에서 PlayerController로 입력 전달
  • 소유 PlayerController의 Reliable Server RPC
  • 서버 GameMode의 역할·단계·값·시간·시도 검증
  • GameMode의 비밀 구역 서버 전용 보관
  • GameState의 경기 상태 Property Replication
  • GameState OnRep와 델리게이트 기반 UI 갱신
  • PlayerState 역할·팀 복제
  • 요청자 전용 개인 시스템 로그 Client RPC
  • UI 생성 전 개인 RPC를 위한 대기 큐
  • PostLogin() 역할 배정과 Logout() 정리
  • 서버 마감 시각 복제와 로컬 카운트다운 계산
  • 공용 로그·채팅의 제한된 배열 복제
  • UI 전용 기본 맵과 명시적 cook 설정

저장된 실행 로그에서 확인된 것

  • 서버의 역할 배정
  • 서버의 비밀 구역 승인
  • 서버의 타자 추측 판정
  • 여러 시도와 아웃·홈런 결과

12. 마지막 자가 설명 기준

타자 추측 제출을 다음 수준으로 설명할 수 있어야 한다.

Client A의 로컬 UI는 추측 의도만 만들고, A가 소유한 PlayerController의 Server RPC를 호출한다.

RPC 본문은 서버의 A PlayerController에서 실행되고 서버 전용 GameMode로 요청을 전달한다.

GameMode는 요청자의 역할, 현재 단계, 서버 마감시간, 시도 번호, 입력 개수·범위·중복, 남은 기회를 검사한 뒤에만 비밀 구역과 비교하고 권위 상태를 변경한다.

공용 현재 상태는 서버 GameState에서 복제되어 Client A/B의 OnRep와 UI를 갱신한다.

요청자만 알아야 하는 거부 이유는 A의 PlayerController Client RPC로 보낸다.

늦은 접속자는 과거 RPC가 아니라 GameState와 PlayerState의 현재 복제값으로 화면을 복구한다.

Dedicated Server에는 UI가 없으며, 서버 판정 로그와 Client A/B의 OnRep·개인 피드백 로그를 분리해 전체 흐름을 검증한다.

이 설명에서 함수 이름이 바뀌어도 실행 PC·원본 데이터·전달 방법·검증 조건을 유지할 수 있으면 다른 멀티플레이 기능에도 적용할 수 있다.