언리얼 - 엔진 17 : 아이템 구현 - 인터페이스, 충돌 처리

김정환·2025년 4월 12일
0

Unreal Engine

목록 보기
18/24

인터페이스 interface

  • 상속받은 클래스가 반드시 구현할 것이라는 일종의 계약서, 명세서, 약속
    • 아이템, 장비들 구현할 때 각각 다른 기능과 내용을 가지더라도 interface에서 명시한 함수를 통해서 호출할 수 있음.
    • 이로 하여금 각각의 아이템 장비들은 자신만의 기능을 구현할 수 있음.
  • C++에선 UInterface를 상속받아 IItemInterface 같은 인터페이스를 만들 수 있음.
    • BP로도 인터페이스를 작성할 수 있음.

상속 vs 인터페이스

  • 상속 Inheritance
    • 부모 클래스로부터 물려받는 것
    • 부모 클래스의 멤버 변수, 함수를 그대로 쓸 수 있으며
      필요하다면 재정의(Override) 할 수 있음.
  • 인터페이스 Interface
    • 어떤 함수의 구현에 대해 약속하는 것
    • 함수의 원형(시그니처)만 정의
    • 실제 동작은 구현 클래스에서 작성

상속은 부모의 실제 구현을 가져다 씀.
인터페이스는 함수의 틀만 빌려쓰고 그 안에 담길 코드는 직접 작성.

인터페이스의 장점

1. 결합도(Coupling) 감소

  • 클래스 간 구체적인 구현 내용을 공유하지 않고
    필요한 함수 목록만 약속하므로 클래스 간 의존도가 낮아짐.
  • 다른 클래스의 내부가 어떻게 돌아가는지 몰라도, 이 함수는 이렇게 호출하면 된다만 알면 됨.

2. 확장성(Extensibility) 향상

  • 새로운 아이템 클래스를 만들 때, 이미 정의된 인터페이스를 구현하기만 하면 기존 시스템에 쉽게 편입할 수 있음.

3. 다형성(Polymorphism) 극대화

  • TArray<IInterface*> Somethings 과 같이 인터페이스 포인터 배열로 관리해서
    아이템 종류가 무엇이든 같은 함수를 호출해서 다룰 수 있음.

아이템 구현

  • interface를 정의하여 아이템 사용을 통일화
    • UInterface : 인터페이스를 만들어 함수의 원형들을 정의 (가상함수 형태)
  • 아이템 클래스들이 이 인터페이스들을 구현
    • OnItemOverlap() 구현할 것
  • 만약 interface를 사용하지 않는다면?
    • 캐릭터는 모든 아이템의 이름과 클래스, 내부의 멤버 함수들을 알아야 호출할 수 있음.
    • 코드 확장성 및 가독성에 안 좋음.

인터페이스 생성

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "IItemInterface.generated.h"

// 인터페이스를 UObject 시스템에서 사용하기 위한 기본 매크로
UINTERFACE(MinimalAPI)
class UItemInterface : public UInterface
{
    GENERATED_BODY()
};

// 실제 C++ 레벨에서 사용할 함수 원형(시그니처)를 정의
class CHAPTER2_API IItemInterface
{
    GENERATED_BODY()

public:
    // 플레이어가 이 아이템의 범위에 들어왔을 때 호출
    // Collision Component에 바인딩할 것.
    virtual void OnItemOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex,
		bool bFromSweep,
		const FHitResult& SweepResult) override;
    // 플레이어가 이 아이템의 범위를 벗어났을 때 호출
    virtual void OnItemEndOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex) override;
    // 아이템이 사용되었을 때 호출
    virtual void ActivateItem(AActor* Activator) = 0;
    // 이 아이템의 유형(타입)을 반환 (예: "Coin", "Mine" 등)
    virtual FName GetItemType() const = 0;
};
  • UINTERFACE(MinimalAPI)
    • UE에 리플렉션 시스템을 위해 사용하는 매크로
    • 이렇게 선언해야 BP나 다른 모듈에서도 해당 인터페이스를 인식하고 사용 가능.
  • class UItemInterface : public UInterface
    • 실제 객체(클래스)를 관리하기 위한 UE 측 클래스.
    • C++의 IItemInterface와 구분해서 사용.
      • 이 클래스는 수정 X
  • class CHAPTER2_API IItemInterface
    • 우리가 직접 구현해서 작업할 인터페이스
      • 여기에 필요한 함수들을 정의할 것
    • 순수 가상 함수로 작성해서 자식에서 반드시 구현하도록 강제.
  • UE에서 인터페이스를 만들면 .cpp도 같이 생성됨.
    • 인터페이스임에도 .cpp가 같이 생성되는 이유는 UE 자체적으로 .h.cpp 매칭을 강조하고 있기 때문.
    • 아마 내부 설계적으로 둘을 묶어서 인식하도록 하는 것 같음.

인터페이스 상속 및 구현

  • IItemInterface를 상속하는 BaseItem을 작성할 것.
//.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"

