UE4 - C++

UE4 - 게임데이터와 UI 위젯

불타는버스 2021. 11. 4. 14:13

이득우의 C++ 11장

 

엑셀데이터의 활용

캐릭터의 스탯은 보통 변하지 않는 데이터이므로 앱이 초기화 할때 불러들인다.

 

게임 인스턴스 - 앱 관리 용도로 언리얼에서 제공하는 기능

 

게임 인스턴스를 이용해 스탯 데이터를 불러들이면, 게임 앱이 종료 될 때 까지 스탯 데이터가 보존된다.

 

파일  - 새로운 C++ 클래스 만들기
모든 클래스 표시 후, GameInstance를 부모로 지정한다,이름은 ABGameInstance
새로만든 ABGameInstance를 프로젝트 세팅으로 설정한다.

ABGameInstance의 초기화는 앱 시작과 동시에 이루어진다.

 

*언리얼 엔진의 실행 순서

게임 앱의 초기화 -> 레벨에 속한 액터 초기화 -> 플레이어의 로그인 및 게임시작

 

ABCharacterData.csv
0.00MB
ABCharacterData.xlsx
0.01MB

 

 

11장에 쓰이는 엑셀 데이터이다.

내용물 구성

언리얼에서 엑셀 파일 그대로 사용이 불가능하여, csv로 형변환 시켜야한다.

엑셀상에서 csv 형식으로 저장하는걸 제공해준다.

csv로 저장하면 쉼표 단위로 메모장처럼 열린다.

 

이 파일을 부르기 위해선 테이블 데이터와 각 열 이름과 유형이 동일한 구조체를 선언해야한다.

 

ABGameInstance.h

// 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"

//구조체 선언
USTRUCT(BlueprintType)
struct FABCharacterData : public FTableRowBase
{
	GENERATED_BODY()

public:
	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")
		int32 MaxHP;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
		int32 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;
};

ABGameInstance.cpp

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


#include "ABGameInstance.h"

UABGameInstance::UABGameInstance()
{

}

void UABGameInstance::Init()
{

}

CSV 파일 드래그시 옵션.

드래그 옵션을 ABCharacterData로 바꾸고 임포트한다.

입력한 수치에 맞게 제대로 들어가있다.

ABGameInstane.h

class ARENABATTLE_API UABGameInstance : public UGameInstance
{
	...
    FABCharacterData* GetABCharacterData(int32 level);
private:
	UPROPERTY()
		class UDataTable* ABCharacterTable;
}

ABGameInstance.cpp

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


#include "ABGameInstance.h"

UABGameInstance::UABGameInstance()
{
	FString CharacterDataPath = TEXT("/Game/Book/GameData/ABCharacterData.ABCharacterData");
	static ConstructorHelpers::FObjectFinder<UDataTable> DT_ABCHARACTER(*CharacterDataPath);
	ABCharacterTable = DT_ABCHARACTER.Object;
}

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

FABCharacterData* UABGameInstance::GetABCharacterData(int32 level)
{
	return ABCharacterTable->FindRow<FABCharacterData>(*FString::FromInt(level), TEXT(""));
}

해당 코드는 20레벨의 DropExp 수치를 표기해주는 코드이다. 테이블에서 제대로 받아지는지 확인한다.

20레벨 DropExp로 105가 나온다.

액터 컴포넌트의 제작

스탯에 대한 데이터를 관리하는 액터 컴포넌트를 캐릭터에 부착해보는 식으로 작업한다.

 

파일 - 새로운 C++ 클래스 추가 - 액터 컴포넌트.이름은 ABCharacterStatComponent

ABCharacter에 부착한다.

 

ABCharacter.h

public:
	...
   	UPROPERTY(VisibleAnyWhere, Category = Stat)
	class UABCharacterStatComponent* CharacterStat;

ABCharcater.cpp

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


#include "ABCharacter.h"
#include "ABAnimInstance.h"
#include "ABGhostTrail.h"
#include "ABWeapon.h"
#include "ABCharacterStatComponent.h"
#include "DrawDebugHelpers.h"

// Sets default values
AABCharacter::AABCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SPRINGARM"));
	CameraS = CreateDefaultSubobject<UCameraComponent>(TEXT("CAMERA"));

	SpringArm->SetupAttachment(GetCapsuleComponent());
	CameraS->SetupAttachment(SpringArm);

	CharacterStat = CreateDefaultSubobject<UABCharacterStatComponent>(
		TEXT("CHARACTERSTAT"));
    ...

Character Stat이 상속됨을 확인 할 수 있다.

이제 ABCharacterStatComponent을 수정한다

우선 스탯변경에 Tick은 필요없으니 제거한다.

InitializeComponent(PostInitializeComponents 이전에 호출)를 구현해 초기화를 시키나, 그러기위해선

bWantsInitializeComponent를 true해주어야한다.

 

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:
	struct FABCharacterData* CurrentStatData = nullptr;
	UPROPERTY(EditInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		int32 Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		float CurrentHP;
};

