본문으로 건너뛰기
Unreal Engine 모듈 · 플러그인 개발LESSON 28

Runtime 상호작용 기능을 재사용 플러그인으로 분리하기

난이도심화
예상 시간190분
선수지식17강의 Runtime Plugin과 20~21강의 설정 계약

28강. Runtime 상호작용 기능을 재사용 플러그인으로 분리하기

1. 이번 강의에서 해결할 문제​

기존 게임의 AMyPlayerCharacter 안에 Line Trace, UI 갱신, 문 열기 코드가 모두 있으면 다른 프로젝트로 옮길 때 클래스 이름과 게임 규칙을 함께 복사해야 합니다. Plugin이 특정 프로젝트 Character나 GameMode를 Include하는 순간 재사용성은 사실상 사라집니다.

이번 강의에서는 무엇을 상호작용할 수 있는지는 Interface가, 어떻게 대상을 찾고 요청하는지는 ActorComponent가, 프로젝트별 기본값은 Settings가 맡도록 분리합니다.

2. 학습 목표​

3. 핵심 개념​

Runtime Plugin이 의존해도 되는 것은 Engine의 일반 계약과 자신이 소유한 타입입니다. 프로젝트 입력, 캐릭터 애니메이션, 특정 문 Actor는 소비 프로젝트가 연결합니다.

입력 발생(프로젝트) → InteractionComponent::TryInteract
→ 카메라/소유 Actor 기준 Trace
→ UInteractable 구현 여부 확인
→ CanInteract 판정
→ Execute_Interact 호출

싱글플레이 기준 구현을 만들되 멀티플레이 프로젝트에서는 서버가 최종 판정을 해야 합니다. Plugin은 임의의 Client 판정을 신뢰하도록 강제하지 않고, 요청 정책을 확장할 수 있게 둡니다.

4. 단계별 실습​

Interface를 구체화합니다.

Public/Interaction/Interactable.h
#pragma once

#include "UObject/Interface.h"
#include "Interactable.generated.h"

UINTERFACE(BlueprintType)
class REUSABLEINTERACTION_API UInteractable : public UInterface
{
GENERATED_BODY()
};

class REUSABLEINTERACTION_API IInteractable
{
GENERATED_BODY()

public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction")
bool CanInteract(AActor* InstigatorActor) const;

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction")
FText GetInteractionPrompt(AActor* InstigatorActor) const;

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction")
void Interact(AActor* InstigatorActor);
};

Component의 Public API는 작게 유지합니다.

Public/Interaction/InteractionComponent.h
#pragma once

#include "Components/ActorComponent.h"
#include "InteractionComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FInteractionTargetChanged,
AActor*, PreviousTarget,
AActor*, NewTarget);

UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class REUSABLEINTERACTION_API UInteractionComponent final
: public UActorComponent
{
GENERATED_BODY()

public:
UInteractionComponent();

UFUNCTION(BlueprintCallable, Category="Interaction")
bool RefreshTarget();

UFUNCTION(BlueprintCallable, Category="Interaction")
bool TryInteract();

UFUNCTION(BlueprintPure, Category="Interaction")
AActor* GetCurrentTarget() const { return CurrentTarget.Get(); }

UFUNCTION(BlueprintPure, Category="Interaction")
FText GetCurrentPrompt() const;

UPROPERTY(BlueprintAssignable, Category="Interaction")
FInteractionTargetChanged OnTargetChanged;

protected:
UPROPERTY(EditAnywhere, Category="Interaction",
meta=(ClampMin="0.0", Units="cm"))
float TraceDistanceOverride = 0.0f;

private:
bool BuildViewRay(FVector& OutStart, FVector& OutDirection) const;
bool IsCandidateInteractable(AActor* Candidate) const;
void SetCurrentTarget(AActor* NewTarget);

TWeakObjectPtr<AActor> CurrentTarget;
};

Trace와 판정을 구현합니다.

Private/Interaction/InteractionComponent.cpp
#include "Interaction/InteractionComponent.h"

#include "DrawDebugHelpers.h"
#include "GameFramework/Controller.h"
#include "GameFramework/Pawn.h"
#include "Interaction/Interactable.h"
#include "Settings/InteractionSettings.h"

UInteractionComponent::UInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}

