[TIL_250211_2] 충돌 이벤트를 활용한 아이템 획득 시스템 구현하기
💭회고
2025.02.11 - [Dev./UE 언리얼 엔진] - [TIL_250211_1] 머티리얼 시스템 이해 (기초) #Graphics
[TIL_250211_1] 머티리얼 시스템 이해 (기초) #Graphics
💭회고머티리얼과 셰이더의 차이점, PBR(Material의 물리 기반 렌더링), 그리고 최적화 방법까지 정리해봤다.앞으로는 머티리얼 노드 활용법과 고급 머티리얼 제작 기법을 추가로 학습하면 좋을
raindrovvv.tistory.com
머티리얼 학습에 이어서, Overlap 이벤트를 활용해 아이템을 획득하는 방법을 정리해본다.
📌 오늘 배운 핵심 내용
- Overlap 이벤트와 Hit 이벤트의 차이점
- 아이템 충돌 영역 설정 방법
- C++ 기반 아이템 클래스 구현 및 Overlap 이벤트 활용
- 아이템 종류별 기능(코인, 힐링, 지뢰) 적용
- 디버깅 및 최종 테스트 방법
🗺️마인드맵
📒학습 내용
1️⃣ 아이템 획득 방식 개요
언리얼 엔진에서 플레이어가 아이템을 자동으로 획득하도록 만들려면 Overlap 이벤트를 활용해야 한다.
플레이어가 아이템 근처에 다가갔을 때 충돌 감지 이벤트가 발생하여 아이템을 획득하는 방식이다.
✅ 아이템 획득 과정 정리
1. 아이템이 충돌 컴포넌트(SphereComponent)를 포함하고 있어야 한다.
2. 플레이어가 충돌 영역에 들어오면 Overlap 이벤트가 발생한다.
3. Overlap 이벤트 핸들러에서 아이템 획득 로직을 실행한다.
4. 아이템 획득 후 자동으로 삭제되거나 특정 효과가 적용된다.
2️⃣ 충돌 이벤트의 이해
⚔️ Overlap 이벤트 vs. Hit 이벤트
이벤트 종류 설명 사용 예시 :
Overlap 이벤트 | 두 액터가 서로 겹치기 시작할 때 발생 | 아이템 획득, 트리거 존 감지 |
Hit 이벤트 | 두 액터가 물리적으로 충돌할 때 발생 | 벽과 탄환 충돌, 충돌 판정이 필요한 액션 |
👉 아이템 획득에는 Overlap 이벤트를 사용해야 한다.
👉 Hit 이벤트는 불필요한 물리 연산을 발생시킬 수 있으므로 주의해야 한다.
🎯 아이템 충돌 영역 (Collision Volume) 설정
- 아이템을 감지하려면 SphereComponent 또는 BoxComponent를 추가해야 한다.
- 플레이어가 충돌 영역에 들어오면 Overlap 이벤트를 트리거한다.
✅ 충돌 컴포넌트 추가
// 루트 컴포넌트 설정
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
// 충돌 컴포넌트 설정
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Collision->SetupAttachment(Scene);
3️⃣ C++ 아이템 클래스 구현 🛠️
📌 기본 아이템 클래스 (BaseItem)
모든 아이템(코인, 포션, 무기 등)의 공통 기능을 정의하는 부모 클래스이다.
✅ BaseItem.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"
class USphereComponent;
UCLASS()
class START_API ABaseItem : public AActor, public IItemInterface
{
GENERATED_BODY()
public:
ABaseItem();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FName ItemType;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item|Component") // 자체 객체 교체가 아니고, 속성 변환 정도이기에 Visible이면 충분.
USceneComponent* RootScene; // 루트 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item|Component")
USphereComponent* SphereCollision; // 콜리전 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Item|Component")
UStaticMeshComponent* StaticMesh; // 메시 컴포넌트
virtual void OnItemOverlap (UPrimitiveComponent* OverlapPrimitiveComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) override;
virtual void OnItemEndOverlap (UPrimitiveComponent* OverlapPrimitiveComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyInde) override;
virtual void ActivateItem (AActor* Activator) override;
virtual FName GetItemType() const override;
virtual void DestroyItem(); // 아이템 제거 함수
};
✅ BaseItem.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->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Collision->SetupAttachment(Scene);
// 메시 설정
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetupAttachment(Collision);
// Overlap 이벤트 바인딩
Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
}
void ABaseItem::OnItemOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && OtherActor->ActorHasTag("Player"))
{
DestroyItem();
}
}
void ABaseItem::DestroyItem()
{
Destroy();
}
📌 설명:
- Overlap 이벤트 발생 시 아이템 삭제
- 이벤트 바인딩을 통해 런타임에서 동적으로 반응하도록 설정
- 이전 작업에서 편의상 OnItemOverlap 함수와 OnItemEndOverlap 함수의 매개 변수를 `AActor*`로 설정했었다.
- 이제 이벤트 바인딩을 해줘야 하기에, 매개 변수가 매칭되어야 한다.
- 즉, 인터페이스 함수 시그니처를 수정해서 매개 변수를 매칭해줘야 한다.
// 이전 코드
void OnItemOverlap(AActor* OtherActor);
void OnItemEndOverlap(AActor* OtherActor);
// 수정된 코드 - 언리얼 엔진 표준 오버랩 시그니처
void OnItemOverlap(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult);
void OnItemEndOverlap(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
🤔 왜 이벤트 바인딩으로 구현해야 하지?
- 게임 실행 중에는 어떤 경우가 발생할 지 알 수가 없다. 예측할 수 없는 상황에 대응해야 한다.
- 따라서 동적으로 함수가 이어지도록 하는 것이다.
➡️ 런타임(게임 실행) 상태에서 이벤트 바인딩이 이루어지게 한다.
3. OnItemOverlap()` 내에서 플레이어만 감지하도록 설정 ➡️ (`ActorHasTag("Player")`)
🏷️ 플레이어 태그 설정
⚠️트러블 슈팅 : 컴포넌트 상속 문제, 컴포넌트 창 닫기⚠️
1. 컴포넌트 상속 문제
해결 방안 : 일반적으로 언리얼 엔진에서는 StaticMeshComponent를 루트 씬 컴포넌트의 직접적인 자식으로 두는 것이 더 안정적이라고 한다. 그래서 두 컴포넌트 모두 루트의 자식으로 두어서 해결하였다!
// 루트 컴포넌트
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
// 메시 설정
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetCollisionProfileName(TEXT("NoCollision"));
StaticMesh->SetupAttachment(Scene);
// 콜리전 설정
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Collision->SetupAttachment(Scene);
2. 컴포넌트 창 닫기
해결 방안 : 상단 메뉴 Window - Component 항목을 선택하면 된다.
4️⃣ ItemInterface 함수 시그니처 수정
✅ ItemInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ItemInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UItemInterface : public UInterface
{
GENERATED_BODY()
};
class START_API IItemInterface
{
GENERATED_BODY()
public:
UFUNCTION()
virtual void OnItemOverlap (
UPrimitiveComponent* OverlapPrimitiveComp, // 오버랩이 발생한 자기 자신 = 구체 콜리전
AActor* OtherActor, // 콜리전 컴포넌트에 부딪힌 상대 액터 = 플레이어
UPrimitiveComponent* OtherComp, // 상대 액터의 충돌 컴포넌트 = 플레이어의 충돌 캡슐 컴포넌트
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult) = 0;
virtual void OnItemEndOverlap (
UPrimitiveComponent* OverlapPrimitiveComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex) = 0;
virtual void ActivateItem (AActor* Activator) = 0;
virtual FName GetItemType() const = 0;
};
24-26에 해당하는 코드는 물리 연산과 관련. 추후에 공부해보자.
5️⃣아이템 블루프린트 생성 및 설정
1) 아이템 블루프린트 생성
2) 아이템 스테틱 메시 할당하기
3) 콜리전 프리셋 설정
- SphereComponent의 충돌 범위(Sphere Radius) 조정
- 콜리전 프리셋 설정(C++ 코딩으로 진행!)
프리셋 종류:
NoCollision | 충돌 처리 off(하늘, 유체) |
BlockAll | 모든 충돌을 막는다(벽, 바닥) |
OverlapAll | 트리거 존(감지 센서)으로 쓰임. |
BlockAllDynamic | Dynamic은 움직이는 객체와 상호작용을 한다. 즉, 플레이어랑만 상호작용하게 할 때 쓰인다. |
OverlapAllDynamic ✅ | 센서용으로 주로 쓰임. 움직이는 것들만 계산 처리하기 때문에 비교적 간단한 계산처리. |
Pawn | 폰 타입 객체를 충돌 처리 |
Custom | Query는 충돌(Overlap과 Hit)만 감지. 하지만 그 외에 물리적 반응은 하지 않는 설정. 보통은 Collisiom Enabled 설정을 한다. |
OverlapAll과 OverlapAllDynamic의 차이점
- OverlapAll: 플레이어가 게임 환경에서 모든 오브젝트와 상호작용해야 하는 경우에 사용됩니다. 예를 들어, 방 안에 있는 모든 오브젝트를 감지하는 트리거 영역을 만들 때 유용합니다.
- OverlapAllDynamic: 움직이는 오브젝트만 감지해야 하는 상황에서 사용됩니다. 예를 들어, 플레이어나 NPC가 들어올 때만 반응해야 하는 문을 자동으로 열어주는 트리거 영역을 설정할 때 유용합니다. 정적인 오브젝트에는 반응하지 않습니다.
6️⃣ 아이템 종류별 구현
💰 코인 아이템 (CoinItem)
✅ CoinItem.h
#pragma once
#include "CoreMinimal.h"
#include "BaseItem.h"
#include "CoinItem.generated.h"
UCLASS()
class SPARTAPROJECT_API ACoinItem : public ABaseItem
{
GENERATED_BODY()
public:
ACoinItem();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 PointValue; // 획득 점수
virtual void ActivateItem(AActor* Activator) override; //오버라이드해서 각각의 기능 설정해주기
};
✅ CoinItem.cpp
#include "CoinItem.h"
ACoinItem::ACoinItem()
{
PointValue = 0;
ItemType = "DefaultCoinItem";
}
void ACoinItem::ActivateItem(AActor* Activator)
{
if (Activator && Activator->ActorHasTag("Player"))
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green,
FString::Printf(TEXT("Player gained %d points!"), PointValue));
DestroyItem();
}
}
📌 설명:
플레이어가 코인을 획득하면 점수 증가
획득 후 아이템 삭제
✅ BigCoinItem.cpp
#include "BigCoinItem.h"
ABigCoinItem::ABigCoinItem()
{
PointValue = 50;
ItemType = "Big Coin";
}
void ABigCoinItem::ActivateItem(AActor* Activator)
{
Super::ActivateItem(Activator);
}
CoinItem.cpp에서 아이템을 지워주기 때문에 부모 호출만 해주면 된다.
SmallCoin.cpp도 이와 동일하다
❤️🩹 힐링 아이템 (HealingItem)
✅ HealingItem.h
#pragma once
#include "CoreMinimal.h"
#include "BaseItem.h"
#include "HealingItem.generated.h"
UCLASS()
class SPARTAPROJECT_API AHealingItem : public ABaseItem
{
GENERATED_BODY()
public:
AHealingItem();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Healing")
int32 HealAmount;
virtual void ActivateItem(AActor* Activator) override;
};
✅ HealingItem.cpp
#include "HealingItem.h"
AHealingItem::AHealingItem()
{
HealAmount = 20;
}
void AHealingItem::ActivateItem(AActor* Activator)
{
if (Activator && Activator->ActorHasTag("Player"))
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green,
FString::Printf(TEXT("Player healed %d HP!"), HealAmount));
DestroyItem();
}
}
- 플레이어가 힐링 아이템을 획득하면 체력 회복
- 힐링 후 아이템 삭제
💣 지뢰 아이템 (MineItem)
✅MineItem.h
#pragma once
#include "CoreMinimal.h"
#include "BaseItem.h"
#include "MineItem.generated.h"
UCLASS()
class START_API AMineItem : public ABaseItem
{
GENERATED_BODY()
public:
AMineItem();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
USphereComponent* MineCollision;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item)
int32 ExplosiveDelay;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item)
int32 ExplosiveRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item)
int32 ExplosiveDamage;
FTimerHandle ExplosiveTimerHandle;
virtual void ActivateItem (AActor* Activator) override;
void Explode();
};
- `FTimerHandle ExplosiveTimerHandle;` : 타이머 핸들러
- Explode: 타이머가 만료되었을 때 실제 폭발 처리를 담당하는 메서드
✅ MineItem.cpp
#include "MineItem.h"
#include "Components/SphereComponent.h"
AMineItem::AMineItem()
{
ExplosiveDelay = 5;
ExplosiveRadius = 300;
ExplosiveDamage = 30;
ItemType = "Mine";
MineCollision = CreateDefaultSubobject<USphereComponent>(TEXT("MineCollision"));
MineCollision->InitSphereRadius(ExplosiveRadius);
MineCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
MineCollision->SetupAttachment(Scene);
}
void AMineItem::ActivateItem(AActor* Activator)
{
// 게임 월드에는 타이머 핸들러들을 관리하는 타이머 매니저가 있다.
GetWorld()->GetTimerManager().SetTimer(
ExplosiveTimerHandle, // 타이머 핸들러
this, // 해당 객체
&AMineItem::Explode, // 동작 메서드
ExplosiveDelay, // 시간
false); // 반복할 것인가?
}
void AMineItem::Explode()
{
TArray<AActor*> OverlappingActors; // 범위 내에 겹친 액터들을 저장해 줄 배열
MineCollision->GetOverlappingActors(OverlappingActors);
for (AActor* Actor : OverlappingActors)
{ if (Actor && Actor->ActorHasTag("Player"))
{ GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("Player damaged %d !"), ExplosiveDamage));
}
} DestroyItem();
}
7️⃣ 최종 테스트 및 디버깅 🎯
✅ 테스트 방법
1. 아이템을 레벨에 배치하고 플레이어가 접근할 때 정상적으로 획득되는지 확인한다.
2. 디버그 메시지(GEngine->AddOnScreenDebugMessage)를 활용하여 이벤트 발생 여부 확인.
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red,
FString::Printf(TEXT("Player damaged %d !"), ExplosiveDamage));
3. 콜리전 디버깅 모드(Alt + C 단축키)를 활용하여 충돌 범위를 시각적으로 확인한다.
4. 런타임 중에는 콘솔 명령어 show collision으로 확인할 수 있다.
show collision
8️⃣ 마무리 및 추가 고민할 점 🤔
✅ 오늘 구현한 내용
- Overlap 이벤트를 활용한 아이템 획득 시스템 구축
- 아이템 인터페이스, 기본 아이템 클래스와 다양한 아이템 유형 추가!
- 아이템 이벤트 처리
💡 앞으로 추가할 기능?
- UI에 획득한 아이템 정보 표시 or 아이템에 의한 플레이어 정보와의 연계(ex. HP Bar)
- 아이템이 사라질 때 애니메이션 효과 추가