본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 2 - 아이템 시스템 (8/15)

728x90
반응형

 

 

 

  • 정리
    • 아이템 상자와 캐릭터의 적용
      1. 기믹 구현을 위한 트리거 액터의 설계
      2. 데이터 에셋을 활용한 아이템 데이터 관리
      3. 의존성 분리를 위한 설계 구현
      4. 메모리 최적화를 위한 소프트 레퍼런싱의 구현

 

 

 

  • 트리거 박스의 구현
    • 루트에 트리거를 설정, 자식에 메시 컴포넌트를 부착
    • 이펙트는 기본 값으로 비활성, 오버랩 이벤트 발생시 발동되도록 설정
    • 이펙트 종료시 액터가 제거되도록 설정

 

 

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABItemBox.generated.h"

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AABItemBox();

protected:
	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UBoxComponent> Trigger;

	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UStaticMeshComponent> Mesh;

	UPROPERTY(VisibleAnywhere, Category = Effect)
	TObjectPtr<class UParticleSystemComponent> Effect;

	UFUNCTION()
	void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

	UFUNCTION()
	void OnEffectFinished(class UParticleSystemComponent* ParticleSystem);
};
  • AABItemBox.h

 

  • 아이템 박스를 만들기 위해 필요한 class들과 함수들을 추가해준다.
  • Trigger, Mesh, Effect
  • Trigger에 반응할 수 있는 OnOverlapBegin, Effect가 끝났을때를 알려주는 OnEffectFinished
#include "Item/ABItemBox.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "Physics/ABCollision.h"

// Sets default values
AABItemBox::AABItemBox()
{
	// 액터 구성
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Effect"));

	// 루트 컴포넌트 설정
	RootComponent = Trigger;
	Mesh->SetupAttachment(Trigger);
	Effect->SetupAttachment(Trigger);

	// 트리거의 콜리전 설정
	Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));

	// 오버랩되면 발생시킬 함수 바인딩
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> BoxMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1'"));
	if (BoxMeshRef.Object)
	{
		Mesh->SetStaticMesh(BoxMeshRef.Object);
	}
	Mesh->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
	Mesh->SetCollisionProfileName(TEXT("NoCollision"));

	static ConstructorHelpers::FObjectFinder<UParticleSystem> EffectRef(TEXT("/Script/Engine.ParticleSystem'/Game/ArenaBattle/Effect/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh'"));
	if (EffectRef.Object)
	{
		Effect->SetTemplate(EffectRef.Object);
		Effect->bAutoActivate = false;
	}
}

// 바인딩 된 함수 설정 및 로직 구성
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	Effect->Activate(true);
	Mesh->SetHiddenInGame(true);
	SetActorEnableCollision(false);

	// 이펙트가 끝날때 발생하는 델리게이트에 끝날때 Destroy를 해주는 함수를 바인딩해준다.
	Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}

void AABItemBox::OnEffectFinished(UParticleSystemComponent* ParticleSystem)
{
	Destroy();
}
  • AABItemBox.cpp

 

  • Actor 클래스를 상속받은 ABItemBox 클래스를 만들어 아이템박스를 구현해준다.
  • 액터를 구성해주고, 루트 컴포넌트와 콜리전 정보를 설정해주고, 오버랩될 경우 발생하는 델리게이트에 함수를 바인딩해준다.
  • 바인딩 된 함수가 실행되면 이펙트가 발생하게 되고, 아이템 박스의 메쉬는 사라지고, 콜리전 또한 반복 되지않도록 꺼준뒤, 아이템 박스 획득 이펙트가 종료되면 발생하는 델리게이트에 함수를 바인딩시켜 Destory()되는 로직을 추가시켜 준다.

 