class USphereComponent;

UCLASS()
class CHAPTER2_API ABaseItem : public AActor, public IItemInterface
{
	GENERATED_BODY()

public:
	ABaseItem();

	virtual FName GetItemType() const override;

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FName ItemType;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
	USphereComponent* Collision;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
	UStaticMeshComponent* StaticMesh;

	virtual void OnItemOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex,
		bool bFromSweep,
		const FHitResult& SweepResult) override;
	virtual void OnItemEndOverlap(
		UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor,
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex) override;
	virtual void ActivateItem(AActor* Activator) override;
	virtual void DestroyItem();
};

//.cpp
#include "BaseItem.h"
#include "Components/SphereComponent.h"

ABaseItem::ABaseItem()
{
	PrimaryActorTick.bCanEverTick = false;

	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
	Collision->SetupAttachment(Scene);
	Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));

	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMesh->SetupAttachment(Collision);

	// 이벤트 바인딩
	/*
		장점
		* 런타임 상으로 이루어짐.
		* 근데, 아이템은 계속 있는 것이 아니라 생성되고 사라지고를 반복함.
		* 그래서 이를 위해 동적으로 함수를 변수처럼 넣어주어 사용할 수 있도록 유연성을 높인 것

	*/
	// 오버랩 시작 시 호출되는 델리게이트. 여기에 바인딩
	Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);	
	// 오버랩 종료 시 호출됨
	Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);		
	
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor->ActorHasTag("Player"))
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("Item activated")));
		ActivateItem(OtherActor);
	}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}

FName ABaseItem::GetItemType() const
{
	return ItemType;
}

void ABaseItem::ActivateItem(AActor* Activator)
{
}

void ABaseItem::DestroyItem()
{
	Destroy(); // 사용 시 파괴
}
  • AItemBase
    • 모든 아이템에 공통적으로 적용될 기능을 담는 부모 클래스
    • 인터페이스의 함수들을 빈 함수로 구현해두고
      필요한 아이템에 상속하고 오버라이드해서 실제 로직을 작성.

인터페이스 계층 설계

  • 아이템 인터페이스 ItemInterface
  • 부모 클래스 Item
    • 공통 코인 클래스 CoinItem
      • 작은 코인 클래스
      • 큰 코인 클래스
    • 지뢰 클래스 MineItem
    • 힐링 클래스 HealItem

충돌 처리 : 아이템과 상호작용하기

  • 아이템에 접근하면 상호작용할 수 있도록 할 것
    • 접근해서 상호작용하는 경우 보통 충돌 처리로 구현

충돌 이벤트

  • 충돌 Collision
    • 액터나 컴포넌트가 부딪혔을 때, 접촉했을 때, 상호작용할 때 발생
  • 충돌 영역 Collision Volume
    • 충돌을 활용할 액터에 SphereComponent, BoxComponent 같은 충돌 컴포넌트를 붙여서 사용.
    • 이 충돌체들의 영역에 들어오거나, 닿으면 충돌 이벤트가 자동으로 호출됨.

오버랩 이벤트 Overlap Event

  • Collision Component 컴포넌트와 겹치거나 통과할 때 발생
    • 유니티의 Trigger
  • 물리적으로 부딪히는 것 없이 액터들이 서로 겹치기 시작했을 때 발생.
    • 어떤 것을 감지하는 용도로 사용

히트 이벤트 Hit Event

  • 충돌체에 닿았을 때 막히는 것.
    • 유니티의 Collision
  • 실제 물리 충돌이 일어날 때 발생.
    • 벽에 탄환이 부딪히거나, 적의 충돌체에 플레이어의 검격이 맞는 등

충돌 유형 설정

  • 스태틱 메시에도 적용할 순 있지만 보통 Collision Component를 만들어서 제어함.
    • 충돌 연산의 비용이 나가기 때문.
  • BP에서 Collision 카테고리를 보면 충돌 방식에 대한 옵션들이 있음

주요 프리셋 옵션

  1. NoCollision
    • 충돌을 전혀 감지하지 않으므로, Overlap 또는 Hit 이벤트가 발생하지 않음.
    • 사용 예시: 단순 배경 오브젝트(하늘, 장식용 액터 등).
  2. BlockAll
    • 모든 객체와 충돌하여 막음.
    • 물리적으로 충돌하지만, Overlap 이벤트는 발생하지 않습니다.
    • 사용 예시: 벽, 바닥 같은 고정된 장치.
  3. OverlapAll
    • 모든 객체와 Overlap 이벤트를 발생.
    • 사용 예시: 트리거 존, 감지 센서, 투명한 오브젝트.
  4. BlockAllDynamic
    • 움직이는 (Dynamic) 객체만 충돌하여 막음.
    • 고정된 객체와는 충돌하지 않습니다.
    • 사용 예시: 움직이는 플레이어나 물리 오브젝트와 상호작용하는 문, 벽. 고정된 환경 요소와는 무관하게 작동.
  5. OverlapAllDynamic
    • 움직이는 (Dynamic) 객체와 Overlap 이벤트를 발생.
    • 사용 예시: 플레이어가 근처에 있는지 확인하는 아이템, 센서. Overlap 이벤트가 필요하지만, 물리 충돌이 필요 없는 경우.
  6. Pawn
    • 플레이어나 AI처럼 Pawn 타입 객체를 대상으로 충돌을 감지하거나 막습니다.
    • 사용 예시: 플레이어 전용 문, AI 전용 센서.
  7. Custom
    • 각 채널에 대해 충돌 응답(Overlap, Block, Ignore)을 객체별로 세부적으로 설정 가능.

