TIL - 증강 트러블 슈팅, NPC상호작용 리팩토링

Kyu_·2026년 6월 25일

31주차

목록 보기
5/6

트러블 슈팅

증강의 선택가능 횟수가 다음 스테이지로 유지가 안되는 문제

증상

  • 스테이지 1에서 증강 추첨횟수가 남아있는 상태로 다음 스테이지로 넘어가면 증강 추첨횟수가 남아있지 않음
  • 고른 증강(인벤토리)은 정상적으로 이관되나, 아직 선택하지 않은 추첨대기횟수(PoolQueue)가 초기화되는 현상

관련 구조

UNSAugmentSelectionComponent (PlayerController에 붙음)
  ├── PoolQueue        : 대기 중인 추첨 풀 태그 목록 (서버 전용)
  ├── PendingCount     : PoolQueue.Num() 복제값 (클라이언트 UI 뱃지용)
  └── OnPendingCountChanged : UI 갱신 델리게이트
  • PendingCount는 SetPendingCount로만 변경이되고, OnRep_PedingCount로만 클라에 복제되는 형태

트러블 슈팅 과정

초기 가설 : 타이밍 문제

  • 가설 : OnRep_PendingCount가 위젯 NativeConstruct보다 먼저 발화되어 델리게이트 미바인딩 -> 소실?
  • 검증 방법 : OnRep_PendingCount와 NativeConstruct에 로그 추가
  • 결과 : NativeConstruct에서 이미 GetPendingCount를 델리게이트 구독하여 타이밍 문제가 아님

2단계 : PostSeamlessTravel 시점 확인

  • 검증 방법 : PostSeamlessTravel함수를 오버라이드해서 PoolQueue가 살아있는지 확인
  • 결과 : PostSeamlessTravel 시점에 PendingCOunt = 0 즉 데이터손실은 그 이전에 발생

3단계 : 데이터 소실 경로 탐색

Reset(), Server_Choose, SetPendingCount, CopyRunStateFrom, 생성자에 순차적으로 로그 추가

로그결과
[AugmentSelection::Reset]미호출
[Server_Choose]미호출
[SetPendingCount] 0으로 변경됨미호출
[AugmentSelectionComponent::Constructor]스테이지 전환 직후 호출됨
  • 결과 : SetPendingCount(0)이 안불리는데 PendingCount가 0 -> 컴포넌트가 새로 생성되어 기본값(0)으로 시작하는구나

4단계 : 포인터 비교로 컴포넌트 재생성 확인

  • 검증 방법 : EnqueueOfferPostSeamlessTravelthis포인터 로그 추가
[EnqueueOffer]       this=000002C9E94F4F80 PoolQueue=4   ← 스테이지1
[Constructor]        this=000002C9E94F3D80               ← 전환 직후 새 컴포넌트 생성
[PostSeamlessTravel] comp=000002C9E94F3D80 PendingCount=0 ← 새 컴포넌트라서 0
  • 결과 : 포인터가 다르다. SeamlessTravel시 새 컴포넌트 (새 PC)가 생성되고 있음이 거의 확실

5단계 : 근본 원인 확정 : AGameModeBase vs AGameMode

엔진소스를 뜯어서 GameModeBase와 GameMode를 보았다.

AGameMode::HandleSeamlessTravelPlayer의 경우

if (PC && PC->GetClass() != PCClassToSpawn)
{
    // 클래스가 다를 때만 새 PC spawn
    PC->SeamlessTravelTo(NewPC);
}
else
{
    // 클래스가 같으면 PC 객체 그대로 유지 ← 데이터 보존
}

클래스가 다를때만 새 PC를 스폰하고 같으면 유지하는 코드가 존재한다.

AGameModeBase::HandleSeamlessTravelPlayer의 경우

// 무조건 새 PC spawn — 클래스 비교 없음
APlayerController* NewPC = SpawnPlayerControllerCommon(...);
PC->SeamlessTravelTo(NewPC);  // ← 이전 PC에서 수동 이관해야 함

PC비교가없고 NewPC로 덮어씌움 결국 수동으로 데이터를 이관해주어야 한다

NeoSanctum의 경우 게임모드가 AGameModeBase를 상속받기 때문에 두가지 해결방안을 냈다.


해결 방안

1. PlayerState로 이관

