상호작용 시스템
장비, 환경 모두 범용으로 사용할 수 있는 구조 만들기
목표
- 아이템과 주변 환경 모두에 사용할 수 있는 범용 상호작용 시스템 만들기
기본 구조
상호작용 시스템은 다음과 같은 세 구성 요소로 이루어져있다.
- 상호작용 컴포넌트(IVInteractionComponent)
- 플레이어에 부착되어 주변의 상호작용 가능 대상을 탐색하고 상호작용을 수행한다.
- 상호작용 인터페이스(IIVInteractableInterface)
- 상호작용 대상이 반드시 구현해야 하는 기능들을 선언한다.
- 상호작용 대상
- 상호작용 인터페이스를 구현한 실제 상호작용 객체로, 여기서는 아이템 액터를 의미한다.
상호작용 컴포넌트
플레이어의 입력으로부터 상호작용 함수가 호출되면, 상호작용 컴포넌트는 대상 탐색을 시작한다. 탐색에는 SweepMultiByObjectType
를 사용하며, 구형 범위 내에서 충돌 오브젝트를 찾아 조건 검사를 진행한다. 이 과정은 타겟팅 대상 탐색 기능과 매우 유사하다. 검사 조건은 다음과 같다.
- 대상이
IIVInteractableInterface
를 구현하는가? - 대상이 현재 상호작용 가능 상태인가? (IsInteractable = true && 가로막는 물체 없음)
- 상호작용 가능 대상 중 가장 가까운 위치에 있는가?
이러한 조건을 통과한 경우, 인터페이스를 통해 대상의 Interact
함수를 호출한다.
// IVInteractionComponent.cpp
void UIVInteractionComponent::SearchAndInteract()
{
// 상호작용 대상 탐색을 위한 콜리전 설정
TArray<FHitResult> HitResults;
FVector Start = GetOwner()->GetActorLocation();
FVector End = Start; // 원점
FQuat Rotation = FQuat::Identity;
FCollisionShape CollisionShape = FCollisionShape::MakeSphere(InteractionDistance);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(GetOwner());
if (GetWorld()->SweepMultiByObjectType(
HitResults,
Start,
End,
Rotation,
FCollisionObjectQueryParams(ECollisionChannel::ECC_WorldStatic),
CollisionShape,
QueryParams))
{
AActor* ClosestActor = nullptr;
float ClosestDistance = InteractionDistance;
// 상호작용 대상 중 가장 가까운 액터 선택
for (const FHitResult& HitResult : HitResults)
{
AActor* HitActor = HitResult.GetActor();
if (HitActor && HitActor->Implements<UIIVInteractableInterface>()) // 상호작용 인터페이스를 구현하는가?
{
IIIVInteractableInterface* InteractableActor = Cast<IIIVInteractableInterface>(HitActor);
if (InteractableActor->IsInteractable() && IsTargetInteractable(HitActor)) // 상호작용이 가능한가?
{
float Distance = FVector::Distance(Start, HitActor->GetActorLocation());
if (Distance < ClosestDistance)
{
ClosestDistance = Distance;
ClosestActor = HitActor;
}
}
}
}
// 상호작용 실시
if (ClosestActor)
{
IIIVInteractableInterface* Interactable = Cast<IIIVInteractableInterface>(ClosestActor);
if (Interactable)
{
Interactable->Interact(GetOwner());
}
}
}
}
상호작용 인터페이스
상호작용 인터페이스에는 상호작용 기능 수행을 위한 다양한 함수가 선언되어 있다. 아래와 같이 크게 세 가지로 나뉜다.
// IIVInteractableInterface.h
class IVAN_API IIIVInteractableInterface
{
GENERATED_BODY()
public:
/* 구체적인 상호작용 동작 */
virtual void Interact(AActor* InteractingActor) = 0;
/* 상호작용 가능 여부 설정 */
virtual void SetInteractable(bool bNewInteractable) = 0;
virtual bool IsInteractable() const = 0;
/* 상호작용 UI 효현 여부 */
virtual void ShowInteractionUI() = 0;
virtual void HideInteractionUI() = 0;
};
상호작용 대상(아이템)
위의 인터페이스를 구현하여 실제로 플레이어와 상호작용 하는 대상이다. 여기서는 아이템 액터가 해당된다.
인터페이스에 명시되지 않았지만 상호작용을 위해 필요한 요소가 몇 가지 더 존재한다. 상호작용 범위에 들어왔는지 판단하기 위한 콜라이더와 상호작용 버튼을 표시하기 위한 UI가 그것이다. 콜라이더의 범위 내로 액터가 들어오면, 해당 액터가 “Player”태그를 보유했는지 검사를 진행한다. 플레이어가 맞다면 UI가 표시된다.
실제 상호작용은 Interact
함수 내부에 정의된다. 아이템의 경우에는 플레이어의 인벤토리 컴포넌트에 접근하여 본인을 추가한다. 이 때 플레이어와 같은 상호작용의 주체에 접근하기 위한 것이 Interact함수의 인자인 InteractingActor 이다.
// IVItemBase.cpp
void AIVItemBase::Interact(AActor* InteractingActor)
{
if (!bIsInteractable || !InteractingActor) return;
UIVInventoryComponent* InventoryComponent = InteractingActor->FindComponentByClass<UIVInventoryComponent>();
if (InventoryComponent)
{
// 인벤토리에 추가 성공 시 레벨에서 아이템 제거
if (InventoryComponent->AddItemToInventory(ItemInfo.ItemID))
{
HideInteractionUI();
SetInteractable(false);
Destroy();
}
}
}
결과물
플레이어가 아이템의 일정 범위 내로 접근하자 UI가 표시되고, 상호작용 하여 인벤토리에 아이템을 추가하는 모습이다.