UE4 - 게임데이터와 UI 위젯
이득우의 C++ 11장
엑셀데이터의 활용
캐릭터의 스탯은 보통 변하지 않는 데이터이므로 앱이 초기화 할때 불러들인다.
게임 인스턴스 - 앱 관리 용도로 언리얼에서 제공하는 기능
게임 인스턴스를 이용해 스탯 데이터를 불러들이면, 게임 앱이 종료 될 때 까지 스탯 데이터가 보존된다.
ABGameInstance의 초기화는 앱 시작과 동시에 이루어진다.
*언리얼 엔진의 실행 순서
게임 앱의 초기화 -> 레벨에 속한 액터 초기화 -> 플레이어의 로그인 및 게임시작
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()
{
}
드래그 옵션을 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 수치를 표기해주는 코드이다. 테이블에서 제대로 받아지는지 확인한다.
액터 컴포넌트의 제작
스탯에 대한 데이터를 관리하는 액터 컴포넌트를 캐릭터에 부착해보는 식으로 작업한다.
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"));
...
이제 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);
}
}
다음은 데미지를 받으면 현재 체력에서 차감 후, 체력이 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);
}
}
}
캐릭터 위젯 UI 제작
HP바가 시각적으로 보이게 만들어 캐릭터에 붙혀보기로 한다
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));
}
...
}
UI와 데이터 연동
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);
}
}
}