TIL - 파츠 시스템 테스트, 최적화

Kyu_·2026년 6월 12일

29주차

목록 보기
4/4

TIL 파츠 시스템 테스트, 최적화


파츠 시스템 PIE 테스트 통합

무엇을 했나

파츠 시스템(드랍 → 줍기 → 장착)의 전체 흐름을 PIE에서 실제로 테스트하기 위한 임시 코드를 추가했다. 몬스터 드랍 연동과 상호작용 시스템이 아직 없어서, 두 군데에 우회 경로를 만들었다.

  1. ANSDroppedPart — DebugDefinition 자동 Initialize

    • 레벨에 배치한 드랍 파츠 액터의 디테일 패널에서 DebugDefinition / DebugRarity / DebugValue를 지정하면 BeginPlay에서 자동으로 Initialize를 호출하도록 했다.
    • 원래 이 코드는 이전 세션에서 한 번 추가했다가 "테스트 코드 삭제" 커밋(a3054f42)으로 지웠는데, PIE 흐름을 확인하려면 다시 필요해서 재복원했다.
  2. UNSPartEquipComponentServerRequestPickup RPC 추가

    • H키로 픽업 테스트를 하려는데 TryPickup이 서버 전용 함수라 클라이언트 BP에서 직접 호출이 안 됐다.
    • 클라 → 서버로 넘겨주는 Server RPC ServerRequestPickup(ANSDroppedPart*)를 임시로 추가하고, BP_NS_PlayerCharacter에서 H키 이벤트에 바인딩했다.
  3. NSPlayerCharacterBaseif문 {} 누락 핫픽스

    • PossessedBy에서 HasAuthority() 블록에 중괄호가 없어 다음 줄 코드가 항상 실행되는 버그를 발견하고 수정했다. (커밋 4d370a6d)

어떤 어려움이 있었나

1. TryPickup은 서버 전용인데 BP는 클라에서 실행된다

제일 막혔던 부분. ANSDroppedPart::TryPickupHasAuthority() 가드가 있어서 클라이언트 컨텍스트에서 호출해도 조용히 무시된다. 처음엔 BP에서 직접 호출하면 되겠지 싶었는데 ListenServer 환경에서 호스트는 되고 클라는 안 돼서 원인 파악에 시간이 걸렸다.

2. DebugDefinition을 지웠는데 테스트가 안 된다

이미 "테스트 코드 삭제"를 커밋해서 없앤 코드인데, 막상 픽업 흐름을 테스트하려니 드랍 파츠에 데이터가 없어서 EquipPart가 early return으로 빠져나갔다. 삭제와 재추가를 두 번 반복한 셈이라 커밋 히스토리가 지저분해졌다.

3. H키 바인딩 위치 오파악

처음에 H키 바인딩이 레벨 BP에 있다고 잘못 안내했다. 실제로는 BP_NS_PlayerCharacter에 있었고, 이 차이로 에디터 작업 안내를 두 번 수정해야 했다.


무엇을 배웠나

서버 전용 함수는 클라이언트 테스트 경로를 따로 뚫어야 한다

HasAuthority() 가드가 붙은 함수는 멀티플레이어 환경에서 클라이언트가 절대 실행할 수 없다. PIE 테스트에서도 마찬가지다. 이 경우 선택지는 두 가지다:

  • Server RPC를 임시로 추가한다 (이번에 한 방식)
  • 테스트를 에디터 전용 호스트(플레이어 0번)로만 한다

임시 RPC 방식은 나중에 상호작용 시스템으로 교체할 예정이므로, 반드시 // 테스트용 임시 코드 블록으로 표시해두고 삭제 시점을 메모해둬야 나중에 놓치지 않는다.

테스트 코드는 삭제 시점을 미리 정하고 추가해야 한다