충돌 반응 여부

  • 충돌 프리셋들은 모두 충돌이 발생하면 어떻게 작동하기 유형들이 있음.
    • Custom으로 프리셋을 선택하면 이를 더 디테일하게 설정할 수 있음.
  • 다음은 그 중 자주 사용하는 것 일부
    • NoCollision: 충돌 비활성화
    • Query Only: Overlap, Hit 등 충돌 이벤트는 감지하지만, 물리적으로 튕기거나 밀리는 반응은 없음.
    • Physics Only: 물리 반응만 일어나고, 이벤트 (Overlap/Hit)는 발생하지 않음.
    • Query and Physics: 충돌 이벤트 + 물리 반응 모두 활성화

태그 Tag

  • 유니티에서도 처음 충돌을 감지할 때 사용하듯 UE에서도 충돌을 감지할 때 Tag를 이용.
    • 레이어 Layer 개념도 있음.
  • BP의 디테일에 들어가서 Actor 섹션의 Tags 항목에 원하는 태그를 추가할 수 있음.
    • C++ 코드에서 다음과 같이 사용 가능
      if(OtherActor->ActorHasTag("Player"))
      {
        // 플레이어 태그를 갖고 있음
      }

이벤트 바인딩

  • Collision Component를 설정했다면 이 충돌에 대한 이벤트를 바인딩해줘야함.
    • 유니티와 차이점
      • 유니티 : OnTrigger~~, OnCollision~~ 충돌 관련 함수를 컴포넌트에서 구현
      • UE : OnComponentBeginOverlap 과 같은 델리게이트에 이벤트를 바인딩
  • 런타임 상으로 이루어진다는 장점이 있음.
    • 아이템은 항상 있는 것이 아니라 생성과 파괴를 반복할 수 있음.
    • 이를 위해 동적으로 함수를 변수처럼 넣어주어 사용할 수 있도록 유연성을 높임

예시

  • 아이템 공통 부모 클래스인 BaseItem의 생성자에 구현
#include "BaseItem.h"
#include "Components/SphereComponent.h"

// Sets default values
ABaseItem::ABaseItem()
{
	PrimaryActorTick.bCanEverTick = false;

	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
	Collision->SetupAttachment(Scene);
	Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));

	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMesh->SetupAttachment(Collision);

	// 오버랩 시작 시 호출되는 델리게이트. 여기에 바인딩
	Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);	
	// 오버랩 종료 시 호출됨
	Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);		
	
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor->ActorHasTag("Player"))
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("Item activated")));
		ActivateItem(OtherActor);
	}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}
  • Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
    Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
    • 델리게이트에 AddDynamic으로 이벤트 등록
    • AddDynamic(Object, Func)
      • 이벤트 등록 시, 호출할 대상과 호출할 동작 함수를 매개변수로 요구함.
    • 또한, 당연하게도 델리게이트 등록 시 함수의 매개변수 구성이 동일해야 함.
  • 이벤트 바인딩에 사용할 함수는 .h에서 UFUNCTION()을 붙여줘야함.
// ItemInterface.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ItemInterface.generated.h"

UINTERFACE(MinimalAPI)
class UItemInterface : public UInterface
{
	GENERATED_BODY()
};

class CHAPTER2_API IItemInterface
{
	GENERATED_BODY()

public:
	// 충돌체에 이벤트 바인딩으로 사용할 것
	UFUNCTION() // 바인딩할 함수에 대해선 리플렉션에 등록해주어야 함
	virtual void OnItemOverlap(
		UPrimitiveComponent* OverlappedComp,		// 오버랩이 발생한 자기 자신.
		AActor* OtherActor,							// 충돌한 상대방의 액터.
		UPrimitiveComponent* OtherComp,				// 충돌한 상대방의 충돌한 원인 컴포넌트. 대체로 충돌의 원인인 충돌체일 것
		int32 OtherBodyIndex,
		bool bFromSweep, 
		const FHitResult& SweepResult) = 0;
	UFUNCTION()
	virtual void OnItemEndOverlap(
		UPrimitiveComponent* OverlappedComp,		
		AActor* OtherActor,							
		UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex) = 0;
	virtual FName GetItemType() const = 0;
	
	// 아이템이 실질적으로 사용됐을때
	virtual void ActivateItem(AActor* Activator) = 0;
};
profile
만성피로 개발자

0개의 댓글