증상
일부 드롭 파츠에 다가가도 프롬프트 위젯이 표시되지 않음. 다른 파츠는 정상.
원인
NSInteractionComponent::OnSphereBeginOverlap에서 겹침 시점에 CanInteract()를 검사해 후보 목록에서 제외했음.
드롭 파츠는 비동기로 데이터(StoredInstance)를 채우기 때문에, 겹침 이벤트가 먼저 도착하면 CanInteract = false → 후보에서 영구 제외.
해결
OnSphereBeginOverlap에서 CanInteract 검사 제거. 모든 INSInteractable 구현체를 즉시 후보에 추가.
UpdateActiveTarget(Tick)에서 CanInteract를 매 프레임 재검사해 활성 대상을 결정.
// NSInteractionComponent.cpp — BeginOverlap
// 이전: CanInteract 통과한 것만 추가
// 수정: 구현체면 무조건 추가, 유효성은 Tick에서 판단
if (OtherActor->Implements<UNSInteractable>())
{
Candidates.AddUnique(OtherActor);
}
증상
프롬프트 위젯이 파츠/NPC 위에 뜨지 않고 화면 한쪽 구석이나 허공에 표시됨.
원인 1 — 바운드 계산이 DetectionSphere 포함
ShowPromptFor에서 액터 바운드(GetActorBounds)를 기준으로 상단 Z를 계산했는데, USphereComponent 반경이 합산되어 높이가 수백 유닛 이상 튀었음.
원인 2 — 상의(Body) 파츠 메시 피벗 어긋남
SetupVisual에서 MeshComp를 Z 방향으로 보정해 이동했지만, DetectionCollision과 PromptAnchor는 루트에 고정된 채 그대로여서 보이는 메시와 위치가 달랐음.
해결
각 액터에 USceneComponent PromptAnchor를 추가하고, INSInteractable 인터페이스에 GetPromptWorldLocation() 메서드를 추가.
ShowPromptFor는 바운드 계산 대신 이 메서드 결과를 사용.
SetupVisual에서 메시 Z 보정 후 DetectionCollision과 PromptAnchor도 함께 재배치.
// NSDroppedPart.cpp — SetupVisual
const FVector MeshCenter(MeshBounds.Origin.X, MeshBounds.Origin.Y, MeshBounds.BoxExtent.Z);
if (DetectionCollision) { DetectionCollision->SetRelativeLocation(MeshCenter); }
if (PromptAnchor) { PromptAnchor->SetRelativeLocation(MeshCenter + FVector(0, 0, MeshBounds.BoxExtent.Z + 30.f)); }
증상
프롬프트는 뜨는데 F를 눌러도 파츠가 장착되지 않음. 아무리 가까이 붙어도 동일.
원인
TryPickup에서 GetActorLocation()(루트 위치)과 플레이어 위치 사이의 거리를 비교했는데,
SetupVisual이 MeshComp를 Z 보정해도 루트는 그대로라 실제 메시 위치와 루트 위치가 달랐음. 루트 기준 거리가 항상 임계값 초과.
해결
거리 계산 대신 DetectionCollision->IsOverlappingActor(InstigatorPawn) 사용.
프롬프트가 뜨는 조건(DetectionSphere 겹침)과 동일한 기준이므로 데드존 없음.
// NSDroppedPart.cpp — TryPickup
if (!DetectionCollision || !DetectionCollision->IsOverlappingActor(InstigatorPawn))
{
return;
}
NSInteractionComponent가 Controller에 붙어 있어 상호작용이 항상 실패증상
TryInteract 호출 시 항상 ActiveTarget이 없어서 아무것도 실행 안 됨.
OnSphereBeginOverlap도 한 번도 호출 안 됨.
원인
NSInteractionComponent를 NSPlayerController 생성자에서 만들었는데,
TryInteract가 Pawn에서 FindComponentByClass를 했고 Controller에 붙은 컴포넌트는 Pawn에서 검색 안 됨.
또한 DetectionSphere가 Controller에 붙어 있어 월드에 존재하지 않으므로 겹침 이벤트 자체가 발생하지 않았음.
해결
NSPlayerCharacterBase 생성자로 이동. Pawn에 붙어야 물리 위치를 가져 DetectionSphere가 정상 작동.
// NSPlayerCharacterBase.cpp — 생성자
InteractionComp = CreateDefaultSubobject<UNSInteractionComponent>(TEXT("InteractionComp"));
LogStreamableManager: RequestAsyncLoad() called with empty or only null assets! 경고증상
파츠 줍기(F)를 누를 때마다 로그 창에 경고 1줄씩 출력됨.
빈 공간에서 F를 누르면 경고 없음. 파츠 주울 때만 발생.
원인 추적 과정
1. 처음에는 GrantAbilities의 빈 배열 RequestAsyncLoad 의심 → 가드 추가했으나 경고 지속
2. [LoadHunt] 로그를 GrantAbilities와 ApplyPartEffect에 추가 → 경로는 유효하게 찍힘
3. GE/GA를 데이터 에셋에서 비워도 경고 발생 → GE/GA 아님 확인
4. 최종 원인: NSPartSlotButton::SetPart에서 PartIconImage->SetBrushFromSoftTexture(InPartDefinition->Icon) 호출 시 Icon이 None(미설정)이어서 내부적으로 빈 경로로 비동기 로드 요청 발생
해결
Icon.IsNull() 가드 추가.
// NSPartSlotButton.cpp — SetPart
if (!InPartDefinition->Icon.IsNull())
{
PartIconImage->SetBrushFromSoftTexture(InPartDefinition->Icon);
PartIconImage->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
PartIconImage->SetBrushFromTexture(nullptr);
PartIconImage->SetVisibility(ESlateVisibility::Hidden);
}
교훈
SetBrushFromSoftTexture는 내부에서 RequestAsyncLoad를 직접 호출함.
소프트 레퍼런스를 넘기기 전에 반드시 IsNull() 검사 필요.
DetectionRadius 값과 실제 스피어 반경이 에디터에서 동기화 안 됨증상
에디터 Details 패널에서 DetectionRadius를 바꿔도 스피어 시각화 크기가 즉시 반영되지 않음.
원인
생성자에서 SetSphereRadius(InteractRadius) 한 번만 호출. 에디터 수정 시 재호출 없음.
해결
OnConstruction에서 SetSphereRadius 재호출. 에디터에서 값 변경 시마다 OnConstruction이 재실행됨.
void ANSDroppedPart::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (DetectionCollision) { DetectionCollision->SetSphereRadius(InteractRadius); }
}
NPC 베이스(NSInteractableNPCBase)도 동일 패턴 적용.
_Implementation 빌드 에러 또는 미호출증상
UFUNCTION(Server, Reliable) 선언 후 빌드 에러 또는 런타임에 RPC가 실행 안 됨.
원인
UE5에서 Server RPC는 함수명이 반드시 Server_로 시작해야 UHT가 올바르게 처리함.
ServerRequestEquip처럼 접두사 없이 쓰면 _Implementation 생성이 의도대로 안 될 수 있음.
해결
Server_RequestEquip, Server_RequestPickup 등 Server_ 접두사로 통일.
Client_ RPC도 동일하게 Client_ 접두사 사용.
증상
2인 리슨 서버 환경. 클라이언트 플레이어가 상호작용 액터(NPC 또는 드롭 파츠)에 접근하면,
클라이언트 화면에는 정상적으로 프롬프트 위젯이 뜨지만 호스트 화면에도 동일 위젯이 표시됨.
클라 플레이어가 멀어지면 호스트 화면의 위젯도 사라짐.

