플레이어 캐릭터와 다양한 오브젝트 간의 상호작용을 유연하게 처리할 수 있도록 구현한 상호작용 시스템입니다. 핵심 구조는 InteractionSystem 컴포넌트를 기반으로 하며, 상호작용 가능한 대상은 인터페이스 기반으로 분리하여 확장성과 재사용성을 고려했습니다.
시스템 구조 및 동작 방식
- 상호작용 기능은 InteractSystem이라는 ActorComponent에 구현되어 있으며, 해당 컴포넌트를 소유한 액터는 상호작용을 수행하는 주체가 됩니다.
- 상호작용 대상은 Interactable 인터페이스를 상속하여 구현되며, 이 인터페이스를 통해 상호작용 관련 데이터를 제공하고 상호작용을 실행합니다.
- 플레이어 캐릭터의 CapsuleComponent에 오버랩된 액터들 중 Interactable 인터페이스를 상속한 액터만을 필터링하여InteractableActors 배열에 저장합니다.
- 상호작용을 시작할 때는 InteractionSystem의 SelectInteractableIndex에 해당하는 액터를 선택하여 해당 액터의 Interactable 인터페이스 함수를 호출함으로써 상호작용을 실행합니다.
시스템 등록
InteractionSystem은 캐릭터에 등록될 때 OnRegister() 함수에서 BindInteractEvent() 함수를 호출하여 초기 설정을 수행합니다.
void UPRInteractionSystemComponent::BindInteractEvent()
{
if (GetPROwner() && GetPROwner()->GetCapsuleComponent())
{
UCapsuleComponent* PROwnerCapsuleComponent = GetPROwner()->GetCapsuleComponent();
PROwnerCapsuleComponent->OnComponentBeginOverlap.AddDynamic(this, &UPRInteractionSystemComponent::InteractableBeginOverlap);
PROwnerCapsuleComponent->OnComponentEndOverlap.AddDynamic(this, &UPRInteractionSystemComponent::InteractableEndOverlap);
}
OnInteractableActorsChanged.AddDynamic(this, &UPRInteractionSystemComponent::UpdateProgressBarOnInteractableActorsChanged);
OnInteractableIndexChanged.AddDynamic(this, &UPRInteractionSystemComponent::UpdateProgressBarOnInteractableIndexChanged);
}
BindInteractEvent() 함수는 캐릭터의 CapsuleComponent의 Overlap 이벤트에 상호작용 가능한 액터를 감지하는 함수를 바인딩하고, 상호작용 대상 목록(InteractableActors 배열) 변경을 알리는 OnInteractableActorsChanged 델리게이트와, 선택된 인덱스가 변경되었음을 알리는 OnInteractableIndexChanged 델리게이트에 각각 프로그래스바를 갱신하는 함수들을 연결합니다.
상호작용 가능한 액터 감지
BindInteractEvent() 함수가 정상적으로 호출되었다면, 캐릭터의 CapsuleComponent에서 OnComponentBeingOverlap 이벤트가 발생했을 때 InteractableBeginOverlap() 함수가 실행됩니다.
void UPRInteractionSystemComponent::InteractableBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (CanAddInteractableActor(OtherActor))
{
InteractableActors.AddUnique(OtherActor);
// 처음 추가 되었을 때 프로그래스바를 업데이트합니다.
if (InteractableActors.Num() == 1)
{
CurrentInteractableActor = OtherActor;
}
OnInteractableActorsChanged.Broadcast(GetInteractableActors());
OnInteractableIndexChanged.Broadcast(SelectInteractableIndex);
}
}
이 함수는 오버랩된 액터가 Interactable 인터페이스를 상속하고 있는지 확인한 후, 조건을 만족할 경우 해당 액터를 InteractableActors 배열에 추가합니다.
배열이 처음으로 채워지는 경우에는 CurrentInteractableActor를 해당 액터로 지정하며, 이후 OnInteractableActorsChanged와 OnInteractableIndexChanged 델리게이트를 실행합니다.
void UPRInteractionSystemComponent::InteractableEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (IsInteractableActor(OtherActor))
{
const int32 RemoveIndex = InteractableActors.IndexOfByKey(OtherActor);
AActor* CurrentTarget = GetInteractableActor();
const int32 CurrentIndex = InteractableActors.IndexOfByKey(CurrentTarget);
// 제거 대상이 현재 상호작용 중인 액터일 경우
if (OtherActor == CurrentTarget)
{
// 진행중인 상호작용을 초기화
ResetInteractionState();
// 배열의 마지막 항목일 경우 SelectInteractableIndex 감소
if (RemoveIndex != 0 && RemoveIndex == InteractableActors.Num() - 1)
{
--SelectInteractableIndex;
}
InteractableActors.Remove(OtherActor);
CurrentInteractableActor = GetInteractableActor();
}
else
{
// 상호작용 중인 액터보다 앞에 있는 액터가 제거됐을 경우 SelectInteractableIndex를 보정
if (RemoveIndex < CurrentIndex)
{
--SelectInteractableIndex;
}
InteractableActors.Remove(OtherActor);
}
OnInteractableIndexChanged.Broadcast(SelectInteractableIndex);
OnInteractableActorsChanged.Broadcast(GetInteractableActors());
}
}
반대로 OnComponentEndOverlap 이벤트가 발생하면 InteractEndOverlap() 함수가 호출되어, 오버랩이 끝난 액터가 상호작용 가능한 대상이었다면 InteractableActors 배열에서 해당 액터를 제거합니다.
이때, 제거되는 액터가 현재 선택된 상호작용 대상(CurrentInteractableActor)일 경우 상호작용 상태를 초기화하고, 선택 인덱스(SelectInteractableIndex)를 상황에 맞게 조정합니다.
이후 동일하게 OnInteractableActorsChanged와 OnInteractableIndexChanged 델리게이트를 호출합니다.
상호작용 실행
입력에 따라 상호작용 방식(Press/Hold/Tap)을 분기하고, 상호작용 진행 상황을 프로그래스바로 표시합니다.
StartInteract() 함수를 실행하여 상호작용을 시작합니다.
void UPRInteractionSystemComponent::StartInteract()
{
CurrentInteractableActor = GetInteractableActor();
if (IsValid(CurrentInteractableActor))
{
const FPRInteractionData& InteractionData = IPRInteractable::Execute_GetInteractionData(CurrentInteractableActor);
switch (InteractionData.InteractionType)
{
case EPRInteractionType::Hold:
OnHoldInteraction.AddDynamic(this, &UPRInteractionSystemComponent::UpdateHoldInteraction);
OnInteractionEnded.AddDynamic(this, &UPRInteractionSystemComponent::InitializeInteractProgressBarWidget);
break;
case EPRInteractionType::Tap:
InteractionTap();
break;
case EPRInteractionType::Press:
default:
ExecuteInteract();
break;
}
}
}
이 함수는 현재 선택된 상호작용 대상 액터(CurrentInteractableActor)를 가져와 유효성을 검사하고, 해당 액터의 FPRInteractionData 구조체에서 설정된 상호작용 방식(EPRInteractionType)을 확인한 뒤 분기 처리합니다.
상호작용 방식
시스템은 다음의 3가지 상호작용 방식을 제공합니다.
방식 | 설명 |
Press | 버튼을 한 번 눌러 즉시 상호작용을 실행합니다. |
Hold | 일정 시간 동안 입력을 유지해야 상호작용이 완료됩니다. |
Tap | 정해진 횟수만큼 버튼을 눌러야 상호작용이 실행됩니다. |
Hold 및 Tap 방식의 경우, 상호작용 대상의 위치에 프로그래스 바를 생성하여 실시간 진행 상태를 시각적으로 제공합니다.
Press
bool UPRInteractionSystemComponent::ExecuteInteract()
{
AActor* Target = GetInteractableActor();
if (IsValid(Target))
{
if (IsValid(GetPROwner()))
{
const FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(GetPROwner()->GetActorLocation(), Target->GetActorLocation());
GetPROwner()->SetActorRotation(FRotator(0.0f, LookAtRotation.Yaw,0.0f));
}
// 상호작용 애니메이션 추가 예정
bool bResult = IPRInteractable::Execute_Interact(Target, GetPROwner());
UpdateInteractProgressBarWidget();
ResetInteractionState();
return bResult;
}
return false;
}
void UPRInteractionSystemComponent::ResetInteractionState()
{
OnHoldInteraction.RemoveDynamic(this, &UPRInteractionSystemComponent::UpdateHoldInteraction);
CurrentInteractableActor = nullptr;
TapInputCount = 0;
}
Press 방식은 버튼을 한 번 누르면 즉시 상호작용이 실행됩니다.
ExecuteInteract() 함수가 호출되며, 내부에서는 GetInteractableActor()를 통해 현재 상호작용 대상을 가져온 뒤, 플레이어가 해당 대상 방향을 바라보도록 회전시킵니다. 이후 상호작용 대상이 구현한 Interact(AActor* Interactor) 인터페이스 함수가 실행됩니다.
상호작용이 완료되면 프로그래스바는 초기화되고, 상호작용 상태는 리셋됩니다.
상호작용 상태를 리셋합니다.
Hold
void UPRInteractionSystemComponent::UpdateHoldInteraction(const float ElapsedSeconds)
{
CurrentInteractableActor = GetInteractableActor();
if (IsValid(CurrentInteractableActor))
{
const float HoldDuration = IPRInteractable::Execute_GetInteractionData(GetInteractableActor()).HoldDuration;
if (IsValid(ProgressBarWidget))
{
ProgressBarWidget->SetProgressPercent(ElapsedSeconds / HoldDuration);
}
if (ElapsedSeconds > HoldDuration)
{
ExecuteInteract();
UnbindOnInteractionOngoing();
}
}
}
Hold 방식은 버튼을 일정 시간 동안 누르고 있어야 상호작용이 완료됩니다.
지속적으로 호출되는 UpdateHoldInteraction(const float ElapsedSeconds) 함수에서 ElapsedSeconds를 기준으로, 인터페이스를 통해 가져온 상호작용 데이터(FPRInteractionData)에서 필요한 유지 시간(HoldDuration)을 확인합니다.
ElapsedSeconds / HoldDuration의 비율로 프로그래스바가 실시간으로 업데이트되며, 유지 시간이 충족되면 ExecuteInteract() 함수를 호출하여 상호작용을 실행합니다.
Tap
void UPRInteractionSystemComponent::InteractionTap()
{
++TapInputCount;
CurrentInteractableActor = GetInteractableActor();
if (IsValid(CurrentInteractableActor))
{
const int32 TapCount = IPRInteractable::Execute_GetInteractionData(GetInteractableActor()).TapCount;
if (TapCount <= 0)
{
// 횟수가 설정되어 있지 않으면 바로 상호작용을 실행합니다.
ExecuteInteract();
}
else
{
if (IsValid(ProgressBarWidget))
{
ProgressBarWidget->SetProgressPercent(static_cast<float>(TapInputCount) / static_cast<float>(TapCount));
if (TapInputCount >= TapCount)
{
ExecuteInteract();
}
}
}
}
}
Tap 방식은 버튼을 여러 번 눌러야 상호작용이 실행됩니다.
InteractionTap() 함수가 호출될 때마다 TapInputCount가 증가하고, 인터페이스를 통해 얻은 TapCount에 도달하면 ExecuteInteract() 함수가 실행됩니다.
중간 진행도는 프로그래스바를 통해 시각적으로 피드백되며, 누적 입력이 목표 횟수에 도달할 때까지 상호작용은 보류됩니다.
상호작용 정보 (InteractionData)
/**
* 상호작용에 대한 정보를 나타내는 구조체입니다.
*/
USTRUCT(Atomic, BlueprintType)
struct FPRInteractionData
{
GENERATED_BODY()
public:
FPRInteractionData()
: Icon(nullptr)
, InteractionText(FText::GetEmpty())
, InteractionType(EPRInteractionType::Press)
, HoldDuration(4.0f)
, HoldInProgressText(FText::GetEmpty())
, TapCount(4)
, bUseItemAcquisitionNotification(false)
{}
FPRInteractionData(UTexture2D* NewIcon, FText NewInteractionText, EPRInteractionType NewInteractionType,
FText NewHoldInProgressText, float NewHoldDuration, int32 NewTapCount, bool bNewbUseItemAcquisitionNotification)
: Icon(NewIcon)
, InteractionText(NewInteractionText)
, InteractionType(NewInteractionType)
, HoldDuration(NewHoldDuration)
, HoldInProgressText(NewHoldInProgressText)
, TapCount(NewTapCount)
, bUseItemAcquisitionNotification(bNewbUseItemAcquisitionNotification)
{}
public:
/** 상호작용 아이콘입니다. 아이템 또는 행동을 시각적으로 나타냅니다. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PRInteractionData")
UTexture2D* Icon;
/** 상호작용 대상의 이름 또는 설명입니다. (예: '오래된 코등이', '문 열기') */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PRInteractionData")
FText InteractionText ;
/** 상호작용 방식입니다. Press, Hold, Tap 중 하나입니다. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PRInteractionData")
EPRInteractionType InteractionType;
/** Hold 방식일 때, 상호작용이 실행되기까지 누르고 있어야 하는 시간입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRInteractionData", meta = (EditCondition = "InteractionType == EPRInteractionType::Hold", ClampMin = "0.0"))
float HoldDuration;
/** Hold 방식일 때, 표시할 진행 상태 문구입니다. (예: "열기 진행 중...") */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRInteractionData", meta = (EditCondition = "InteractionType == EPRInteractionType::Hold", ClampMin = "0.0"))
FText HoldInProgressText;
/** Tap 방식일 때, 상호작용을 실행하기 위해 눌러야 하는 횟수입니다. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "PRInteractionData", meta = (EditCondition = "InteractionType == EPRInteractionType::Tap", ClampMin = "0"))
int32 TapCount;
/** 아이템 획득 알림의 사용 여부입니다. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PRInteractionData")
bool bUseItemAcquisitionNotification;
};
상호작용에 필요한 다양한 데이터를 하나로 관리하기 위해 FPRInteractionData 구조체를 설계하였습니다.
이 구조체는 아이콘, 상호작용 설명 텍스트, 상호작용 방식(Press/Hold/Tap), 진행 중 문구, 그리고 입력 지속 시간 또는 누적 횟수 등 상호작용 수행에 필요한 핵심 정보를 포함하고 있습니다.
이러한 정보를 통합해둠으로써, 개별 오브젝트의 상호작용 동작을 더 유연하게 정의하고 제어할 수 있습니다.
프로그래스바 UI
void UPRInteractionSystemComponent::UpdateInteractProgressBarWidget()
{
if (GetInteractableActor()
&& GetPRPlayerController()
&& ProgressBarWidgetComponent
&& ProgressBarWidgetClassReference)
{
AActor* Target = GetInteractableActor();
if (IsValid(Target))
{
const FPRInteractionData& InteractionData = IPRInteractable::Execute_GetInteractionData(Target);
switch (InteractionData.InteractionType)
{
case EPRInteractionType::Hold:
case EPRInteractionType::Tap:
InitializeOrUpdateProgressBarWidget(InteractionData);
break;
case EPRInteractionType::Press:
default:
ProgressBarWidgetComponent->SetWidget(nullptr);
break;
}
}
}
else
{
ProgressBarWidgetComponent->SetWidget(nullptr);
}
}
Hold 방식과 Tap 방식의 상호작용에서는 플레이어가 얼마나 상호작용을 수행했는지 시각적으로 피드백할 필요가 있습니다. 이를 위해 프로그래스바 UI를 구현하였습니다.
프로그래스바는 다음과 같은 시점에서 UpdateInteractProgressBarWidget() 함수를 통해 위젯을 생성하거나 제거하며, 상태를 최신화합니다.
- 상호작용 대상이 변경되었을 때
- 상호작용이 진행 중일 때
- 상호작용이 완료되었을 때
이러한 동작을 통해 플레이어는 직관적으로 상호작용의 진행 상황을 파악할 수 있으며, 입력 피드백이 즉각적으로 전달되어 몰입감을 높일 수 있습니다.
아이템 획득과 알림
플레이어가 아이템을 획득했을 때, 해당 아이템의 이름과 이이콘을 화면에 표시하여 직관적인 피드백을 제공하기 위해 아이템 획득 알림 UI를 구현하였습니다.
이를 위해 플레이어 컨트롤러 클래스는 IPRPlayerControllerInterface를 상속하며, 다음과 같은 방식으로 알림을 생성합니다.
void APRPlayerController::CreateItemAcquisitionNotify_Implementation(const FText& NewItemName, UTexture2D* NewItemIcon)
{
if (GetInGameHUDWidget() && GetInGameHUDWidget()->GetItemAcquisitionList())
{
UPRItemAcquisitionListWidget* NotifyList = GetInGameHUDWidget()->GetItemAcquisitionList();
if (NotifyList)
{
NotifyList->AddItemAcquisitionNotification(NewItemName, NewItemIcon);
}
}
}
아이템 획득 알림을 생성할 경우 CreateItemAcquisitionNotify(const FText& NewItemName, UTexture2D* NewItemIcon) 함수를 호출합니다. 이 함수는 인터페이스 기반으로 구현되어 있어, 컨트롤러가 어떤 방식으로든 이를 재정의 UI 반응을 정의할 수 있습니다.
함수 내부에서는 AddItemAcquisitionNotification(const FText& NewItemName, UTexture2D* NewItemIcon) 함수를 통해 아이콘과 아이템 이름을 담은 슬롯 위젯을 생성하고 알림 리스트에 추가하여 화면에 출력합니다.
'Project > Replica' 카테고리의 다른 글
[Project Replica] PoolableInterface / EffectSytstem (0) | 2024.06.29 |
---|---|
[Project Replica] PoolableInterface / BaseObjectPoolSystem (0) | 2024.06.25 |
[Project Replica] EffectSystem (0) | 2024.04.30 |
[Project Replica] Vaulting / Vault / 장애물 뛰어넘기 (0) | 2024.04.15 |
[Project Replica] DamageSystem 대미지 시스템 (0) | 2024.01.11 |