아이템 시스템

Unreal 이론|2024. 10. 30. 16:21

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

트리거 박스의 구현

  • 루트에 트리거를 설정하고 자식에 메시 컴포넌트 부착
  • 이펙트는 기본 값으로 비활성화 상태로 두고 오버랩 이벤트 발생시 발동되도록 설정
  • 이펙트 종료시 액터가 제거되도록 설정
// ABItemBox.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#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; // Loot Component

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

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

	// 캐릭터가 박스를 밟았을 때 이펙트
	UFUNCTION() // Trigger에 원래 있는 델리게이트에 연결할 함수
	void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

	UFUNCTION()
	void OnEffectFinished(class UParticleSystemComponent* ParticleSystem);
};
  • 헤더에 먼저 Loot Component인 Trigger를 선언하고, Mesh와 Effect도 선언해준다.
  • BoxComponent에 존재하는 델리게이트 OnComponentBeginOverlap과 PatricleSystemComponent의 델리게이트 OnSystemFinished에 연결할 함수를 각각 선언해준다.
// ABItemBox.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#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()
{
	// CreateDefaultSubobject 함수로 객체 생성
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Effect"));

	// 액터 내부에 Scene Setup
	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);

	// Effect 끝난 뒤 델리게이트 사용
	Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}

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

 

  • 액터의 RootComponent를 Trigger로 설정하고, Trigger에 Mesh와 Effect를 Setup해준다.
  • Trigger의 충돌은 CPROFILE_ABTRIGGER로 ABCollision.h에 선언했던걸 사용
  • OnComponentBeginOverlap 델리게이트는 컴포넌트간에 겹칠때 호출되는 이벤트. 워프포인트, 캐릭터의 히트판정시 데미지 구현 등등 많이 쓴다고 하니 잘 기억해두자.
  • OnComponentBeginOverlap에 바인딩한 OnOverlapBegin 함수에서는 Effect를 true로 설정해 재생하고, 발동한 상자 메시를 지우고 Collision을 없앤다. Effect 끝난 뒤 OnSystemFinished 델리게이트를 다시 바인딩
  • OnSystemFinished 델리게이트가 실행되면 최종적으로 OnEffectFinished 함수에서 이펙트를 없앤다.

이 아이템 상자에 아이템 정보를 넣어 무기를 습득하도록 구현

// ABItemData.h

// Fill out your copyright notice in the Description page of Project Settings.

#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(EditAnywhere, BlueprintReadWrite, Category = Type)
	EItemType Type;
};
  • 아이템 데이터를 에셋으로 관리할 수 있도록 PrimaryDataAsset 상속받은 ItemData 클래스 생성
// ABWeaponItemData.h

// Fill out your copyright notice in the Description page of Project Settings.

#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;
};
  • ItemData를 상속받은 WeaponItemData 클래스 생성해 확장 구현
  • 별도로 프로젝트 폴더에 Item 클래스들을 관리하기 위한 폴더 생성, 각각 DataAsset 생성

 

프로젝트에서 사용할 아이템 에셋

  • 총 3가지 종류의 아이템 타입 지정
  • 무기 타입: 캐릭터에 무기 부착(무기에 의한 부가 스탯 강화)
  • 포션 타입: 캐릭터의 HP 회복
  • 스크롤 타입: 캐릭터의 기본 스탯 상승
  • 실제 스탯 구현은 차후 강좌

프로젝트의 주요 레이어

  • 데이터 레이어: 게임을 구성하는 기본 데이터 (스탯 정보, 캐릭터 레벨 테이블 등)
  • 미들웨어 레이어: 게임에 사용되는 미들웨어 모듈 (UI, 아이템, 애니메이션, AI 등)
  • 게임 레이어: 게임 로직을 구체적으로 구현하는데 사용 (캐릭터, 게임모드 등)
  • 위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정
  • ex) 아이템을 먹어 캐릭터가 행동을 수행하도록 아래에서 위로 명령을 내리는 상황 -> 인터페이스 사용

 

  • ABCharacterItemInterface 클래스를 생성해 TakeItem 가상함수 생성
// ABCharacterItemInterface.h

// Fill out your copyright notice in the Description page of Project Settings.

