UInteractionSystem & UInteractableComponent 기술 리포트
문서 정보
- 작성일: 2025-11-23
- 대상 시스템: Onepiece 프로젝트 Interaction System
- 관련 파일:
Source/Onepiece/Character/Public/UInteractionSystem.hSource/Onepiece/Character/Private/UInteractionSystem.cppSource/Onepiece/Interactable/Public/InteractableComponent.hSource/Onepiece/Interactable/Private/InteractableComponent.cpp
목차
시스템 개요
목적
플레이어와 환경 객체 간의 상호작용을 제공하는 범용 시스템으로, Overlap 감지 + LineTrace 타겟팅 방식을 사용하여 정밀한 상호작용을 구현합니다.
핵심 특징
- 이중 감지 시스템: Overlap으로 근접 감지 → LineTrace로 정확한 타겟 확정
- 타입별 상호작용: PickUp, Button 등 다양한 상호작용 타입 지원
- 확장 가능한 설계: 새로운 상호작용 타입 추가 용이
- 멀티플레이어 지원: Replication 기반 네트워크 동기화
시스템 구성 요소
- UInteractionSystem: 플레이어에 부착되어 상호작용을 감지하고 처리
- UInteractableComponent: 상호작용 가능한 객체에 부착되는 컴포넌트
- Interactable Actors: luggage, PedestalSwitch, WeightSwitch, Door 등 구체적인 상호작용 객체
아키텍처 설계
시스템 다이어그램
┌─────────────────────────────────────────────────────────────┐
│ Player Actor │
│ ┌────────────────────────────────────────────────────┐ │
│ │ UInteractionSystem │ │
│ │ - CurrentTarget │ │
│ │ - HoldingInteractable │ │
│ │ - NearbyInteractables[] │ │
│ │ │ │
│ │ Methods: │ │
│ │ + TryInteract() │ │
│ │ + TryPickUp() │ │
│ │ + TryDrop() │ │
│ │ + RegisterInteractable() │ │
│ │ + UnregisterInteractable() │ │
│ │ - DetectInteractableTarget() │ │
│ │ - PerformCenterLineTrace() │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ (Overlap Events)
┌─────────────────────────────────────────────────────────────┐
│ Interactable Objects │
│ ┌────────────────────────────────────────────────────┐ │
│ │ UInteractableComponent │ │
│ │ - InteractionType (PickUp/Button) │ │
│ │ - DetectionRange (BoxComponent) │ │
│ │ - bCanInteract │ │
│ │ - HoldingOwner │ │
│ │ │ │
│ │ Methods: │ │
│ │ + PickUp() / Drop() │ │
│ │ + TriggerInteraction() │ │
│ │ + ShowDebugInfo() │ │
│ │ - OnDetectionBeginOverlap() │ │
│ │ - OnDetectionEndOverlap() │ │
│ │ │ │
│ │ Delegate: │ │
│ │ • OnInteractionTriggered │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ luggage │ │PedestalSwitch│ │WeightSwitch │ ... │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
데이터 흐름
1. 근접 감지 (Overlap)
Interactable Object → DetectionRange Overlap → Player
→ RegisterInteractable() → NearbyInteractables[] 추가
2. 타겟팅 (LineTrace - 매 프레임)
UInteractionSystem::TickComponent()
→ DetectInteractableTarget()
→ PerformCenterLineTrace() (화면 중앙)
→ NearbyInteractables 내에서 Hit된 객체 확정
→ CurrentTarget 설정
3. 상호작용 실행
Player Input → TryInteract()
→ InteractionType 확인
- PickUp: TryPickUp() → PickUp() → Attach to HoldPosition
- Button: TriggerInteraction() → OnInteractionTriggered 델리게이트
4. 범위 이탈
Player leaves DetectionRange
→ OnDetectionEndOverlap()
→ UnregisterInteractable()
→ NearbyInteractables[]에서 제거
핵심 컴포넌트 상세
UInteractionSystem
위치: Source/Onepiece/Character/
부착 대상: APlayerActor
주요 속성
// 설정
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float InteractionDistance = 1200.0f; // LineTrace 최대 거리
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bShowDebugInfo = true; // 디버그 표시 여부
// 상태
UPROPERTY(BlueprintReadOnly)
TObjectPtr CurrentTarget; // 현재 타겟팅 중인 객체
UPROPERTY(BlueprintReadOnly)
TObjectPtr HoldingInteractable; // 들고 있는 객체
// 내부 데이터
UPROPERTY()
TArray> NearbyInteractables; // Overlap된 객체들
핵심 메서드
1. DetectInteractableTarget()
UInteractableComponent* UInteractionSystem::DetectInteractableTarget()
- 목적: 화면 중앙 LineTrace로 타겟 감지
- 로직:
- NearbyInteractables가 비어있으면 nullptr 반환
- 화면 중앙에서 LineTrace 실행
- Hit된 Actor에서 InteractableComponent 검색
- NearbyInteractables에 포함된 객체만 반환 (범위 검증)
2. PerformCenterLineTrace()
bool UInteractionSystem::PerformCenterLineTrace(FHitResult& OutHit)
- 목적: 화면 중앙에서 월드 공간으로 Ray 발사
구현 세부사항:
// 뷰포트 크기 가져오기 int32 ViewportSizeX, ViewportSizeY; PC->GetViewportSize(ViewportSizeX, ViewportSizeY); // 화면 중앙 픽셀 좌표 float ScreenX = ViewportSizeX * 0.5f; float ScreenY = ViewportSizeY * 0.5f; // 화면 → 월드 변환 FVector WorldLocation, WorldDirection; PC->DeprojectScreenPositionToWorld(ScreenX, ScreenY, WorldLocation, WorldDirection); // Ray Trace FVector TraceStart = WorldLocation; FVector TraceEnd = TraceStart + (WorldDirection * InteractionDistance); return GetWorld()->LineTraceSingleByChannel(OutHit, TraceStart, TraceEnd, ECC_Visibility);
3. TryInteract()
void UInteractionSystem::TryInteract()
- 목적: 상호작용 타입별 자동 처리
로직:
if (!CurrentTarget || !CurrentTarget->bCanInteract) return; switch (CurrentTarget->InteractionType) { case EInteractionType::PickUp: TryPickUp(); break; case EInteractionType::Button: CurrentTarget->TriggerInteraction(OwnerPlayer); break; }
4. TryPickUp()
void UInteractionSystem::TryPickUp()
중요한 구현 디테일:
// PickUp() 호출 **전**에 HoldingInteractable에 저장 // 이유: PickUp() 과정에서 DetachFromActor로 인해 // Overlap이 해제되고 CurrentTarget이 nullptr이 될 수 있음 HoldingInteractable = CurrentTarget; HoldingInteractable->HoldingOwner = OwnerPlayer; HoldingInteractable->PickUp();
5. RegisterInteractable() / UnregisterInteractable()
void UInteractionSystem::RegisterInteractable(UInteractableComponent* Interactable)
void UInteractionSystem::UnregisterInteractable(UInteractableComponent* Interactable)
- 목적: Overlap 이벤트로 호출되어 NearbyInteractables 배열 관리
- 안전성: 유효성 검사 + 중복 방지
UInteractableComponent
위치: Source/Onepiece/Interactable/
부착 대상: 상호작용 가능한 모든 Actor
상호작용 타입
UENUM(BlueprintType)
enum class EInteractionType : uint8
{
None,
PickUp, // 집어올리기 (luggage)
Button, // 버튼 누르기 (PedestalSwitch)
};
주요 속성
// 상호작용 설정
UPROPERTY(EditAnywhere, BlueprintReadWrite)
EInteractionType InteractionType = EInteractionType::PickUp;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString InteractionPrompt = TEXT("Press E to Interact");
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr DetectionRange; // 자동 생성됨
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float DetectionDistance = 150.0f; // 감지 범위 (cm)
// 상태
UPROPERTY(BlueprintReadOnly)
bool bCanInteract = true;
UPROPERTY(ReplicatedUsing=OnRep_HoldingOwner)
AActor* HoldingOwner; // 현재 들고 있는 플레이어
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
bool bIsPickedUp = false;
// 델리게이트
UPROPERTY(BlueprintAssignable)
FOnInteractionTriggered OnInteractionTriggered;
핵심 메서드
1. BeginPlay() - DetectionRange 자동 생성
void UInteractableComponent::BeginPlay()
{
// Owner Actor에 BoxComponent 생성
DetectionRange = NewObject(Owner, ...);
DetectionRange->RegisterComponent();
DetectionRange->AttachToComponent(Owner->GetRootComponent(), ...);
// DetectionDistance 기반으로 크기 설정
FVector BoxExtent(DetectionDistance, DetectionDistance, DetectionDistance);
DetectionRange->SetBoxExtent(BoxExtent);
// Overlap 이벤트만 감지
DetectionRange->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
DetectionRange->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// 콜백 바인딩
DetectionRange->OnComponentBeginOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionBeginOverlap);
DetectionRange->OnComponentEndOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionEndOverlap);
}
2. OnDetectionBeginOverlap()
void UInteractableComponent::OnDetectionBeginOverlap(...)
{
ACharacter* Character = Cast(OtherActor);
if (Character && Character->IsPlayerControlled())
{
// InteractionSystem 찾기 및 등록
UInteractionSystem* InteractionSystem = Character->FindComponentByClass();
if (InteractionSystem)
{
InteractionSystem->RegisterInteractable(this);
}
}
}
3. PickUp() / Drop() - 네트워크 동기화
void UInteractableComponent::PickUp()
{
GetOwner()->SetOwner(HoldingOwner);
Server_PickUp(); // RPC 호출
}
void UInteractableComponent::Server_PickUp_Implementation()
{
if (bIsPickedUp) return;
UPrimitiveComponent* PrimComp = GetOwnerPrimitiveComponent();
// 원래 상태 저장
bOriginalSimulatePhysics = PrimComp->IsSimulatingPhysics();
OriginalCollisionType = PrimComp->GetCollisionEnabled();
// 물리 비활성화
PrimComp->SetSimulatePhysics(false);
// 서버에서 Attach
if (HoldingOwner->HasAuthority())
{
OnRep_HoldingOwner();
}
bIsPickedUp = true;
}
void UInteractableComponent::OnRep_HoldingOwner()
{
if (HoldingOwner)
{
APlayerActor* MyPlayer = Cast(HoldingOwner);
GetOwner()->AttachToComponent(MyPlayer->HoldPosition, ...);
}
else
{
GetOwner()->DetachFromActor(...);
}
}
4. TriggerInteraction() - 델리게이트 기반
void UInteractableComponent::TriggerInteraction(AActor* Interactor)
{
if (!bCanInteract || !Interactor) return;
// 델리게이트 브로드캐스트
OnInteractionTriggered.Broadcast(Interactor);
}
동작 메커니즘
전체 흐름도
[1단계: 초기화]
Interactable Actor Spawn
└─> UInteractableComponent::BeginPlay()
└─> DetectionRange (BoxComponent) 자동 생성
└─> Overlap 콜백 바인딩
[2단계: 근접 감지]
Player enters DetectionRange
└─> OnDetectionBeginOverlap() 호출
└─> UInteractionSystem::RegisterInteractable()
└─> NearbyInteractables[] 배열에 추가
[3단계: 타겟팅 (매 프레임)]
UInteractionSystem::TickComponent()
└─> DetectInteractableTarget()
├─> NearbyInteractables 비어있음? → nullptr 반환
└─> PerformCenterLineTrace()
├─> 화면 중앙에서 Ray 발사
├─> Hit된 Actor에서 InteractableComponent 찾기
└─> NearbyInteractables에 포함됨? → CurrentTarget 설정
[4단계: 상호작용 실행]
Player Input (E 키)
└─> UInteractionSystem::TryInteract()
└─> CurrentTarget->InteractionType 확인
├─> PickUp: TryPickUp()
│ └─> HoldingInteractable = CurrentTarget
│ └─> PickUp() → Server_PickUp()
│ └─> Attach to HoldPosition
└─> Button: TriggerInteraction()
└─> OnInteractionTriggered.Broadcast()
[5단계: 범위 이탈]
Player leaves DetectionRange
└─> OnDetectionEndOverlap()
└─> UInteractionSystem::UnregisterInteractable()
├─> NearbyInteractables[]에서 제거
└─> CurrentTarget == this? → CurrentTarget = nullptr
주요 시나리오별 상세 동작
시나리오 1: 물건 들기 (PickUp)
[준비 단계]
1. Player가 luggage 근처로 접근
2. luggage의 DetectionRange Overlap 발생
3. luggage의 InteractableComponent가 UInteractionSystem에 등록됨
→ NearbyInteractables[luggage]
[타겟팅]
4. 매 프레임 화면 중앙에서 LineTrace 실행
5. luggage가 Hit되면 CurrentTarget = luggage
[상호작용]
6. Player가 E 키 입력
7. TryInteract() → TryPickUp() 호출
8. HoldingInteractable = luggage (중요: PickUp 호출 전)
9. luggage->PickUp() 호출
a. GetOwner()->SetOwner(Player)
b. Server_PickUp() RPC 호출
[서버 실행]
10. Server_PickUp_Implementation()
a. 물리 상태 저장 (bOriginalSimulatePhysics, OriginalCollisionType)
b. SetSimulatePhysics(false)
c. OnRep_HoldingOwner() → AttachToComponent(HoldPosition)
d. bIsPickedUp = true
[결과]
11. luggage가 Player의 HoldPosition에 부착됨
12. DetachFromActor로 인해 Overlap 해제 → UnregisterInteractable 호출됨
(하지만 HoldingInteractable에는 여전히 저장되어 있음)
시나리오 2: 버튼 누르기 (Button)
[준비 단계]
1. Player가 PedestalSwitch 근처로 접근
2. DetectionRange Overlap → 등록
[타겟팅]
3. 화면 중앙 LineTrace → CurrentTarget = PedestalSwitch
[상호작용]
4. E 키 입력 → TryInteract()
5. InteractionType이 Button이므로 TriggerInteraction() 호출
6. OnInteractionTriggered.Broadcast(Player)
[델리게이트 처리]
7. PedestalSwitch::OnInteractionTriggered() 콜백 실행
a. AnimBlueprint->ChangeState(true) - 버튼 눌림 애니메이션
b. Timer 설정 (RecoveryDelay)
c. OnActivate() 호출 (Blueprint 이벤트)
[타이머 종료]
8. RecoveryButton() 호출
→ AnimBlueprint->ChangeState(false) - 버튼 복원
Interactable 객체 구현
1. luggage (PickUp 타입)
파일: Source/Onepiece/Interactable/luggage.h/cpp
특징
- 타입: EInteractionType::PickUp
- 물리: 시뮬레이션 활성화 (무게 5kg)
- 네트워크: Replication 지원
구현
Aluggage::Aluggage()
{
// Mesh 설정
Mesh = CreateDefaultSubobject(TEXT("Mesh"));
SetRootComponent(Mesh);
// InteractableComponent 생성
InteractableComp = CreateDefaultSubobject(TEXT("Interactable"));
InteractableComp->InteractionType = EInteractionType::PickUp;
InteractableComp->InteractionPrompt = TEXT("Press E to Grab");
// 물리 설정
Mesh->SetSimulatePhysics(true);
Mesh->SetEnableGravity(true);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Mesh->SetCollisionProfileName(TEXT("PhysicsActor"));
Mesh->SetMassOverrideInKg(NAME_None, 5.f, true);
// Replication
bReplicates = true;
SetReplicateMovement(true);
}
사용 방법
- 레벨에 배치
- 자동으로 DetectionRange 생성됨
- Player가 접근하여 E 키로 들기/놓기
2. PedestalSwitch (Button 타입)
파일: Source/Onepiece/Interactable/APedestalSwitch.h/cpp
특징
- 타입: EInteractionType::Button
- 동작: 버튼 누르기 → 애니메이션 → 자동 복원
- 확장성: OnActivate() Blueprint 이벤트로 동작 커스터마이징
구현
APedestalSwitch::APedestalSwitch()
{
SwitchBody = CreateDefaultSubobject(TEXT("SwitchBody"));
InteractableComp = CreateDefaultSubobject(TEXT("Interactable"));
InteractableComp->InteractionType = EInteractionType::Button;
InteractableComp->InteractionPrompt = TEXT("Press E to Activate");
}
void APedestalSwitch::BeginPlay()
{
Super::BeginPlay();
InitSwitch();
// 델리게이트 바인딩
InteractableComp->OnInteractionTriggered.AddDynamic(this, &APedestalSwitch::OnInteractionTriggered);
}
void APedestalSwitch::OnInteractionTriggered(AActor* Interactor)
{
if (AnimBlueprint)
{
// 버튼 눌림
AnimBlueprint->ChangeState(true);
// 자동 복원 타이머
GetWorld()->GetTimerManager().SetTimer(
RecoveryTimerHandle,
this,
&APedestalSwitch::RecoveryButton,
RecoveryDelay,
false
);
}
OnActivate(); // Blueprint 이벤트
}
확장 예시
Blueprint에서 OnActivate 이벤트를 구현하여 동작 정의:
OnActivate 이벤트
└─> 문 열기
└─> 퍼즐 진행
└─> 사운드/VFX 재생
3. WeightSwitch (압력판 타입)
파일: Source/Onepiece/Interactable/AWeightSwitch.h/cpp
특징
- 감지 방식: Overlap (InteractableComponent가 아닌 자체 BoxComponent 사용)
- 동작: 물체가 올라가면 활성화, 내려가면 비활성화
- 브로드캐스트: UBroadcastManager를 통한 전역 이벤트 발송
구현
AWeightSwitch::AWeightSwitch()
{
SwitchBody = CreateDefaultSubobject(TEXT("SwitchBody"));
SwitchCollision = CreateDefaultSubobject(TEXT("SwitchCollision"));
SwitchCollision->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
SwitchCollision->SetCollisionResponseToChannel(ECC_PhysicsBody, ECR_Overlap);
}
void AWeightSwitch::BeginPlay()
{
Super::BeginPlay();
SwitchCollision->OnComponentBeginOverlap.AddDynamic(this, &AWeightSwitch::OnBeginOverlap);
SwitchCollision->OnComponentEndOverlap.AddDynamic(this, &AWeightSwitch::OnEndOverlap);
UBroadcastManager::Get(GetWorld())->OnWeightSwitch.AddDynamic(this, &AWeightSwitch::OnWeightSwitch);
}
void AWeightSwitch::OnBeginOverlap(...)
{
OverlappingActors.AddUnique(OtherActor);
if (OverlappingActors.Num() == 1)
{
DetectTarget = true;
ElapsedTime = 0.0;
}
}
void AWeightSwitch::Tick(float DeltaTime)
{
if (!DetectTarget) return;
ElapsedTime += DeltaTime;
if (ActivateTrigger()) // TriggerDelay 경과
{
UBroadcastManager::Get(GetWorld())->SendWeightSwitch(ButtonIndex, true);
}
}
void AWeightSwitch::OnWeightSwitch(int InButtonIndex, bool InActive)
{
if (InButtonIndex != ButtonIndex) return;
SetActivate(InActive); // 애니메이션 + 머티리얼 변경
OnActivate(InActive); // Blueprint 이벤트
}
특징 정리
- 여러 물체 감지: OverlappingActors 배열로 관리
- 딜레이 트리거: TriggerDelay 시간 동안 물체가 올라가 있어야 활성화
- 시각적 피드백: EmissiveMaterial로 색상 변경 (DeactivateColor ↔ ActivateColor)
4. Door (수신 전용 객체)
파일: Source/Onepiece/Interactable/ADoor.h/cpp
특징
- InteractableComponent 없음: 직접 상호작용 불가
- 브로드캐스트 수신: UBroadcastManager::OnDoorMessage 리스닝
- 퍼즐 메커니즘: ReqCount만큼 활성화 신호를 받아야 열림
구현 개념
void ADoor::BeginPlay()
{
Super::BeginPlay();
// 브로드캐스트 리스닝
UBroadcastManager::Get(GetWorld())->OnDoorMessage.AddDynamic(this, &ADoor::OnDoorMessage);
}
void ADoor::OnDoorMessage(int32 InDoorIndex, bool bInOpen)
{
if (InDoorIndex != DoorIndex) return;
if (bInOpen)
{
CurCount++;
if (CurCount >= ReqCount)
{
OpenDoor();
}
}
else
{
CurCount--;
if (CurCount < ReqCount)
{
CloseDoor();
}
}
}
확장 가이드
새로운 상호작용 타입 추가
1단계: EInteractionType에 타입 추가
InteractableComponent.h:
UENUM(BlueprintType)
enum class EInteractionType : uint8
{
None,
PickUp,
Button,
Examine, // 새로운 타입 추가
};
2단계: UInteractionSystem에 처리 로직 추가
UInteractionSystem.cpp:
void UInteractionSystem::TryInteract()
{
if (!CurrentTarget || !CurrentTarget->bCanInteract)
return;
switch (CurrentTarget->InteractionType)
{
case EInteractionType::PickUp:
TryPickUp();
break;
case EInteractionType::Button:
CurrentTarget->TriggerInteraction(OwnerPlayer);
break;
case EInteractionType::Examine: // 새로운 처리
TryExamine();
break;
}
}
void UInteractionSystem::TryExamine()
{
if (!CurrentTarget) return;
// 검사 UI 표시, 카메라 줌 등
CurrentTarget->TriggerInteraction(OwnerPlayer);
}
3단계: 새로운 Interactable Actor 생성
UCLASS()
class ONEPIECE_API AExaminableObject : public AActor
{
GENERATED_BODY()
public:
AExaminableObject()
{
Mesh = CreateDefaultSubobject(TEXT("Mesh"));
SetRootComponent(Mesh);
InteractableComp = CreateDefaultSubobject(TEXT("Interactable"));
InteractableComp->InteractionType = EInteractionType::Examine;
InteractableComp->InteractionPrompt = TEXT("Press E to Examine");
}
void BeginPlay() override
{
Super::BeginPlay();
// 델리게이트 바인딩
InteractableComp->OnInteractionTriggered.AddDynamic(this, &AExaminableObject::OnExamine);
}
UFUNCTION()
void OnExamine(AActor* Interactor)
{
// 검사 로직
UE_LOG(LogTemp, Log, TEXT("Examining object"));
}
private:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere)
UInteractableComponent* InteractableComp;
};
복잡한 상호작용 체인 구성
예시: 퍼즐 문 시스템
[시나리오]
3개의 PedestalSwitch를 모두 눌러야 문이 열림
[구성]
PedestalSwitch A (ButtonIndex = 0)
└─> OnActivate() → SendDoorMessage(0, true)
PedestalSwitch B (ButtonIndex = 1)
└─> OnActivate() → SendDoorMessage(0, true)
PedestalSwitch C (ButtonIndex = 2)
└─> OnActivate() → SendDoorMessage(0, true)
Door (DoorIndex = 0, ReqCount = 3)
└─> OnDoorMessage(0, true) → CurCount++
└─> if (CurCount >= 3) → OpenDoor()
Blueprint 구현 예시
PedestalSwitch_BP:
OnActivate 이벤트
└─> Get BroadcastManager
└─> SendDoorMessage
- DoorIndex: 0
- bOpen: true
Door_BP:
OnDoorMessage 델리게이트
└─> if (InDoorIndex == DoorIndex)
└─> CurCount++
└─> if (CurCount >= ReqCount)
└─> OpenDoor (Timeline Animation)
Best Practices
1. 네트워크 동기화
PickUp 타입 객체
// Actor 클래스
bReplicates = true;
SetReplicateMovement(true);
// InteractableComponent
SetIsReplicatedByDefault(true);
UPROPERTY(ReplicatedUsing=OnRep_HoldingOwner)
AActor* HoldingOwner;
// GetLifetimeReplicatedProps 구현
DOREPLIFETIME(UInteractableComponent, HoldingOwner);
RPC 호출
// Client → Server
void PickUp()
{
GetOwner()->SetOwner(HoldingOwner);
Server_PickUp(); // RPC
}
UFUNCTION(Server, Reliable)
void Server_PickUp();
void Server_PickUp_Implementation()
{
// 서버에서만 실행되는 로직
if (HoldingOwner->HasAuthority())
{
OnRep_HoldingOwner();
}
}
2. 유효성 검사
UInteractionSystem에서
void UInteractionSystem::TryPickUp()
{
// 유효성 검사 강화
if (!IsValid(CurrentTarget))
{
CurrentTarget = nullptr;
return;
}
AActor* TargetOwner = CurrentTarget->GetOwner();
if (!TargetOwner || !IsValid(TargetOwner))
{
return;
}
// 로직 실행
...
}
UInteractableComponent에서
void UInteractableComponent::RegisterInteractable(UInteractableComponent* Interactable)
{
if (!Interactable || !IsValid(Interactable))
{
return;
}
if (NearbyInteractables.Contains(Interactable))
return;
NearbyInteractables.Add(Interactable);
}
3. 중요한 순서 보장
PickUp 시 HoldingInteractable 설정 타이밍
// ❌ 잘못된 구현
void TryPickUp()
{
CurrentTarget->PickUp();
HoldingInteractable = CurrentTarget; // 너무 늦음!
}
// ✅ 올바른 구현
void TryPickUp()
{
HoldingInteractable = CurrentTarget; // PickUp 호출 **전**
HoldingInteractable->PickUp();
}
이유: PickUp() 과정에서 DetachFromActor가 호출되면 Overlap이 해제되고,OnDetectionEndOverlap → UnregisterInteractable → CurrentTarget = nullptr이 됨.
4. DetectionRange 자동 생성
BeginPlay에서 생성
void UInteractableComponent::BeginPlay()
{
Super::BeginPlay();
AActor* Owner = GetOwner();
if (!Owner) return;
// DetectionRange가 없으면 자동 생성
if (!DetectionRange)
{
DetectionRange = NewObject(Owner, ...);
DetectionRange->RegisterComponent();
DetectionRange->AttachToComponent(Owner->GetRootComponent(), ...);
// 크기 설정
FVector BoxExtent(DetectionDistance, DetectionDistance, DetectionDistance);
DetectionRange->SetBoxExtent(BoxExtent);
// Collision 설정
DetectionRange->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
DetectionRange->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// 콜백 바인딩
DetectionRange->OnComponentBeginOverlap.AddDynamic(...);
DetectionRange->OnComponentEndOverlap.AddDynamic(...);
}
}
5. 디버그 시각화
UInteractionSystem
void UInteractionSystem::TickComponent(...)
{
CurrentTarget = DetectInteractableTarget();
if (bShowDebugInfo && CurrentTarget)
{
CurrentTarget->ShowDebugInfo(OwnerPlayer);
}
}
bool UInteractionSystem::PerformCenterLineTrace(FHitResult& OutHit)
{
bool bHit = GetWorld()->LineTraceSingleByChannel(...);
if (bShowDebugInfo)
{
DrawDebugLine(
GetWorld(), TraceStart, TraceEnd,
bHit ? FColor::Green : FColor::Red,
false, 0.0f, 0, 0.5f
);
}
return bHit;
}
UInteractableComponent
void UInteractableComponent::ShowDebugInfo(AActor* ViewerActor)
{
FVector DebugLocation = GetOwner()->GetActorLocation() + FVector(0, 0, 100);
DrawDebugString(
GetWorld(),
DebugLocation,
InteractionPrompt,
nullptr,
bIsPickedUp ? FColor::Yellow : FColor::Green,
0.0f,
true,
1.2f
);
}
6. 델리게이트 패턴 활용
선언
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractionTriggered, AActor*, Interactor);
UPROPERTY(BlueprintAssignable)
FOnInteractionTriggered OnInteractionTriggered;
바인딩
void APedestalSwitch::BeginPlay()
{
Super::BeginPlay();
InteractableComp->OnInteractionTriggered.AddDynamic(
this,
&APedestalSwitch::OnInteractionTriggered
);
}
브로드캐스트
void UInteractableComponent::TriggerInteraction(AActor* Interactor)
{
OnInteractionTriggered.Broadcast(Interactor);
}
사용 사례 요약
사용 사례 1: 물건 집어서 퍼즐 풀기
[시나리오]
luggage를 집어서 WeightSwitch 위에 올려놓으면 문이 열림
[흐름]
1. Player가 luggage를 E 키로 집음 (PickUp)
2. Player가 WeightSwitch 위로 이동
3. E 키로 luggage를 놓음 (Drop)
4. luggage가 WeightSwitch와 Overlap
→ WeightSwitch 활성화
→ SendWeightSwitch(ButtonIndex, true)
5. Door가 OnDoorMessage 수신
→ 문 열림
사용 사례 2: 연속 버튼 퍼즐
[시나리오]
3개의 PedestalSwitch를 순서대로 눌러야 문이 열림
[구현]
각 PedestalSwitch의 OnActivate()에서:
- 올바른 순서면 다음 단계 활성화
- 잘못된 순서면 리셋
Blueprint 로직으로 순서 검증 구현
사용 사례 3: 시간 제한 퍼즐
[시나리오]
WeightSwitch를 TriggerDelay 시간 동안 눌러야 활성화
[구현]
AWeightSwitch::Tick()에서:
- DetectTarget == true일 때만 ElapsedTime 증가
- TriggerDelay 경과 시 활성화
- 물체가 내려가면 리셋
문제 해결 가이드
문제 1: CurrentTarget이 null이 되는 경우
원인: PickUp() 호출 중 DetachFromActor로 Overlap 해제
해결:
// HoldingInteractable에 먼저 저장
HoldingInteractable = CurrentTarget;
HoldingInteractable->PickUp();
문제 2: 멀티플레이어에서 물건이 동기화되지 않음
원인: Replication 설정 누락
해결:
// Actor
bReplicates = true;
SetReplicateMovement(true);
// Component
SetIsReplicatedByDefault(true);
DOREPLIFETIME(UInteractableComponent, HoldingOwner);
문제 3: DetectionRange가 생성되지 않음
원인: Owner가 null이거나 BeginPlay 타이밍 문제
해결:
void UInteractableComponent::BeginPlay()
{
Super::BeginPlay();
AActor* Owner = GetOwner();
if (!Owner)
{
UE_LOG(LogTemp, Error, TEXT("Owner is null!"));
return;
}
// DetectionRange 생성 로직
...
}
성능 고려사항
1. LineTrace 최적화
- 현재: 매 프레임 실행
- 최적화: NearbyInteractables가 비어있을 때는 스킵 (이미 구현됨)
2. NearbyInteractables 배열 관리
- 중복 방지:
Contains()체크 - 자동 정리: Overlap 해제 시 제거
3. 디버그 표시
- 에디터 전용: Shipping 빌드에서는 자동으로 제거됨
- 조건부 실행:
bShowDebugInfo플래그로 제어
결론
이 Interaction System은 Overlap + LineTrace 이중 감지 방식을 통해
정밀하고 확장 가능한 상호작용을 제공합니다.
핵심 장점
- 정확한 타겟팅: 화면 중앙 LineTrace로 의도한 객체만 선택
- 성능 효율: Overlap으로 후보군 필터링 → LineTrace는 최소한만 실행
- 확장성: 새로운 InteractionType 추가 용이
- 네트워크 지원: Replication 기반 멀티플레이어 대응
- 재사용성: UInteractableComponent를 붙이기만 하면 자동으로 동작
확장 가능성
- 새로운 상호작용 타입 (Examine, Dialogue, Craft 등)
- 복잡한 퍼즐 메커니즘
- 조건부 상호작용 (아이템 보유, 퀘스트 상태 등)
- 애니메이션/사운드 통합
참고 파일 위치
Source/Onepiece/
├─ Character/
│ ├─ Public/UInteractionSystem.h
│ └─ Private/UInteractionSystem.cpp
│
├─ Interactable/
│ ├─ Public/
│ │ ├─ InteractableComponent.h
│ │ ├─ luggage.h
│ │ ├─ APedestalSwitch.h
│ │ ├─ AWeightSwitch.h
│ │ └─ ADoor.h
│ │
│ └─ Private/
│ ├─ InteractableComponent.cpp
│ ├─ luggage.cpp
│ ├─ APedestalSwitch.cpp
│ ├─ AWeightSwitch.cpp
│ └─ ADoor.cpp
문서 버전: 1.0
최종 수정: 2025-11-23
작성자: Claude Code Agent