ABCharacterStatComponent.cpp

// 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()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;
	bWantsInitializeComponent = true;
	// ...
	Level = 1;
}


// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}

void UABCharacterStatComponent::InitializeComponent()
{
	Super::InitializeComponent();
	SetNewLevel(Level);
}

void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

	if (ABGameInstance != nullptr)
	{
		CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
		if (nullptr != CurrentStatData)
		{
			Level = NewLevel;
			CurrentHP = CurrentStatData->MaxHP;
		}
		else
		{
			//요청한 레벨이 파서를 넘어갔을경우
			UE_LOG(LogTemp, Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
		}
	}

1레벨에 체력 100으로 세팅됨을 확인 가능

다음은 데미지를 받으면 현재 체력에서 차감 후, 체력이 0이하일 경우 캐릭터가 죽도록 설정한다

 

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"

DECLARE_MULTICAST_DELEGATE(FOnHPIsZeroDelegate);

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);
	void SetDamage(float NewDamage);
	float GetAttack();

	FOnHPIsZeroDelegate OnHPIsZero;

private:
	struct FABCharacterData* CurrentStatData = nullptr;
	UPROPERTY(EditInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		int32 Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		float CurrentHP;
};

ABCharacterStatComponent.cpp

void UABCharacterStatComponent::SetDamage(float NewDamage)
{
	if (CurrentStatData != nullptr)
	{
		//Clamp는 최소수치와 최대 수치를 정해 그 사이에서만 결과가 나오게 하는 함수다.
		CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0, CurrentStatData->MaxHP);
		if (CurrentHP <= 0)
		{
			OnHPIsZero.Broadcast();
		}
	}
}

float UABCharacterStatComponent::GetAttack()
{
	return CurrentStatData->Attack;
}

ABCharacter.cpp

void AABCharacter::PostInitializeComponents()
{
	...
    //기존에 데미지를 입으면 추가하던 코드를 여기로 이동
	CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
		ABAnim->SetDeadAnim();
		SetActorEnableCollision(false);
	});
}

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	UE_LOG(LogTemp, Warning, TEXT("Actor Name : %s Damage : %f"), *GetName() , FinalDamage);

	CharacterStat->SetDamage(FinalDamage);
	return FinalDamage;
}

void AABCharacter::AttackCheck()
{
	...
	if (bResult)
	{
		if (HitResult.Actor.IsValid())
		{
			//UE_LOG(LogTemp, Warning, TEXT("Hit Actor Name : %s"), *HitResult.Actor->GetName());
			//로그 대신 데미지
			FDamageEvent DamageEvent;
			//찾은 대상에게 데미지 입히기
			HitResult.Actor->TakeDamage(CharacterStat->GetAttack(), DamageEvent, GetController(), this);
		}
	}
}

떄릴때마다 데미지를 입고,체력이 0이되면 죽는걸 확인 할 수 있다.

캐릭터 위젯 UI 제작

HP바가 시각적으로 보이게 만들어 캐릭터에 붙혀보기로 한다

유저 인터페이스 - 위젯 블루프린트.이름은 UI_HPBar
오른쪽 구석 옵션을 Custom으로 변경
사이즈를 150 x 50 으로
다음은 기존 캔버스 패널 제거
프로그레스 바에서 드래그해서 끼워넣는다.회색으로 변경된다.
Vertical Box로 래핑한다
'프리미티브의 Spacer을 위아래에 배치
Spacer 두개 다 클릭후 채우기로 변경후 30.0으로 세팅하면,저렇게 중앙만 하얀색으로 변한다.
프로그레스바는,아래에서 색을 바꾸고, Percent에서 길이를 조절하면 색이 변하는걸 알 수 있다.

ProgressBar_49는 이름을 PB_HPBar로 바꾼다.

 

ABCharacter.h

	//위젯 컴포넌트 추가 
    UPROPERTY(VisibleAnyWhere, Category = UI)
		class UWidgetComponent* HPBarWidget;

ArenaBattle.Build.cs

using UnrealBuildTool;

public class ArenaBattle : ModuleRules
{
	public ArenaBattle(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
 		//끝에 UMG 추가
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

UMG 모듈의 위치는 UE4의 내 프로젝트 -> Source->Runtime->UMG이다.

UMG 모듈을 추가함으로써 내 프로젝트에 UMG모듈을 자유롭게 참조 할 수 있다.

 

ABCharacter.cpp

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


#include "ABCharacter.h"
#include "ABAnimInstance.h"
#include "ABGhostTrail.h"
#include "ABWeapon.h"
#include "ABCharacterStatComponent.h"
#include "DrawDebugHelpers.h"
#include "Components/WidgetComponent.h"

AABCharacter::AABCharacter()
{
	....
    //메쉬 세팅이 다 끝난다음 HPBar을 세팅한다.
	HPBarWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPBARWIDGET"));
    HPBarWidget->SetupAttachment(GetMesh());

	HPBarWidget->SetRelativeLocation(FVector(0, 0, 180));
	HPBarWidget->SetWidgetSpace(EWidgetSpace::Screen);
	static ConstructorHelpers::FClassFinder<UUserWidget> UI_HUD(TEXT("/Game/Book/UI/UI_HPBar.UI_HPBar_C"));
	if (UI_HUD.Succeeded())
	{
		HPBarWidget->SetWidgetClass(UI_HUD.Class);
		HPBarWidget->SetDrawSize(FVector2D(150, 50));
	}
	...
}

머리위에 HP바가 1/3의 게이지로 뜨고있다.

UI와 데이터 연동

 

C++을 새로 생성하면서 UserWidget을 부모로 설정.이름은 ABCharacterWidget으로 한다

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"

DECLARE_MULTICAST_DELEGATE(FOnHPIsZeroDelegate);
DECLARE_MULTICAST_DELEGATE(FOnHPChangedDelegate);

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);
	void SetDamage(float NewDamage);
	void SetHP(float NewHP);
	float GetAttack();
	float GetHPRatio();