아이템박스를 획득하고, 이펙트가 재생된 뒤 사라진다.

 

  • 다음은 아이템을 획득하면 아이템 정보와 함께 무기를 습득할 수 있도록 기능 구현
  • DataAsset을 활용하여 아이템 에셋을 구성해준다.
    • PrimaryDataAsset을 상속 받아 ABItemData 클래스를 만들어준다.
    • ABItemData를 상속받은 WeaponItemData 클래스를 만들어 확장시켜준다.

 

 

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ABItemData.generated.h"

UENUM(BlueprintType)
enum class EItemType :uint8
{
	Weapon = 0,
	Potion,
	Scroll
};

UCLASS()
class ARENABATTLE_API UABItemData : public UPrimaryDataAsset
{
	GENERATED_BODY()
	
public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Type)
	EItemType Type;
};
  • ABItemData.h
#pragma once

#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "ABWeaponItemData.generated.h"

UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, Category = Weapon)
	TObjectPtr<USkeletalMesh> WeaponMesh;
};
  • ABWeaponItemData.h

 

 

  • 아이템 에셋
    • 총 3가지 종류로 간단히 정리
    • 무기 타입
      • 캐릭터에 무기 부착
      • 무기에 의한 부가 스탯 강화
    • 포션 타입
      • HP 회복
    • 스크롤 타입
      • 기본 스탯 상승
  • 아이템 에셋의 관리
    • ItemData를 부모 클래스로 상속받은 세 가지 종류의 아이템 클래스를 선언

아이템 에셋의 관리

 

 

의존성 분리를 위한 설계 규칙 세우기

 

 

  • 프로젝트의 주요 레이어
    • 데이터 레이어 : 게임을 구성하는 기본 데이터
      • 스탯 정보, 캐릭터 레벨 테이블 등등..
    • 미들웨어 레이어 : 게임에 사용되는 미들웨어 모듈
      • UI, 아이템, 애니메이션, AI 등등..
    • 게임 레이어 : 게임 로직을 구체적으로 구현하는데 사용
      • 캐릭터, 게임 모드 등등..
    • 위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정하기

위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정

 

  • 상단의 레이어들은 하단의 레이어들을 직접 헤더를 추가해서 참조할 수 있지만, 하단에서 상단의 레이어는 직접 참조하지 않도록 규칙을 설계한다.
  • 하단에서 상단으로의 명령을 보내려고 할때는 인터페이스를 사용해서 전달한다.

 

#pragma once

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

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UABCharacterItemInterface : public UInterface
{
	GENERATED_BODY()
};

class ARENABATTLE_API IABCharacterItemInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void TakeItem(class UABItemData* InItemData) = 0;
};
  • ABCharacterItemInterface.h

 

  • 아이템을 먹었다는걸 캐릭터에게 전달해주기 위하여 ABCharacterItemInterface클래스를 만들어 준다.
// ...
#include "Interface/ABCharacterItemInterface.h"
#include "ABCharacterBase.generated.h"

// 확인용 로그
DECLARE_LOG_CATEGORY_EXTERN(LogABCharacter, Log, All);

// ...

// 아이템을 처리할 수 있게 델리게이트 선언
// 해당 델리게이트의 경우 다수를 배열로 관리하려고 한다. 이것 자체를 인자로 쓸 수가 없다.
// 이것을 배열로 관리하기위해서 쉬운 방법은 이것을 감싸는 구조체를 하나 만들어 주는것
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);

USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper
{
	GENERATED_BODY()
	FTakeItemDelegateWrapper(){}		// 생성자
	FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDelegate(InItemDelegate){}		// 인자를 받는 생성자
	
	FOnTakeItemDelegate ItemDelegate;
};

UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface, public IABCharacterWidgetInterface, public IABCharacterItemInterface		// 인터페이스 상속
{
	// ...
		
// Item Section
protected:

	//FTakeItemDelegateWrapper를 관리해줄 수 있는 배열 선언 (TakeItemActions)
	UPROPERTY()
	TArray<FTakeItemDelegateWrapper> TakeItemActions;

	virtual void TakeItem(class UABItemData* InItemData) override;
	// TakeItemActions에 바인딩될 함수들
	virtual void DrinkPostion(class UABItemData* InItemData);
	virtual void EquipWeapon(class UABItemData* InItemData);
	virtual void ReadScroll(class UABItemData* InItemData);
}
  • ABCharacterBase.h

 

  • 아이템을 처리할 수 있도록 델리게이트를 선언해주고, 아이템의 경우 여러 아이템이 있고 다수를 관리해야하기 때문에 배열로 관리하기 위해 FTakeItemDelegateWrapper라는 구조체를 만들어 관리해준다.
  • 생성자와 매개 변수를 가지는 생성자, 델리게이트를 구조체 내부에 선언해준다.
  • TArray를 통해 FTakeItemDelegateWrapper 를 관리할 구조체 배열을 만들어주고, 해당 구조체에 바인딩할 함수들도 선언해준다.
// ...
#include "Item/ABWeaponItemData.h"

// 확인용 로그
DEFINE_LOG_CATEGORY(LogABCharacter);

// Sets default values
AABCharacterBase::AABCharacterBase()		// 생성자
{
	// ...
		
	// Item Action
	// 인자를 받는 생성자에 즉석에서 생성해서 TakeItemActions 배열에 집어넣기 (열거형 순)
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::EquipWeapon)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::DrinkPostion)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::ReadScroll)));
}

// 아이템데이터를 인터페이스로부터 받았을때
void AABCharacterBase::TakeItem(UABItemData* InItemData)
{
	if (InItemData)
	{
		// Type을 가져오기 위해 #inlcude "Item/ABWeaponItemData"
		// ExecuteIfBound를 통해 해당 아이템 넘겨주기
		TakeItemActions[(uint8)InItemData->Type].ItemDelegate.ExecuteIfBound(InItemData);
	}
}

void AABCharacterBase::DrinkPostion(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Drink Potion"));
}

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}

void AABCharacterBase::ReadScroll(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Read Scroll"));
}
  • ABCharacterBase.cpp

 

  • ABItemData를 상속받은 ABWeaponItemDatainclue하고, 생성자에서 TakeItemActions에 인자를 받는 생성자로 즉석에서 생성하여 배열에 집어넣어준다.
  • 정상적으로 동작하는지를 위해 로그도 심어준다.
UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
	// ...
	
	// 부모 클래스형을 지정했기 때문에 Weapon, Potion, Scroll 중 하나가 여기에 대응된다.
	UPROPERTY(EditAnywhere, Category = Item)
	TObjectPtr<class UABItemData> Item;
	
	// ...
};
  • ABItemBox.h

 

  • ABItemBox클래스에는 아이템 정보를 넣어주기 위해 ABItemData변수를 만들어 준다.
// ...

#include "Interface/ABCharacterItemInterface.h"

// ...

// 바인딩 된 함수 설정 및 로직 구성
// 아이템 획득했을 때
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	// 아이템에 꽝이 존재할 경우
	if (nullptr == Item)
	{
		Destroy();
		return;
	}

	// Item이 있다면 TakeItem에 인자로 넘겨주기
	IABCharacterItemInterface* OverlappingPawn = Cast<IABCharacterItemInterface>(OtherActor);
	if (OverlappingPawn)
	{
		OverlappingPawn->TakeItem(Item);
	}
	
	// ...
}
  • ABItemBox.cpp

 

  • 인터페이스를 include 하고, OnOverlapBegin함수를 통해 아이템을 획득했을 때 아이템 정보에 따라 로직을 구성해준다.

로그가 정상적으로 찍히는걸 확인할 수 있다.
ItemBox에 설정한 Type별로 로그가 정상적으로 찍힘

 

 

