🎮 구현 목표
서버에 접속한 플레이어들은 로비 레벨에서 모인 뒤, 게임 시작 시 SeamlessTravel로 플레이 레벨로 이동합니다. 이 과정에서 플레이어의 Username이 유지되어야 DB에 플레이 기록을 정상적으로 저장할 수 있습니다.
🚨 문제 상황
SeamlessTravel 후 닉네임이 공백으로 표시
플레이어 정보 저장 전략:
PlayerState: 닉네임(Username), PlayerSessionId (게임 내 공유 데이터)LocalPlayerSubsystem: AccessToken, Email (로컬 전용 데이터)// 로비 입장 시 (GameSessionsManager.cpp) const FString Options = "PlayerSessionId=" + PlayerSession.PlayerSessionId + "?Username=" + PlayerSession.PlayerId; UGameplayStatics::OpenLevel(this, Address, true, Options);
로비 레벨에서는 정상적으로 닉네임이 표시되지만, SeamlessTravel로 플레이 레벨 전환 후 닉네임이 초기화되는 문제 발생:
로비 레벨: 닉네임 "Kabu" ✅
↓ (SeamlessTravel)
플레이 레벨: 닉네임 "" (공백) ❌
연쇄 문제:
- 킬 로그에 닉네임 공백 표기 ❌
- DB에 플레이어 기록 저장 실패 (Username이 공백) ❌
💭 해결 방법
"왜 SeamlessTravel 후 데이터가 날아가지?"
처음엔 "레벨 전환은 자동으로 데이터를 유지해주는 거 아닌가?" 생각했지만, 테스트 결과 PlayerState의 커스텀 변수는 자동으로 복사되지 않음을 확인.
첫 번째 시도: PlayerState 클래스 통일
의심: "로비 레벨과 플레이 레벨의 PlayerState 클래스가 달라서 그런가?"
// Before
로비 레벨: DS_DefaultPlayerState (서버 모듈)
플레이 레벨: BP_MatchPlayerState (컨텐츠 모듈, DS_DefaultPlayerState 상속)
// After: 통일 시도
로비 레벨: BP_MatchPlayerState
플레이 레벨: BP_MatchPlayerState
결과: 여전히 초기화됨 ❌
상속받았어도 문제가 해결되지 않는 이유를 찾아야 했습니다.
두 번째 시도: SeamlessTravel 동작 원리 조사
UE5 공식 문서와 코드를 찾아보니, SeamlessTravel 시 PlayerState는:
1. 구 레벨에서 PlayerState 복사본 생성 (CopyProperties 호출)
2. 신 레벨로 PlayerState 이동
3. 신 레벨의 기존 PlayerState와 병합 (OverrideWith 호출)
CopyProperties()와 OverrideWith()를 오버라이드하지 않으면 커스텀 변수는 복사 안 됨!
해결: 수동 복사 구현
// DS_DefaultPlayerState.cpp
void ADS_DefaultPlayerState::CopyProperties(APlayerState* PlayerState)
{
Super::CopyProperties(PlayerState);
// 구 PlayerState → 신 PlayerState로 복사
if (ADS_DefaultPlayerState* NewPlayerState = Cast(PlayerState))
{
NewPlayerState->DefaultUsername = DefaultUsername;
NewPlayerState->DefaultPlayerSessionId = DefaultPlayerSessionId;
}
}
void ADS_DefaultPlayerState::OverrideWith(APlayerState* PlayerState)
{
Super::OverrideWith(PlayerState);
// 신 레벨의 임시 PlayerState → 실제 PlayerState로 덮어쓰기
if (ADS_DefaultPlayerState* OldPlayerState = Cast(PlayerState))
{
DefaultUsername = OldPlayerState->DefaultUsername;
DefaultPlayerSessionId = OldPlayerState->DefaultPlayerSessionId;
}
}
동작 흐름:
[로비 레벨]
PlayerState A (닉네임: "Kabu")
↓
1. CopyProperties() 호출
→ 임시 PlayerState B 생성 (닉네임: "Kabu" 복사)
↓
2. SeamlessTravel 시작
↓
[플레이 레벨]
PlayerState C (새로 생성, 닉네임: "")
↓
3. OverrideWith() 호출
→ PlayerState B의 데이터를 C에 덮어쓰기
→ 최종: PlayerState C (닉네임: "Kabu") ✅
🔧 시행착오
왜 PlayerState에 저장했는가?
처음엔 PlayerController에 닉네임을 저장했습니다. PlayerController는 SeamlessTravel 시 자동으로 데이터가 유지되므로 편리했습니다.
// 초기 구조 (잘 작동함)
class ADS_PlayerController
{
UPROPERTY(Replicated)
FString Username; // SeamlessTravel 후에도 유지됨 ✅
};
그런데 왜 PlayerState로 옮겼나?
설계 원칙상 "플레이어 게임 데이터는 PlayerState가 소유"하는 것이 맞다고 판단:
- PlayerController: 입력 처리, 카메라 제어 (제어 관련)
- PlayerState: 점수, 닉네임, 팀 정보 (게임 데이터)
실제 사용 케이스:
void UEliminationComponent::ProcessElimination(bool bHeadShot, AMatchPlayerState* AttackerPS, AMatchPlayerState* VictimPS)
{
AMatchGameState* GameState = Cast<AMatchGameState>(UGameplayStatics::GetGameState(AttackerPS));
if (IsValid(GameState))
{
HandleFirstBlood(GameState, SpecialElimType, AttackerPS);
UpdateLeaderStatus(GameState, SpecialElimType, AttackerPS, VictimPS);
// GameState->Multicast Deleaget Broadcast -> Widget Kill Feed Update
FKillInfo KillInfo;
KillInfo.KillerName = AttackerPS->GetUsername();
KillInfo.VictimName = VictimPS->GetUsername();
GameState->AnnounceKill(KillInfo);
}
}
Replicated 변수인데 왜 복사가 필요한가?
처음엔 "UPROPERTY(Replicated)로 선언했으니 자동 동기화 아닌가?" 생각했지만:
UPROPERTY(ReplicatedUsing = OnRep_Username)
FString DefaultUsername = "";
Replication ≠ SeamlessTravel 데이터 유지
- Replication: 서버 → 클라이언트 동기화
- SeamlessTravel: 구 레벨 → 신 레벨 데이터 이동 (별도 메커니즘!)
따라서 Replicated 변수여도 CopyProperties()/OverrideWith() 구현 필수.
✅ 결과
✅ 닉네임 정상 표시: SeamlessTravel 후에도 "Kabu" 유지
✅ DB 저장 성공: PlayerId가 공백이 아닌 실제 닉네임으로 저장
"SeamlessTravel은 PlayerState를 자동으로 복사하지 않는다"
언리얼의 PlayerState는 기본 프로퍼티(PlayerName, Score 등)만 자동 복사하고, 커스텀 변수는 개발자가 직접 CopyProperties()와 OverrideWith()를 구현해야 합니다.
CopyProperties(): 구 레벨에서 신 레벨로 데이터 복사OverrideWith(): 신 레벨의 임시 PlayerState를 실제 PlayerState에 병합
'프로젝트 회고' 카테고리의 다른 글
| [UE5 팀 프로젝트] Pull-Request 시행착오와 교훈 (0) | 2025.12.02 |
|---|---|
| [Dedicated Server] 콜백(Callback) 체인을 활용한 데이터 무결성 확보 (0) | 2025.12.02 |
| [Dedicated Server] 비동기 폴링 기반 서버 접속 시도 (0) | 2025.12.02 |
| [UE5 팀 프로젝트] 지연 초기화: 런타임 액터 스폰, BeginPlay 호출 전 DataTable 데이터 무결성 확보 (0) | 2025.12.02 |
| [UE5 액션] 델리게이트(Delegate)를 활용한 유연한 인벤토리 시스템 (0) | 2025.12.02 |