bool UInteractionComponent::BuildViewRay(
FVector& OutStart,
FVector& OutDirection) const
{
const APawn* PawnOwner = Cast<APawn>(GetOwner());
const AController* Controller = PawnOwner ? PawnOwner->GetController() : nullptr;
if (Controller)
{
FRotator ViewRotation;
Controller->GetPlayerViewPoint(OutStart, ViewRotation);
OutDirection = ViewRotation.Vector();
return true;
}

if (const AActor* Owner = GetOwner())
{
OutStart = Owner->GetActorLocation();
OutDirection = Owner->GetActorForwardVector();
return true;
}

return false;
}

bool UInteractionComponent::IsCandidateInteractable(
AActor* Candidate) const
{
if (!IsValid(Candidate) || Candidate == GetOwner())
{
return false;
}

if (!Candidate->GetClass()->ImplementsInterface(
UInteractable::StaticClass()))
{
return false;
}

return IInteractable::Execute_CanInteract(
Candidate, GetOwner());
}

bool UInteractionComponent::RefreshTarget()
{
UWorld* World = GetWorld();
const UInteractionSettings* Settings =
GetDefault<UInteractionSettings>();
if (!World || !ensure(Settings))
{
SetCurrentTarget(nullptr);
return false;
}

FVector Start;
FVector Direction;
if (!BuildViewRay(Start, Direction))
{
SetCurrentTarget(nullptr);
return false;
}

const float Distance = TraceDistanceOverride > 0.0f
? TraceDistanceOverride
: FMath::Max(50.0f, Settings->DefaultTraceDistance);
const FVector End = Start + Direction * Distance;

FCollisionQueryParams Params(SCENE_QUERY_STAT(ReusableInteraction), false);
Params.AddIgnoredActor(GetOwner());

FHitResult Hit;
const bool bHit = World->LineTraceSingleByChannel(
Hit,
Start,
End,
Settings->DefaultTraceChannel,
Params);

AActor* Candidate = bHit ? Hit.GetActor() : nullptr;
SetCurrentTarget(IsCandidateInteractable(Candidate) ? Candidate : nullptr);

if (Settings->bDrawDebugTrace)
{
DrawDebugLine(
World, Start, End,
CurrentTarget.IsValid() ? FColor::Green : FColor::Red,
false, 1.0f, 0, 1.0f);
}

return CurrentTarget.IsValid();
}

bool UInteractionComponent::TryInteract()
{
if (!RefreshTarget())
{
return false;
}

AActor* Target = CurrentTarget.Get();
if (!IsCandidateInteractable(Target))
{
SetCurrentTarget(nullptr);
return false;
}

IInteractable::Execute_Interact(Target, GetOwner());
return true;
}

void UInteractionComponent::SetCurrentTarget(AActor* NewTarget)
{
AActor* Previous = CurrentTarget.Get();
if (Previous == NewTarget)
{
return;
}

CurrentTarget = NewTarget;
OnTargetChanged.Broadcast(Previous, NewTarget);
}

FText UInteractionComponent::GetCurrentPrompt() const
{
AActor* Target = CurrentTarget.Get();
return IsCandidateInteractable(Target)
? IInteractable::Execute_GetInteractionPrompt(Target, GetOwner())
: FText::GetEmpty();
}

GetWorld()가 null일 수 있는 생성·종료 시점을 방어한 뒤 같은 World 포인터를 Trace와 Debug Draw에 사용합니다.

5. 코드가 동작하는 이유​

Component는 소유 Pawn이 있으면 Controller의 View Point를 사용하고, 일반 Actor에도 붙일 수 있도록 Forward Vector로 대체합니다. TWeakObjectPtr는 대상 Actor가 파괴되었을 때 강한 소유권을 남기지 않습니다. Interface의 Execute_ 경로는 C++와 Blueprint 구현을 같은 방식으로 호출합니다.

6. 자주 하는 실수와 해결법​

7. 직접 실습​

8. 이해 점검 질문 3개​

9. 핵심 요약​

MINI QUIZ

Runtime 상호작용 기능을 재사용 플러그인으로 분리하기 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“Runtime 상호작용 기능을 재사용 플러그인으로 분리하기”의 역할과 의존성을 판단하는 올바른 기준은 무엇인가요?
  2. 문제 2“Runtime 상호작용 기능을 재사용 플러그인으로 분리하기”에서 ‘Multicast로 지속 상태 동기화’ 문제가 생겼습니다. 가장 알맞은 진단 또는 대응은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

28강. Runtime 상호작용 기능을 재사용 플러그인으로 분리하기 미완료 상태