📘 [UE5 C++] 인벤토리 학습 노트 - 3단계: 필드 상호작용 액터
월드 상에 실제로 베치되거나 버려져 플레이어와 부딪히고, 눈에 보이며, 상호작용할 수 있는 아이템 액터의 설계와 구현 원리를 정리하고, 상호작용 인터페이스 구현, 물리 및 콜리전 설정, 그리고 소멸 라이프사이클을 서브시스템으로 양도하는 느슨한 결합 아키텍처를 공부할 예정이다.
1. 상호작용 인터페이스 구현
왜 상호작용을 상속이 아닌 인터페이스로 구현했을까?
플레이어가 아이템 외에도 다양한 환경 물체와 상호작용해야 한다고 가정해 보자.
만약 이들을 상속으로 해결하려 한다면, 이 모든 상호작용 객체들의 공통 부모인 클래스를 만들어야 하며, 이는 클래스 계층 구조를 지나치게 복잡하게 만들고, 불필요한 기능까지 상속받게 되는 원인이 된다.
반면 인터페이스를 사용하면 상위 부모 클래스에 관계없이 상호작용 기능이 가능한 객체로 묶을 수 있게 된다.
c++ 구현 분석 [FTItemActor]
UCLASS()
class PROJECTFT_API AFTItemActor : public AActor, public IFTInteractable
{
GENERATED_BODY()
public:
AFTItemActor();
/* @brief : 아이템의 외형을 데이터 에셋에 맞춰 업데이트 하는 메서드 */
void UpdateAppearance();
protected:
virtual void BeginPlay() override;
// IFTInteractable 인터페이스 구현
// (BlueprintNativeEvent 대응을 위해 _Implementation 접미사 필요)
virtual bool Interact_Implementation(AActor* Interactor) override;
/* @brief : 아이템 제거 메서드입니다. */
virtual void DestroyItem();
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TObjectPtr<UFTItemDataAsset> ItemData;
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> MeshComponent;
};
· BlueprintNativeEvent :
c++에서 기본 동작을 구현하고, 블루프린트에서 이를 완전히 오버라이드 하거나 추가 연출을 덧붙일 수 있도록 언리얼 리플렉션 시스템이 지원하는 특수 이벤트 형식이다. c++로 재정의할 경우 뒤에 '_Implementation'을 붙여 구현해야 한다.
2. 렌더링 및 물리 설정 (Rendering & Physics)
필드에 아이템이 떨어졌을 때, 허공에 둥둥 떠 있는 대신 바닥으로 자연스럽게 떨어져 튕기는 물리 작용이 필요하다.
AFTItemActor::AFTItemActor()
{
PrimaryActorTick.bCanEverTick = false;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
SetRootComponent(MeshComponent);
// 물리 시뮬레이션 및 콜리전 프로필 활성화
MeshComponent->SetSimulatePhysics(true);
MeshComponent->SetCollisionProfileName(TEXT("PhysicsBody"));
}
· SetSimulatePhysics(true) : 중력과 외부 충돌의 힘을 받아 현실적인 물리 낙하와 튕김을 구현한다.
· PhysicsBody 프로필 : 이 프로필이 적용된 액터는 월드의 정적 충돌체(바닥, 벽 등) 및 캐릭터와 물리적으로 부딪히는 동작을 취한다.
· bCanEverTick = false : 물리 연산은 언리얼의 물리 엔진이 비동기적으로 처리하므로, 메인 스레드 틱을 꺼두어 무수히 많은 드랍 아이템이 쌓여도 cpu 점유율을 낮게 유지한다.
3. 상호작용을 통한 습득 이벤트 발행 (Event-Driven Looting)
플레이어가 아이템과 상호작용하여 습득할 때, 아이템 액터가 플레이어의 인벤토리 컴포넌트를 직접 찾아가 함수를 호출하지 않고, 전역 메시지 라우터를 통해 간접적으로 통신한다.
bool AFTItemActor::Interact_Implementation(AActor* Interactor)
{
if (!ItemData) return false;
// GameplayMessageSubsystem을 통해 아이템 획득 메시지 전송
UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this);
FFTMessagePayloadStruct Payload;
Payload.ItemId = ItemData->ItemData.ItemId; // 주워진 아이템 ID
Payload.InstigatorActor = Interactor; // 주운 플레이어 캐릭터
Payload.TargetActor = this; // 주워진 액터 본인
Payload.Value = 1.0f; // 습득 수량 고정
// "Event.Item.PickedUp" 태그를 채널로 사용하여 이벤트 발행
MessageSubsystem.BroadcastMessage(TAG_FT_Event_ItemPickedUp, Payload);
UE_LOG(LogFTItem, Log, TEXT("Picked up item: %s"), *ItemData->ItemData.ItemName.ToString());
DestroyItem(); // 소멸 단계로 이전
return true;
}
왜 인벤토리를 직접 가져오지 않고 전역 메시지를 쏠까?
만약 `Interactor->FindComponentByClass<UFTInventoryComponent>()->AddItem(...)`처럼 직접 호출하게 되면 다음과 같은 결함이 생기게 된다.
1. 강한 결합도: AFTItemActor가 UFTInventoryComponent에 강하게 종속된다. 만약 상호작용하는 주체가 인벤토리가 없는 몬스터이거나 특정 환경 오브젝트라면 예외 처리가 까다로워진다.
2. 확장성 저하: 플레이어가 아이템을 주웠을 때 퀘스트 시스템에서 아이템 습득 퀘스트 완료 체크를 하거나, 시스템에서 카운트를 올리고 싶다면, 아이템 액터 내부에 퀘스트 매니저.. 등을 전부 include 하고 일일이 호출해 줘야 한다.
3. 이벤트 기반의 이점: 브로드캐스트 메시지로 메시지만 발행해 두면, 인벤토리 시스템뿐만 아니라 퀘스트, UI 알림 등 메시지를 필요로 하는 모든 별도 시스템이 각자 리스너를 등록해 독립적으로 반응할 수 있어 유지보수가 매우 유연해진다.
4. 수명 주기 위임 (풀링을 위한 설계)
아이템이 습득되어 사라질 때, 액터 내에서 즉시 'Destory()'를 호출해 버리면 추후 가비지 컬렉터 부하를 예방하기 위한 오브젝트 풀링을 적용할 수 없게 된다.
소멸 과정을 위한 래핑 구현
void AFTItemActor::DestroyItem()
{
// 1. 월드가 유효한지 확인 후 풀 서브시스템 획득 시도
if (UWorld* World = GetWorld())
{
if (UFTItemPoolSubsystem* PoolSubsystem = World->GetSubsystem<UFTItemPoolSubsystem>())
{
// 2. 파괴 권한을 서브시스템으로 양도 (풀링 반환 처리)
PoolSubsystem->ReleaseItemActor(this);
return;
}
}
// 3. 서브시스템이 없거나 오류 상황일 때만 백업용 직접 파괴 실행
Destroy();
}
이렇게 되면 액터는 자신이 메모리에서 수멸하는지, 아니면 비활성화되어 대기 풀에 숨겨지는지 알 필요가 없다. 오직 전역 풀 매니저(UFTItemPoolSubsystem)에게 자기를 되돌려준다고 알릴 뿐이며, 이로 인해 액터 클래스의 로직 변경 없이 풀링 시스템을 완벽하게 탈부착할 수 있게 된다.
'내배캠 > CH3 팀프로젝트(ProjectFT)' 카테고리의 다른 글
| [UE5 C++] MVVM 패턴 기반의 인벤토리 UI 데이터 바인딩 및 뷰모델 구현 (0) | 2026.07.10 |
|---|---|
| [UE5 C++] WorldSubsystem과 GameplayMessage를 활용한 드롭 아이템 스폰 및 오브젝트 풀링(Pooling) 최적화 (0) | 2026.07.09 |
| [UE5 C++] 컴포넌트 패턴으로 구현하는 인벤토리 핵심 기능과 데이터 동기화 (0) | 2026.07.07 |
| [UE5 C++] 데이터 기반 아이템 시스템 구축: UDataAsset과 AssetManager 활용법 (0) | 2026.07.06 |
| [TIL: 2026.06.17] CH3 팀프로젝트 ProjectFT의 아키텍처 분석 (0) | 2026.06.17 |