본문 바로가기
언리얼 C++ 게임 개발의 정석/10. 게임 데이터와 UI 위젯

10.1 액셀 데이터 활용

by ftftgop3 2024. 4. 22.

게임 인스턴스

게임 앱을 관리 하는 용도로 언리얼 엔진에서 제공 

캐릭터 스텟을 관리 하게 설계, 엑셀에 저장돼 있는 캐릭터 스텟 데이터 테이블을 사용

게임 앱이 초기화 될 때 캐릭터 스텟 테이터를 로드, 종료 시 스텟 데이터 보존

 

게임 인스턴스 생성 및 프로젝트에 적용

 

게임 앱 초기화 과정 

UGameInstance::Init ▶ AActor::PostInitializeComponents ▶ ABGameMode :: PostLogin 

▶ ABGameMode :: StartPlay, AActor ::BeginPlay 

순으로 동작이 끝나야 게임 앱이 정상적으로 시작

 

엑셀 파일 로드 방법

1. 엑셀을 CSV 파일 형식으로 변환 

2. CSV 파일의 테이블 각 열의 이름과 유형이 동일한 구조체 선언

3. FTableRowBase 구조체 상속 받은 FABCharacterData 구조체를 게임 인스턴스에 선언

 

구조체 선언 시 CSV Name 열 데이터는 언리얼 엔진에서 자동 키 값으로 사용 

Name 열은 선언에서 제외

 

ABGameInstance 예제

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

#pragma once

#include "ArenaBattle.h"
#include "Engine/DataTable.h"
#include "Engine/GameInstance.h"
#include "ABGameInstance.generated.h"

//구조체 선언, CSV 파일 읽기 위해서 FTableRowBase 상속
USTRUCT(BlueprintType)
struct FABCharacterData : public FTableRowBase
{
	GENERATED_BODY()

public:
	//CSV 파일의 열의 이름, 유형 선언
	FABCharacterData() : Level(1), MaxHP(100.0f), Attack(10.0f), DropExp(10), NextExp(30) {}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		int32 Level;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		float MaxHP;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		float Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		int32 DropExp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		int32 NextExp;
};

UCLASS()
class ARENABATTLE_API UABGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UABGameInstance();
	virtual void Init() override;

};

 

4. 컴파일 완료 시 CSV 테이터를 임포트 후 데이터 확인

 

5. 데이블 데이터를 선언 및 테이블 접근 함수 선언

UCLASS()
class ARENABATTLE_API UABGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UABGameInstance();

	virtual void Init() override;
	//Level 값을 입력 시 해당 테이블 리턴
	FABCharacterData* GetABCharacterData(int32 Level);

private:
	//테이블 로드 후 적용 시킬 테이블 객체
	UPROPERTY()
		class UDataTable* ABCharacterTable;
};
#include "ABGameInstance.h"
UABGameInstance::UABGameInstance()
{
	//테이블 위치
	FString CharacterDataPath = TEXT("/Game/Book/GameData/ABCharacterData.ABCharacterData");

	static ConstructorHelpers::FObjectFinder<UDataTable> DT_ABCHARACTER(*CharacterDataPath);
	ABCHECK(DT_ABCHARACTER.Succeeded());
	//변수에 테이블값 적용
	ABCharacterTable = DT_ABCHARACTER.Object;
	//정확하게 들어 왔는지 확인
	ABCHECK(ABCharacterTable->RowMap.Num() > 0);
}

void UABGameInstance::Init()
{
	Super::Init();
	//ABLOG(Warning, TEXT("DropExp of Level 20 ABCharacter : %d"), GetABCharacterData(20)->DropExp);
}

FABCharacterData* UABGameInstance::GetABCharacterData(int32 Level)
{
	//레벨에 해당하는 행값 리턴
	return ABCharacterTable->FindRow<FABCharacterData>(*FString::FromInt(Level), TEXT(""));
}

 

스텟 관리 액터 컴포넌트 제작

캐릭터에 부착 해 스탯에 대한 관리 하는 액터 컴포넌트 제작

레벨에 따라서 스탯 변경 및 게임 인스턴스에서 테이블 참조 해 관련 데이터 변경

 

ABCharacter 에 ABCharacterStatComponent 추가

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
    public:
    	UPROPERTY(VisibleAnywhere, Category = Stat)
	class UABCharacterStatComponent* CharacterStat;
}