원인
NSInteractionComponent::BeginPlay에서 모든 머신의 모든 폰에 대해 무조건 셋업을 실행했기 때문.
리슨 서버는 모든 플레이어 폰을 서버 프로세스 안에서 관리한다. 문제는 두 가지였다.
오버랩 델리게이트가 비로컬 폰에도 바인딩됨
BeginPlay에서 IsLocallyControlled() 검사 없이 DetectionSphere->OnComponentBeginOverlap.AddDynamic(...) 을 호출해, 호스트 위에서 실행되는 클라 폰의 NSInteractionComponent에도 델리게이트가 붙었다.
DetectionSphere 콜리전이 모든 폰에서 활성 상태
생성자에서 SetCollisionProfileName(TEXT("OverlapAllDynamic"))만 설정하고 콜리전 자체를 끄지 않아, 비로컬 폰의 스피어도 오버랩 이벤트를 발생시켰다.
결과적으로 호스트 위에서 돌아가는 클라 폰이 오버랩을 감지 → Candidates에 추가 → Tick에서 UpdateActiveTarget 실행 → 위젯을 호스트 화면에 표시하는 흐름이 발생했다.
UpdateActiveTarget 내부의 GetOwnerController() 가드(if (!PC))는 컨트롤러 유무만 보는데,
리슨 서버에서 원격 플레이어 폰도 서버 입장에서 GetController()가 non-null PlayerController를 반환하므로 이 가드는 원격 폰을 걸러내지 못한다.
해결
컴포넌트에 EnableLocalInteraction() 함수를 추가하고, 로컬 전용 셋업을 이 함수 하나로 집중. 비로컬·중복 호출은 즉시 리턴.
변경된 파일과 내용:
NSInteractionComponent.h
EnableLocalInteraction() public 함수 추가IsOwnerLocallyControlled() private 헬퍼 추가bLocalInteractionEnabled 중복 방지 플래그 추가NSInteractionComponent.cpp
생성자: DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision) 추가
→ 비로컬 폰의 스피어는 콜리전 쿼리 자체가 발생하지 않음
BeginPlay: 오버랩 바인딩 / 위젯 클래스 세팅 제거, EnableLocalInteraction() 호출로 대체
→ attach 등 머신 공통 셋업만 남음. 스탠드얼론/리슨 호스트 본인 폰은 이미 possess 돼 있어 여기서 활성화됨
EnableLocalInteraction() 구현:
void UNSInteractionComponent::EnableLocalInteraction()
{
if (bLocalInteractionEnabled) { return; }
if (!IsOwnerLocallyControlled()) { return; }
bLocalInteractionEnabled = true;
if (PromptWidgetClass) { PromptWidgetComponent->SetWidgetClass(PromptWidgetClass); }
DetectionSphere->OnComponentBeginOverlap.AddDynamic(this, &UNSInteractionComponent::OnSphereBeginOverlap);
DetectionSphere->OnComponentEndOverlap.AddDynamic(this, &UNSInteractionComponent::OnSphereEndOverlap);
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}
IsOwnerLocallyControlled() 구현: APawn::IsLocallyControlled() 사용
NSPlayerCharacterBase.h/.cpp
OnRep_Controller() 오버라이드 추가PossessedBy(서버/리슨 호스트 경로)와 OnRep_Controller(클라이언트 본인 폰 — 컨트롤러가 리플리케이션으로 도착하는 시점) 양쪽에서 EnableLocalInteraction() 호출OnRep_PlayerState가 아닌 OnRep_Controller를 진입점으로 쓴 이유: IsLocallyControlled()는 내부적으로 Controller에 의존하고, 클라이언트 본인 폰에는 Controller가 리플리케이트된다. BeginPlay 단독으로는 클라이언트에서 컨트롤러가 아직 null일 수 있어 불안정하다.
결과
| 경로 | 활성화 여부 |
|---|---|
| 스탠드얼론 / 리슨 호스트 본인 폰 | PossessedBy 또는 BeginPlay 폴백에서 활성 |
| 리슨 서버 위의 원격 플레이어 폰 | IsLocallyControlled() == false → 무시 |
| 클라이언트 본인 폰 | OnRep_Controller에서 활성 |

비로컬 폰의 DetectionSphere는 NoCollision 상태로 남아 오버랩 이벤트 자체가 발생하지 않으며, 오버랩 델리게이트 바인딩과 위젯 인스턴스 생성도 일어나지 않는다.