	FOnHPIsZeroDelegate OnHPIsZero;
	FOnHPChangedDelegate OnHPChanged;
private:
	struct FABCharacterData* CurrentStatData = nullptr;
	UPROPERTY(EditInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		int32 Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = true))
		float CurrentHP;
};

ABCharacterStatComponent.cpp

void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

	if (ABGameInstance != nullptr)
	{
		CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
		if (nullptr != CurrentStatData)
		{
			Level = NewLevel;
			//최대체력 세팅
			SetHP(CurrentStatData->MaxHP);
			CurrentHP = CurrentStatData->MaxHP;
		}
		else
		{
			//요청한 레벨이 파서를 넘어갔을경우
			UE_LOG(LogTemp, Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
		}
	}
}

void UABCharacterStatComponent::SetDamage(float NewDamage)
{
	if (CurrentStatData != nullptr)
	{
		//Clamp는 최소수치와 최대 수치를 정해 그 사이에서만 결과가 나오게 하는 함수다.
		/*CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0, CurrentStatData->MaxHP);
		if (CurrentHP <= 0)
		{
			OnHPIsZero.Broadcast();
		}*/
		SetHP(FMath::Clamp<float>(CurrentHP - NewDamage, 0, CurrentStatData->MaxHP));
	}
}

void UABCharacterStatComponent::SetHP(float NewHP)
{
	CurrentHP = NewHP;
	//체력 변동 딜리게이트 호출
	OnHPChanged.Broadcast();
	// float수치로 0에 최대한 근접한 오차 이하일경우
	if (CurrentHP < KINDA_SMALL_NUMBER)
	{
		CurrentHP = 0;

		//죽었을떄의 딜리게이트 호출
		OnHPIsZero.Broadcast();
	}
}

//남은 체력의 퍼센티지 반환
float UABCharacterStatComponent::GetHPRatio()
{
	//삼항연산자
	return (CurrentStatData->MaxHP < KINDA_SMALL_NUMBER) ? 0 : (CurrentHP / CurrentStatData->MaxHP);
}

ABCharacterWidget.h

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

#pragma once

#include "ArenaBattle.h"
#include "Blueprint/UserWidget.h"
#include "ABCharacterWidget.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABCharacterWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	void BindCharacterStat(class UABCharacterStatComponent* NewCharacterStat);
protected:
	virtual void NativeConstruct() override;
	void UpdateHPWidget();
private:
	TWeakObjectPtr<class UABCharacterStatComponent> CurrentCharacterStat;

	UPROPERTY()
		class UProgressBar* HPProgressBar;
};

ABCharacterWidget.cpp

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


#include "ABCharacterWidget.h"
#include "ABCharacterStatComponent.h"
#include "Components/ProgressBar.h"

void UABCharacterWidget::BindCharacterStat(UABCharacterStatComponent* NewCharacterStat)
{
	if (nullptr != NewCharacterStat)
	{
		CurrentCharacterStat = NewCharacterStat;
		NewCharacterStat->OnHPChanged.AddUObject(this, &UABCharacterWidget::UpdateHPWidget);
	}
}

void UABCharacterWidget::NativeConstruct()
{
	Super::NativeConstruct();
	HPProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HPBar")));
	UpdateHPWidget();
}

void UABCharacterWidget::UpdateHPWidget()
{
	if (CurrentCharacterStat.IsValid())
	{
		if (nullptr != HPProgressBar)
		{
			HPProgressBar->SetPercent(CurrentCharacterStat->GetHPRatio());
		}
	}
}

ABCharacter.cpp

#include "ABCharacterWidget.h"

void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	if (CurveFloat)
	{
		...
		auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
		if (nullptr != CharacterWidget)
		{
			CharacterWidget->BindCharacterStat(CharacterStat);
		}
	}
}

UI_HPBar 위젯을 킨다음 그래프
UI_HPBar 위젯을 킨다음 그래프 -> 클래스 세팅 -> 부모클래스를 ABCharacterWidget으로 바꾼다.
체력바가 정해둔 스탯에 맞춰 까인다