기록일: 2026-06-22
브랜치: feature/NPC-Interaction
관련 파일:NSInteractionComponent,NSInteractable,NSInteractableNPCBase,NSPartsNPC,NSPetNPC,NSPartEquipWidget,NSPetUpgradeWidget,NSPlayerCharacterBase
[플레이어 폰]
└── NSInteractionComponent 감지 + 입력 처리 + RPC 담당
└── DetectionSphere 근접 오버랩 감지
└── PromptWidgetComponent 화면에 띄우는 상호작용 프롬프트
[NPC 액터]
└── ANSInteractableNPCBase NPC 공통 베이스 (ACharacter + INSInteractable)
└── ANSPartsNPC 파츠 장착 NPC
└── ANSPetNPC 펫 강화 NPC
① 플레이어가 NPC에 접근
DetectionSphere 오버랩 → Candidates 목록에 추가 → Tick 활성화
② Tick마다 UpdateActiveTarget() 실행
후보 중 가장 가까운 INSInteractable 대상 → ActiveTarget
해당 NPC의 PromptAnchor 위치에 프롬프트 위젯 표시
③ F키 입력 → TryInteract()
클라이언트에서 CanInteract 사전 검사 (반응성용)
→ Server_RequestInteract(Target) RPC 전송
④ 서버에서 Server_RequestInteract_Implementation 실행
CanInteract 재검증 (서버 권위)
→ 통과 시 Client_OnInteractApproved(Target) RPC 전송
⑤ 클라이언트에서 Client_OnInteractApproved_Implementation 실행
Execute_OnInteract(Target, PC) 호출
→ ANSPartsNPC or ANSPetNPC의 OnInteract_Implementation 실행
→ 해당 UI 위젯 생성 + AddToViewport
모든 상호작용 가능한 액터가 구현해야 하는 인터페이스.
| 함수 | 설명 | 실행 위치 |
|---|---|---|
CanInteract(PC) | 이 플레이어가 상호작용 가능한지 판단 | 클라(Tick 표시용) + 서버(검증용) |
OnInteract(PC) | 실제 상호작용 처리 (UI 오픈 등) | 클라이언트 |
GetPromptText() | 프롬프트에 표시할 텍스트 | 클라이언트 |
GetPromptWorldLocation() | 프롬프트 위젯이 뜰 월드 위치 | 클라이언트 |
Execute_OnInteract(Target, PC) 형태로 호출하는 이유: UHT가 자동 생성하는 정적 디스패처로, C++ 구현체든 BP 구현체든 올바른 쪽으로 알아서 디스패치한다.
ACharacter + INSInteractable 조합. 공통 로직 구현.
CanInteract_Implementation: PlayerState의 NSPlayerProgressComponent에서 IsNPCUnlocked(NPCId) 확인. NPCId는 에디터 Details → NPC 카테고리에서 설정.PromptText: 에디터 Details → NPC 카테고리에서 직접 입력.PromptAnchor: 프롬프트 위젯이 뜰 위치. 에디터에서 드래그로 조정 가능.DetectionCollision: NSInteractionComponent의 DetectionSphere가 오버랩 감지에 사용.리슨 서버에서 서버가 모든 플레이어 폰을 관리하기 때문에, 아무 처리 없이 두면 클라이언트 폰의 상호작용 UI가 호스트 화면에도 표시되는 문제가 생긴다.
해결: 로컬 전용 셋업(오버랩 바인딩 + 콜리전 활성화 + 위젯 클래스)을 EnableLocalInteraction() 하나로 집중하고, 비로컬 폰에서는 아무것도 하지 않는다.
생성자: DetectionSphere 콜리전 = NoCollision (기본 OFF)
EnableLocalInteraction() 호출 시점:
- BeginPlay → 스탠드얼론 / 리슨 호스트 폴백
- PossessedBy → 서버 / 리슨 호스트 경로
- OnRep_Controller → 클라이언트 본인 폰 경로 (컨트롤러 리플리케이션 도착 시)
IsLocallyControlled() == false → 즉시 리턴, 아무것도 안 함
| 폰 | 결과 |
|---|---|
| 스탠드얼론 / 리슨 호스트 본인 폰 | 활성 |
| 리슨 서버 위의 원격 플레이어 폰 | 비활성 (콜리전 OFF, 바인딩 없음, 위젯 없음) |
| 클라이언트 본인 폰 | OnRep_Controller에서 활성 |
오버랩 중인 후보가 없으면 Tick을 끈다. 후보가 생기면(BeginOverlap) 켜고, 전부 사라지면(EndOverlap) 다시 끈다.
| 단계 | 실행 위치 | 이유 |
|---|---|---|
| 오버랩 감지 + 프롬프트 표시 | 클라이언트 로컬 | UI는 로컬에서만 |
| CanInteract (Tick) | 클라이언트 로컬 | 프롬프트 표시 여부 판단용 |
| TryInteract → Server RPC | 클라→서버 | 서버에 검증 요청 |
| CanInteract (서버 검증) | 서버 | 실제 권위 검증 |
| OnInteract → UI 오픈 | 클라이언트 | UI는 서버에서 열 수 없음 |
| 함수 | 역할 |
|---|---|
OpenForInteractor(PC) | AddToViewport + 마우스 커서 표시 + GameAndUI 입력 모드 |
RequestClose() | 변경사항 있으면 저장 확인 다이얼로그, 없으면 즉시 닫기 |
CloseWidget() | RemoveFromParent + 마우스 숨김 + GameOnly 입력 모드 복구 |
닫기(ESC) 처리: NativeOnKeyDown에서 Escape 키 감지 → RequestClose() 호출 → FReply::Handled() 반환으로 이벤트 소비. PIE에서는 ESC가 세션 종료로 먼저 잡히므로 테스트 시 BP 닫기 버튼 사용.
| 파일 | 역할 |
|---|---|
Interaction/Component/NSInteractionComponent.h/.cpp | 감지 + RPC + 프롬프트 |
Interaction/Core/NSInteractable.h | 인터페이스 정의 |
Interaction/NPC/NSInteractableNPCBase.h/.cpp | NPC 공통 베이스 |
Interaction/NPC/NSPartsNPC.h/.cpp | 파츠 NPC |
Interaction/NPC/NSPetNPC.h/.cpp | 펫 NPC |
UI/Interaction/NSPartEquipWidget.h/.cpp | 파츠 장착 UI |
UI/Interaction/NSPetUpgradeWidget.h/.cpp | 펫 강화 UI |
UI/Interaction/NSInteractionPromptWidget.h/.cpp | 머리 위 프롬프트 위젯 |
Character/Player/NSPlayerCharacterBase.h/.cpp | InteractionComp 소유, PossessedBy/OnRep_Controller 훅 |