AugmentSelectionComponent는 기존에 PlayerController에 장착되어있다. 결국에는 조작과 관련된 기능들이 있어서 PC에 ActorComponent로 달게 되었었고, SeamlessTravel시 해당문제는 단지 PC는 무조건 끝까지 살아있는 구조로 알고있었어서 이렇게 짠것도 있다. 이쪽 방안이 조금 더 깔끔한 구조라고 생각이 된다.

2. SeamlessTravelTo 오버라이드 (PC유지)

기존 PC에 달아놓는 방식에서 SeamlessTravelTo함수를 오버라이드하여 PC에서 복제하는 함수를 하나 만들고 새로운 PC가 생겼을때 데이터를 복제해주는 식으로도 짤 수 있다. 이쪽의 장점은 구현이 간단하고, 현재 다른 기능들은 정상작동하는데 굳이 PC에서 PlayerState로 옮겨야 하는 생각이 있었다. 그래서 결론적으로 이쪽으로 구현하게 되었다.

선택이유

위에서 작성한것처럼 우선 잘 작동하는데 굳이 건드렸다가 다른 버그가 터질수도 있다고 생각하여 PC에 두는 구조를 계속 이어가기로 하였다.


리팩토링

위젯을 npc에서 pc로 옮기는 작업

기존구조에서는 npc에서 위젯을 띄워주는 역할을하고 있었는데 프로젝트 구조상 PC에서 UI를 모두 관리하고 있는 구조였다. 그래서 잠재적인 버그 (PC쪽 UI와 겹쳐서 버그발생)을 미연에 방지하기 위해서 PC쪽으로 옮기기로 하였다.

기존 구조

NPO의 OnInteract함수에서 CreateWidget -> Widget->OpenForInteractor()로 NPC가 위젯 타입을 직접 알고 생성

변경 구조

NPC의 GetInteractionWidgetClass -> Type별 WidgetClass 반환, PlayerController에서 OpenInteractionWidget(NPC) -> CreateWidget -> OpenForInteractor

변경 후 이점

PC는 UNSNPCInteractionWidgetBase 타입만 알고, 실제로 UNSPetUpgradeWidget인지 UNSPartEquipWidget인지는 모름
위젯 클래스는 NPC에서 받아오고 열고닫는건 베이스 타입의 가상함수로 처리

덕분에 PC는 ActiveInteractionWidget하나로 어떤 NPC위젯이든 관리할 수 있다.

개념

Seamless Travel에서 살아남는 것 vs 사라지는 것

사라지는 것(새로 생성)

  • GameMode : 새 레벨에서 새로 생성
  • GameState : 새로 생성
  • Pawn : 새로 생성 (또는 리스폰)
  • HUD / 위젯 : 새 레벨에서 새로 생성

살아남는 것

  • PlayerController : 재사용
  • PlayerState : 재사용
    • 같은 PlayerState를 재사용하는것이 새 인스턴스를 생성한 뒤 CopyProperties()를 호출해서 데이터를 복사하는 방식을 사용한다.
    • 커스텀 데이터를 유지하고 싶으면 CopyProperties를 사용하는것 (우리가 했던 구조)
  • PC/PS에 붙어있는 ActorComponent : 재사용

으로 정리를 했는데....

변수등장

우선 언리얼 공식문서를 보면 Traveling Actor 목록에서 PC객체 자체가 들어있다고 한다. 근데 이게 AGameModeBase를 상속받냐 AGameMode를 상속받냐에 따라 다른데
왜 다른가 하면 둘의 HandleSeamlessTravelPlayer()의 구조가 조금 다르다.

HUD

NativeConstruct

위젯이 뷰포트에 추가되는 순간 호출

생성자와의 차이점은

호출 시점

  • 생성자 : 에디터/쿡 타임 포함, 객체 생성 시
  • NativeConstruct : AddToViewport() / AddToPlayerScreen() 호출 시

BP 변수 바인딩

  • 생성자 : 아직 없음
  • NativeConstruct : 완료(BindWidget 변수 사용 가능)

소유 PC

  • 생성자 : 없음
  • NativeConstruct : GetOwningPlayer() 작동

추후 작업

인런재화는 아웃런에 안들어감

인런 상점

인런에서 먹은 파츠 강화
증강 리롤

아웃런 상점

  • 각각 npc가 있는 구조

파츠 구매
공용 스킬
드론

0개의 댓글