#include "ABCharacterStatComponent.h"
AABCharacter::AABCharacter()
{
	CharacterStat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("CHARACTERSTAT"));
}

ABCharacterStatComponent.h

게임 인스턴스가 가지고 있는 테이블을 참조 해 레벨에 해당하는 데이터 로드

컴포넌트 초기화 시점에 데이터 로드 시작

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

#pragma once

#include "ArenaBattle.h"
#include "Components/ActorComponent.h"
#include "ABCharacterStatComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UABCharacterStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	virtual void InitializeComponent() override;


public:
	//새로운 레벨 맞는 스탯 결정
	void SetNewLevel(int32 NewLevel);

	//스탯 관련 변수는 private 로 지정
private:
	struct FABCharacterData* CurrentStatData = nullptr;

	UPROPERTY(EditInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		int Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPirvateAccess = true))
		float CurrentHP;
public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

};

 

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

#include "ABCharacterStatComponent.h"
#include "ABGameInstance.h"


// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{

	// TickComponent 비활성화 
	PrimaryComponentTick.bCanEverTick = false;
	//InitializeComponent 활성화
	bWantsInitializeComponent = true;

	Level = 1;
}

void UABCharacterStatComponent::InitializeComponent()
{
	Super::InitializeComponent();
	//처음 레벨로 초기화
	SetNewLevel(Level);
}
void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	//월드에서 게임 인스턴스 객체 참조
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	//게임 인스턴스 참조 확인
	ABCHECK(ABGameInstance != nullptr)

	//새로운 레벨에 맞는 현재 스탯 지정
	CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
	//데이터 업데이트
	if (CurrentStatData != nullptr)
	{
		Level = NewLevel;
		CurrentHP = CurrentStatData->MaxHP;
	}
	else
	{
		ABLOG(Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
	}
}

 

CurrentHP에 대미지 연동

대미지 만큼 CurrentHP 차감 및 0보다 작을면 캐럭터가 죽는 기능으로 수정

1. 기존은 캐릭터의 TakeDamge 함수로 처리 하는 부분 과  SetDamage 연결

2. ABCharacterStatComponent 에서 CurrentHP 소진 시 캐릭터에 알림 델리게이트 활용

 

DECLARE_MULTICAST_DELEGATE(FOnHPIsZeroDelegate);


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	GENERATED_BODY()
    public:
	//새로운 레벨 맞는 스탯 결정
	void SetNewLevel(int32 NewLevel);
    
	void SetDamage(float NewDamage);
	float GetAttack();
	FOnHPIsZeroDelegate OnHPIsZero;
}
void UABCharacterStatComponent::SetDamage(float NewDamage)
{
	ABCHECK(CurrentStatData != nullptr);
	//최소, 최대 값 지정
	CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP);

	//HP 가 0이하 시 델리게이트 실행
	if (CurrentHP >= 0.0f)
		OnHPIsZero.Broadcast();
}

float UABCharacterStatComponent::GetAttack()
{
	ABCHECK(CurrentStatData != nullptr, 0.0f);
	return CurrentStatData->Attack;
}

 

ABCharter 기능 추가

람다 형식으로 델리게이트에 함수 등록

SetDamage 함수 호출로 현재 HP 감소

캐릭터의 현재 공격 값을 가지고 AttackCheck () 호출

void AABCharacter::PostInitializeComponents()
{
	CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
		ABLOG(Warning, TEXT("OnHPIsZero"));
		ABAnim->SetDeadAnim();
		SetActorEnableCollision(false);
		});
}

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const & DamageEvent, AController * EventInstigator, AActor * DamageCauser)
{
	float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	ABLOG(Warning, TEXT("Actor : %s took Damage : %f"), *GetName(), FinalDamage);
	//캐릭터 스텟에 대미지 전달
	CharacterStat->SetDamage(FinalDamage);

	return FinalDamage;
}

void AABCharacter::AttackCheck()
{
	if (bResult)
	{
		//가비지 컬렉션 때문에 참조 문제가 생길 수 있어서 약 포인트 지정, 
		//Isvalid 로 액터 유효 확인
		if (HitResult.Actor.IsValid())
		{
			ABLOG(Warning, TEXT("Hit Actor Name: %s"), *HitResult.Actor->GetName());
			FDamageEvent DamageEvent;
			//캐릭터의 스텟에 따라 대미지 변경
			HitResult.Actor->TakeDamage(CharacterStat->GetAttack(), DamageEvent,
				GetController(), this);
		}
	}
	
}