AABCharacterBase::AABCharacterBase()
{
	// 생성자 ...
	// Weapon Component
	// Weapon을 만들어준뒤, 캐릭터의 Hand_rSocket이라는 조인트에 Attach해준다.
	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
	Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
}

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));

	// ABItemData인 InItemData를 ABWeaponItemData로 캐스팅해준뒤, 올바른 캐스팅이라면 Weapon의 스켈레탈 메쉬를 WeaponItemData의 WeaponMesh로 변경해준다.
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData)
	{
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
	}
}
  • ABCharacterBase.cpp

 

  • Weapon에 대한 내용을 초기화해준다. 캐릭터의 hand_rSocket이라는 조인트에 장착될 수 있도록 위치시켜준다. (현재는 무기 1종만 구현하기 위해)
  • 캐릭터가 Weapon을 획득했을 때, 무기를 장착할 수 있도록 해준다.
  • 만약, 무기 위치가 맞지 않는다면 캐릭터 스켈레톤의 hand_rSocket 위치를 조금 수정해주도록 하자.

 

ItemBox에서 Weapon을 획득한 뒤, Weapon을 장착한 모습

 

 

 

  • 소프트 레퍼런싱 vs 하드 레퍼런싱
    • 액터 로딩시 TObjectPtr로 선언한 언리얼 오브젝트도 따라서 메모리에 로딩이 된다. 이를 하드 레퍼런싱이라고 한다.
    • 게임 진행에 필수적인 언리얼 오브젝트의 경우 이렇게 선언해도 되지만, 아이템의 경우 데이터 라이브러리에 1000종의 아이템 목록이 있을 때 이를 모두 다 로딩한다는건 큰 부담이 될 것이다.
    • 그렇기 때문에 필요한 데이터만 로딩하도록 TSoftObjectPtr로 선언하고 대신 에셋 주소 문자열을 지정한다. 필요시에 에셋을 로딩하도록 구현을 변경할 수 있으나 에셋 로딩 시간이 소요 된다.

 

Cmd : Obj List Class=SkeletalMesh

  • 플레이를 하고 `키를 눌러 Obj List Class=SkeletalMesh 명령어를 입력하면 현재 로딩된 SkeletalMesh에 대한 정보가 나오게 된다.
  • 아직 아이템을 먹지 않았는데도 Weapon 아이템 박스를 획득했을 때 나오는 SkeletalMesh를 하드 레퍼런싱으로 참조하고 있기 때문에 기본으로 로딩되고 있다.

 

#pragma once

#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "ABWeaponItemData.generated.h"

UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, Category = Weapon)
	TSoftObjectPtr<USkeletalMesh> WeaponMesh;
};
  • ABWeaponItemData.h

 

  • TObjectPtrTSoftObjectPtr로 변경해준다.
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	//UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));

	// ABItemData인 InItemData를 ABWeaponItemData로 캐스팅해준뒤, 올바른 캐스팅이라면 Weapon의 스켈레탈 메쉬를 WeaponItemData의 WeaponMesh로 변경해준다.
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData)
	{
		// 아직 로딩이 되지 않았다면
		if (WeaponItemData->WeaponMesh.IsPending())
		{
			// LoadSynchronous()를 통해 로딩
			WeaponItemData->WeaponMesh.LoadSynchronous();
		}
		//Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
	}
}
  • ABWeaponItemData.cpp

 

  • TSoftObjectPtr로 변경했기 때문에 로딩이 안될 경우가 존재한다. 그렇기 때문에 IsPending()을 통해 아직 로딩이 되지 않았다면 LoadSynchronous()로 로딩을 해주고, Get()을통해 SkeletalMesh를 가져와 준다.

 

 

아이템을 획득하기전
아이템을 획득한 후

 

  • 아이템을 획득하기전에는 SkeletalMesh가 2개로 나오지만, 아이템을 획득하고 나면 SkeletalMesh가 3개 로딩되어 있는걸 확인할 수 있다.
  • 이렇게 초기의 게임이 로드가 될 때 메모리 양을 최소화시키는 것이 언리얼엔진 최적화의 중요한 요소라고 할 수 있다.

 

 

 

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런

청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래

www.inflearn.com

 

 

 

728x90