1. 목표
게임상에서 캐릭터와 아이템 간의 충돌이 발생하면 특정 이벤트가 발생하도록 구현해야 한다.
구현해야 할 것은 아래와 같다.
- 아이템 클래스와 그것을 상속받은 여러 자식 아이템 클래스들
- 아이템 (actor)와 캐릭터의 충돌 감지
전체적인 구조
- 인터페이스 클래스
- base item 클래스
- coin item 클래스
- big coin item 클래스
- small coin item 클래스
- healing item 클래스
- mine item 클래스
- coin item 클래스
- base item 클래스
2. 아이템 클래스 만들기
2 - 1. 인터페이스 클래스 만들기
우리가 C++에서 순수 가상 함수를 가지는 추상 클래스를 만들었던 것처럼 언리얼 엔진에서도 인터페이스를 만들어 계층 구조를 구현할 수 있다.
- 인터페이스 클래스
- 반드시 구현해야할 함수 목록만을 미리 정의해두고, 실제 함수의 동작은 상속받는 클래스에서 구현하는 것
- 언리얼 C++에서는 UInterface를 상속받아서 구현하게 된다. (아래 사진의 언리얼 인터페이스)
- 인터페이스와 상속의 차이점
- 상속
- 부모 클래스의 모든 속성과 기능을 자식 클래스가 물려받는 것
- 부모 클래스의 로직을 자식이 사용할 수도 있고, 오버라이딩해서 재정의 할 수도 있다.
- 인터페이스
- 인터페이스에는 반드시 만들어야 하는 함수의 원형들만 정의되어 있다.
- 해당 함수들의 동작은 자식 클래스에서 작성해야 한다.
- 즉, 상속은 부모의 구현을 가져다 쓰는 것이고 인터페이스는 함수의 틀만 자식이 가져와서 구현하는 것이다.
- 상속
2 - 2. 언리얼 C++ 인터페이스 코드
- UInterface를 상속받아서 클래스를 만들면, 아래와 같은 구조를 가지게 된다.
- UINTERFACE가 붙어있는 UItemInterface 클래스
- 리플렉션을 위해 자동으로 만들어진 부분
- 언리얼 내부의 동작을 한다.
- IItemInterface 클래스
- 실제 우리가 코드를 작성해야 하는 부분
- 순수 가상 함수를 이 부분에 작성하면 된다.
- UINTERFACE가 붙어있는 UItemInterface 클래스
- 인터페이스 클래스라서 cpp 파일, 즉 구현부는 필요가 없지만 언리얼 내부 동작에 문제가 생길수도있으니 빈 cpp파일 그대로 두는 것이 좋다.
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ItemInterface.generated.h"
UINTERFACE(MinimalAPI)
class UItemInterface : public UInterface
{
GENERATED_BODY()
};
class SCC_PROJECT_API IItemInterface
{
GENERATED_BODY()
public:
}
- 모든 아이템들이 해야하는 동작을 순수가상함수 형태로 구현한 모습
- OnItemOverlap 함수와 OnItemEndOverlap 함수는 아래에서 추가로 설명할 예정
- 이때 이 함수들을 UFUNCTION 매크로를 통해 리플렉션에 등록해 주어야한다.
- 위 함수들을 추후에 AddDynamic을 통해 런타임 바인딩 해줄 것이기 때문
- 만약 인터페이스 클래스에서 등록해준 경우 상속받은 자식 클래스에서는 자동으로 등록된다. (자식 클래스에서는 UFUNCTION 안써도 괜찮다)
- 참고) type을 return 하는 함수에서 return 값을 FName으로 한 이유는 FName이 FString보다 빠르기 때문이다.
- OnItemOverlap 함수와 OnItemEndOverlap 함수는 아래에서 추가로 설명할 예정
class SCC_PROJECT_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 void ActivateItem(AActor* Activator) = 0;
virtual FName GetItemType() const = 0;
};
2 - 3. 인터페이스 클래스를 상속 받은 최상위 부모 클래스
- 구현 내용
- 인터페이스 클래스를 include 한 후, 상속을 해주어야 한다.
- 인터페이스 클래스의 모든 순수 가상 함수를 구현해주어야 한다.
- 추가적으로 모든 아이템 클래스에 필요한 컴포넌트를 추가해 주면 된다.
//BaseItem.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"
class USphereComponent;
UCLASS()
class SCC_PROJECT_API ABaseItem : public AActor, public IItemInterface
{
GENERATED_BODY()
public:
ABaseItem();
protected:
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 FName GetItemType() const override;
virtual void DestroyItem();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "item")
FName ItemType;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
TObjectPtr<USceneComponent> SceneComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
TObjectPtr<USphereComponent> CollisionComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
TObjectPtr<UStaticMeshComponent> StaticMeshComp;
};
2 - 4. 최상위 부모 클래스를 상속받은 자식 클래스들
- 맨 위에서 언급한 구조대로 클래스들을 추가해 주었다.
- 생각할 점
- 인터페이스 클래스가 아니라 BaseItem 클래스(최상위 부모 클래스) 를 상속하면 된다.
- 부모 클래스의 함수 중, 필요한 함수를 재정의 혹은 사용하면 된다.
- Coin 클래스 코드 예시
- BaseItem을 상속하였고,
- ActivateItem 함수를 override 하였다.
//CoinItem.h
#pragma once
#include "CoreMinimal.h"
#include "BaseItem.h"
#include "CoinItem.generated.h"
UCLASS()
class SCC_PROJECT_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 = "Default Coin";
}
void ACoinItem::ActivateItem(AActor* Activator)
{
if (Activator && Activator->ActorHasTag("Player"))
{
GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Yellow, FString::Printf(TEXT("Player gain coins: %d"), PointValue));
DestroyItem();
}
}
- SmallCoinItem 클래스 코드 예시
- CoinItem 클래스를 상속하였고,
- CoinItem 클래스의 ActivateItem을 오버라이드하고, Super를 통해 부모의 함수를 call했다.
//SmallCoinItem.cpp
#include "SmallCoinItem.h"
ASmallCoinItem::ASmallCoinItem()
{
PointValue = 10;
ItemType = "Small Coin";
}
void ASmallCoinItem::ActivateItem(AActor* Activator)
{
Super::ActivateItem(Activator);
}
3. 충돌 처리
3 - 1. Collision
- 언리얼 엔진에서 Static Mesh를 더블클릭해서 들어간 후, 단순 콜리전을 표시하면 아래 사진처럼 Mesh 자체적으로 가지고 있는 콜리젼을 볼 수 있다.
- 그러나 우리가 게임을 구현할 때 충돌 처리를 위해서는 Static Mesh를 감싸는 추가적인 Collision을 만들어 주는 것 이 좋다. (아래 사진 참고)
- Collision에는 크게 두가지 방식이 있다.
- 오버랩
- 물리적으로 부딪히는 것 없이 액터들이 서로 겹치는지를 체크하는 것
- 아이템 획득, 트리거 존 감지 등에 사용할 수 있다.
- 히트
- 실제 물리 충돌이 일어날 때 발생하는 것 (막히고, 충돌하는 등)
- 총알이 벽에 부딪히는 경우 등에 사용할 수 있다.
- 오버랩
3 - 2. Collision Preset
위에서 언급한 오버랩과 히트를 위해 어떤 것을 오버랩하고 어떤 것을 히트할지 정하는 것이 Collision Preset이다.
- 주요 Collision Preset
- NoCollision
- 충돌 감지x
- overlap과 hit 모두 발생하지 않음
- 단순 장식용 오브젝트(구름)에 사용
- BlockAll
- 모든 객체와 충돌
- overlap 이벤트는 발생하지 않음
- 벽이나 바닥 등
- OverlapAll
- 모든 객체와 Overlap 이벤트 발생
- 트리거 존, 감지 센서, 투명 오브젝트에 사용
- BlockAllDynamic
- 움직이는 객체만 충돌
- 플레이어와 물리 오브젝트간의 상호작용
- 아이템과 아이템의 충돌을 막아서, 성능을 향상
- OverlapAllDynamic (우리가 구현할 아이템 클래스)
- 움직이는 객체와 Overlap 발생
- 플레이어가 근처에 있는지 확인하는 아이템, 센서 등
- 충돌을 하지 않는 Overlap 발생시킬 때
- Pawn
- 플레이어나 AI와 같은 Pawn 을 상속받은 대상들의 충돌 감지
- Custom
- 개발자가 직접 만들 수 있다.
- NoCollision
3 - 3. Collision Enabled
충돌이 켜져 있다면, 물리적인 처리를 하는지 하지 않는지를 체크하는 부분이다.
- No Collision : 충돌 비활성화
- Query Only : Overlap, Hit등 충돌 이벤트는 감지하지만, 물리적 처리는 안함
- Physics Only : 물리처리만 진행하고, Overlap, Hit 이벤트는 발생안함
- Query and Physics : 충돌 이벤트 + 물리 처리 둘 다 진행
3 - 4. C++로 콜리전 프리셋 설정
- SetCollisionProfileName 함수로 설정
- 캐릭터는 기본적으로 "Pawn" 으로 설정되어 있다.
//BaseItem.cpp
ABaseItem::ABaseItem()
{
...
CollisionComp->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
...
}
3 - 5. OnComponentBeginOverlap & OnComponentEndOverlap
이 함수들을 통해서 overlap 체크를 할 수 있다. 이벤트 바인딩을 통해 런타임에 바인딩을 하도록 한다.
- 이벤트 바인딩
- OnComponentBeginOverlap 이 함수를 통해 받아온 정보를 토대로 우리가 원하는 함수를 실행해 주는 것이다.
- 주의할 점은 함수 시그니처를 잘 맞춰야 한다는 점이다.
- 함수 시그니처
- 각각 BeginOverlap과 EndOverlap에 바인딩 할 수 있는 함수의 시그니처 모습이다.
- 매개변수 설명
- OverlappedComp : 오버랩이 발생한 자기 자신의 컴포넌트
- OtherActor : 오버랩이 발생한(충돌한) 다른 Actor (예를 들어 플레이어)
- OtherComp : 충돌한 다른 Actor의 충돌 감지된 컴포넌트 (예를 들어 플레이어의 캡슐 컴포넌트)
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;
//BaseItem.cpp
ABaseItem::ABaseItem()
{
...
//이벤트 바인딩
CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
CollisionComp->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}
3 - 6. 태그 시스템을 이용한 충돌 체크
언리얼 엔진에서도 Tag 시스템을 통해 충돌한 Actor의 Tag를 체크할 수 있다.
- 플레이어 캐릭터를 선택 후 Actor 검색 후 아래에 "태그" 에서 원하는 이름으로 태그를 추가
- C++에서는 아래와 같이 지정해 줄 수 있다.
//Player Character
APlayerCharacter::APlayerCharacter()
{
...
Tags.Add("Player");
...
}
- C++에서 사용
- ActorHasTag 함수를 통해 Tag를 검사할 수 있다.
//BaseItem.cpp
void ABaseItem::OnItemOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && OtherActor->ActorHasTag("Player"))
{
ActivateItem(OtherActor);
}
}
3 - 7. 추가 기억할 만한 구현들
- GetOverlappingActors
- 충돌된 액터들을 모두 가져와서 TArray 안에 넣어주는 함수
void AMineItem::Explode()
{
TArray<AActor*> OverlappingActors;
ExplosionCollisionComp->GetOverlappingActors(OverlappingActors);
for (AActor* Actor : OverlappingActors)
{
if (Actor && Actor->ActorHasTag("Player"))
{
//폭발 시 처리
}
}
DestroyItem();
}
- 타이머
- ExplosionDelay 뒤에 Explode 함수를 발동시킨다.
- 마지막 파라메터를 false로 해서 반복을 안하도록 만들었다.
void AMineItem::ActivateItem(AActor* Activator)
{
//타이머 핸들러
GetWorld()->GetTimerManager().SetTimer(
ExplosionTimerHandle
, this
, &AMineItem::Explode
, ExplosionDelay
, false //반복 안함
);
}
'Unreal Engine' 카테고리의 다른 글
Unreal Engine - Game Loop (0) | 2025.02.09 |
---|---|
Unreal Engine - 아이템 스폰 및 캐릭터와 연동 (0) | 2025.02.07 |
Unreal Engine - Simple Niagara (with cpp) (0) | 2025.02.04 |
Unreal Engine - Data Table (0) | 2025.02.03 |
UE5 Issues : C++ 충돌 처리 (1) | 2025.01.27 |