"일단 넣고 나중에 지우자"는 항상 두 번 일하게 만든다. 이번에도 한 번 지웠다가 다시 추가했고, 커밋 히스토리에 흔적이 남았다. 앞으로는:
1. 추가할 때 삭제 조건을 주석에 명시 (// 몬스터 드랍 연동 후 삭제)
2. 메모리/작업 큐에 "임시 코드 삭제" 항목을 바로 등록
3. 정식 흐름이 생기는 시점에 묶어서 한 번에 제거

if (HasAuthority()) 단문은 반드시 {}로 감싸야 한다

Unreal 코드베이스에서 HasAuthority() 블록은 한 줄처럼 보여도 나중에 줄이 추가될 가능성이 높다. 단문이어도 {} 없이 쓰면 이번처럼 다음 줄이 무조건 실행되는 버그가 생긴다. 제어문은 항상 {}를 쓰는 스타일을 지킬 것.


NSPartUtils: 중복 ResolveDefinition 공통 유틸 추출

무엇을 했나

NSPartEquipComponentNSPartVisualComponent 두 곳에 완전히 동일한 구현으로 존재하던 ResolveDefinitionNSPartUtils 네임스페이스 자유 함수로 추출했다. (커밋 7afabc66)

  • 추출 전: UNSPartEquipComponent::ResolveDefinition(const FNSPartData&) const (멤버 함수) / NSPartVisualComponent에도 동일 구현
  • 추출 후: NSPartUtils::ResolvePartDefinition(const UObject* WorldContextObject, const FNSPartData&)NSPartUtils.h/.cpp 신규 파일

멤버 함수가 아니라 자유 함수로 만든 이유는 UNSDataSubsystem::Get()WorldContext가 필요한데, UObject*를 파라미터로 받으면 어느 컴포넌트에서든 호출할 수 있기 때문이다. BlueprintFunctionLibrary는 BP 노출이 필요 없어서 사용하지 않았다.

이 커밋에서 DOREPLIFETIME_CONDITION(..., COND_OwnerOnly)DOREPLIFETIME으로도 변경됐는데, 파츠 슬롯이 오너 전용으로 복제되면 다른 플레이어 캐릭터에 파츠 비주얼이 표시되지 않는 문제를 해결하기 위해서였다.


어떤 어려움이 있었나

자유 함수 vs 멤버 함수 vs BlueprintFunctionLibrary 선택

처음엔 UNSPartDefinition을 반환하는 함수니까 멤버 함수 형태가 자연스럽게 느껴졌다. 하지만 멤버 함수로 두면 두 컴포넌트 중 어느 쪽에 소속시킬지 결정해야 하고, 한쪽에 의존성이 생기거나 또 중복이 발생한다. BlueprintFunctionLibrary는 BP 노출이 목적인데 이 함수는 서버 로직에서만 쓰이므로 과한 선택이었다.

결론적으로 namespace 자유 함수가 가장 작은 형태였지만, Unreal에서 namespace 함수를 NEOSANCTUM_API로 모듈 외부에 노출하는 패턴이 익숙하지 않아서 헤더 선언 형태를 한 번 고쳐야 했다.

유니티 빌드 이름 충돌 가능성

Unreal은 기본적으로 Unity Build(여러 .cpp를 하나로 묶어 컴파일)를 사용한다. 이 경우 서로 다른 파일에 같은 이름의 namespace나 로컬 심볼이 있으면 충돌이 날 수 있다. NSPartUtils라는 이름 앞에 프로젝트 접두사를 붙인 것도 이 때문이다.


무엇을 배웠나

중복 코드는 "어디에 두느냐"가 핵심이다

중복을 없애는 건 쉬운데, 합친 코드를 어디에 두느냐는 별개의 결정이다. Unreal에서 선택지는 보통 세 가지다:

형태언제
멤버 함수특정 클래스의 상태에 의존할 때
BlueprintFunctionLibraryBP에서도 쓸 때
namespace 자유 함수상태 의존 없이 순수 로직, C++만 쓸 때

이번 ResolvePartDefinitionWorldContext만 있으면 되고 BP 노출도 불필요해서 namespace 자유 함수가 맞았다.

COND_OwnerOnly는 비주얼 데이터에 쓰면 안 된다

DOREPLIFETIME_CONDITION(..., COND_OwnerOnly)은 오너 클라이언트에게만 복제한다. 파츠 장착 정보(EquippedParts)에 이걸 붙이면 내 화면에서는 장착 비주얼이 보이지만, 다른 플레이어 시점에서는 내 캐릭터가 빈 손으로 보인다. 비주얼에 영향을 주는 복제 프로퍼티는 DOREPLIFETIME(모든 클라이언트)을 써야 한다. 보안/트래픽 이유로 오너만 받아야 하는 건 인벤토리 수량, 자원 같은 private 데이터다.

0개의 댓글