#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;
};
  • ItemBox 클래스에 Item 언리얼 오브젝트 포인터 변수 추가
  • OnOverlapBegin 함수에 아이템 습득 추가
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	if (nullptr == Item) {
		Destroy();
		return;
	}

	IABCharacterItemInterface* OverlappingPawn = Cast<IABCharacterItemInterface>(OtherActor);
	if (OverlappingPawn) {
		OverlappingPawn->TakeItem(Item);
	}

	Effect->Activate(true);
	Mesh->SetHiddenInGame(true);
	SetActorEnableCollision(false);

	// Effect 끝난 뒤 델리게이트 사용
	Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
  • CharacterBase 클래스에 TakeItem 가상함수 구현 및 아이템 종류별 행동 구현할 Delegate 및 구조체 선언, Delegate에 바인딩 할 가상함수 3개 선언
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);
USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper {
	GENERATED_BODY()
	FTakeItemDelegateWrapper() {}
	FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDelegate(InItemDelegate) {}

	FOnTakeItemDelegate ItemDelegate;
};

protected:
	UPROPERTY()
	TArray<FTakeItemDelegateWrapper> TakeItemActions;

	virtual void TakeItem(class UABItemData* InItemData) override;

	virtual void DrinkPotion(class UABItemData* InItemData);
	virtual void EquipWeapon(class UABItemData* InItemData);
	virtual void ReadScroll(class UABItemData* InItemData);
  •  cpp 파일에서 구체적인 동작 구현
AABCharacterBase::AABCharacterBase()
{
	// ItemActions
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::EquipWeapon)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::DrinkPotion)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::ReadScroll)));
}

void AABCharacterBase::TakeItem(UABItemData* InItemData)
{
	// Item 종류 따라 캐릭터의 다른 동작 구현
	if (InItemData) {
		TakeItemActions[(uint8)InItemData->Type].ItemDelegate.ExecuteIfBound(InItemData);
	}
}

void AABCharacterBase::DrinkPotion(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"));
}
  • 생성자에 무기 스켈레탈 메시 부여, EquipWeapon 함수에서 적용
AABCharacterBase::AABCharacterBase()
{	
    // Weapon Component
	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
	// 그냥 트랜스폼 부여x, 캐릭터의 특정 본에 무기가 항상 부착되도록 소켓 이름 지정
	// hand_rSocket은 Infinity Warrior Character에 원래 존재하는 소켓 명
	Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
}

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData) {
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
	}
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}

 

소프트 레퍼런싱 vs 하드 레퍼런싱

  • 액터 로딩시 TObjectPtr로 선언한 언리얼 오브젝트도 따라서 메모리에 로딩됨
  • 이를 하드 레퍼런싱이라 함
  • 게임 진행에 필수적인 언리얼 오브젝트는 이렇게 선언 가능. 근데 아이템은?
  • 데이터 라이브러리에 1000종의 아이템 목록이 있으면 이걸 다 로딩할것인지? -> 메모리 부담 1000배
  • 필요한 데이터만 로딩하도록 TSoftObjectPtr로 선언하고 대신 에셋 주소 문자열 지정
  • 필요시 에셋 로딩하도록 구현 변경할 수 있으나 에셋 로딩 시간 소요됨
  • 현재 게임에서 로딩되어 있는 스켈레탈 메시 목록 살펴보기 -> 콘솔 명령어 [Obj List Class=SkeletalMesh]
  • 그럼 WeaponMesh를 SoftObjectPtr로 바꿔서 확인
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData) {
		if (WeaponItemData->WeaponMesh.IsPending()) {
			WeaponItemData->WeaponMesh.LoadSynchronous();
		}
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
	}
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}
  • 이렇게 바꾸면 처음 엔진 로딩시에는 스켈레탈 메시가 로딩되지 않고, 아이템을 먹었을 때 로딩된다.
  • 언리얼 엔진 최적화의 중요한 요소 [초기의 게임이 로드될 때 메모리 양 최소화]

'Unreal 이론' 카테고리의 다른 글

게임데이터 관리  (0) 2024.11.06
무한 맵의 제작  (0) 2024.10.31
캐릭터 스탯과 위젯  (1) 2024.10.27
캐릭터 콤보 액션  (0) 2024.10.22
캐릭터 애니메이션 설정  (0) 2024.10.21

댓글()