엑셀 데이터로부터 캐릭터의 능력치를 관리할 수 있도록
테이블 데이터를 언리얼 엔진에 불러들이는 방법을 알아본다.
게임 데이터를 효과적으로 관리하는 게임 인스턴스 클래스,
방대한 로직을 분산시킬 수 있도록 액터 컴포넌트를 설계해 액터에 적용
캐릭터의 현재 스탯을 HP바로 표시하는 기능 구현
<1>. 엑셀 데이터의 활용
엑셀에 저장돼 있는 캐릭터의 스탯 데이터 테이블을 언리얼 엔진에 불러들여보자.
스탯 데이터는 변하지 않는 데이터이므로, 보통 게임 앱이 초기화될 때 불러들인다.
→ 게임 앱을 관리하기 위해 '게임 인스턴스'라는 언리얼 오브젝트가 제공된다.
→ "ABGameInstance" 게임 인스턴스 클래스를 생성하고, 맵&모드 탭에 있는 GameInstance를 바꿔준다.
→ GameInstance의 Init 함수를 호출해 본다.
//ABGameInstance.h
#include "ArenaBattle.h"
class ARENABATTLE_API UABGameInstance : public UGameInstance
{
public:
UABGameInstance();
virtual void Init() override;
}
//ABGameInstance.cpp
void UABGameInstance::Init()
{
Super::Init();
ABLOG_S(Warning);
}
※게임을 시작하는 과정
게임 앱의 초기화 → 레벨에 속한 액터의 초기화 → 플레이어의 로그인 → 게임의 시작
UGameInstance::Init → AActor::PostInitializeComponents → AGameMode::PostLogin → AGameMode::StartPlay
AActor::BeginPlay
게임 데이터를 관리할 게임 인스턴스를 설정했으니, 게임 데이터를 프로젝트에 임포트 하고 이를 불러들여보자.
→ 엑셀 파일 형식은 안되고, CSV 파일 형식으로 변환한다.
이 데이터를 불러들이려면, 테이블 데이터의 각 열의 이름과 유형이 동일한 구조체를 선언해야 한다.
※ 구조체를 생성할 때 규칙 : 1. USTRUCT 매크로를 구조체 선언 윗줄에 넣어준다.
2. 구조체 내부에 GENERATED_BODY() 매크로를 선언해 준다.
3. Name 열 데이터는 자동으로 키 값으로 사용되기 때문에 선언에서 제외한다.
//ABGameInstance.h
#include "Engine/DataTable.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")
float MaxHP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Data")
float Attack;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Data")
int32 DropExp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Data")
int32 NextExp;
};
이렇게 FABCharacterData가 컴파일 됐으면, CSV데이터를 임포트 한다.
→ 데이터 테이블 옵션에서 'ABCharacterData' 선택
게임 데이터 에셋을 성공적으로 임포트 했다면, 게임 인스턴스에서 이를 로딩하는 기능을 구현해 보자.
언리얼 엔진은 테이블 데이터를 관리하도록 'DataTable'이라는 언리얼 오브젝트를 제공한다.
→ DataTable을 게임 인스턴스의 멤버 변수로 선언하고 레퍼런스를 복사한 후, 데이터를 불러들이는 기능을 구현한다.
※ 잘 로딩됐는지 파악하기 위해 20 레벨 데이터를 출력해 보자
//ABGameInstance.h
class ARENABATTLE_API UABGameInstance:public UGameInstance
{
public:
FABCharacterData* GetABCharacterData(int32 Level);
private:
UPROPERTY()
class UDataTable* ABCharacterTable;
};
//ABGameInstance.cpp
UABGameInstance::UABGameInstane()
{
FString CharacterDataPath = TEXT("(경로)");
static ConstructorHelpers::FObjectFinder<UDataTable> DT_ABCHARACTER(*CharacterDataPath);
ABCHECK(DT_ABCHARACTER.Succeeded());
ABCharacterTable = DT_ABCHARACTER.Object;
}
void UABGameInstance::Init()
{
ABLOG(Warning, TEXT("%d"), GetABCharacterDate(20)->DropExp); //레벨20의 Exp수치 출력
}
FABCharacterData* UABGameInstance::GetABCharacterData(int32 Level)
{
return ABCharacterTable->FindRow<FABCharacterData>(*FString::FromInt(Level),TEXT(""));
//정수를 문자열로 변환한 후 데이터 테이블에서 해당 행을 찾음
//FromInt는 FString 클래스의 함수로, 정수를 FString으로 변환하는 기능을 함
}
<2>. 액터 컴포넌트의 제작
이번엔 액터 컴포넌트 클래스를 생성하고 이를 캐릭터에 부착해 캐릭터 스탯에 대한 관리를 액터 컴포넌트가 일임하도록 기능을 구현해 본다.
→ 'ActorComponent' 클래스를 만든다 ("ABCharacterStatComponent") → ABCharacter 클래스에 멤버 변수로 선언한다.
//ABCharacter.h
UPROPERTY(VisibleAnywhere, Category=Stat)
class UABCharacterStatComponent* CharacterStat;
//ABCharacter.cpp
#include "ABCharacterStatComponent.h"
AABCharacter::AABCharacter()
{
CharacterStat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("CHARACTERSET"));
}
액터 컴포넌트 클래스에는 자동으로 제공되는 템플릿 코드에 BeginPlay, TickComponent 함수가 있다.
→ 스탯에 변경이 일어날 때만 데이터를 처리하므로, Tick 로직은 필요 없다 → TickComponent 삭제
→ 액터의 PostInitializeComponents = 액터 컴포넌트의 InitializeComponent
→이 함수를 호출하려면 생성자에서 bWantsInitializeComponent = true 해줘야 함.
//ABCharacterStatComponent.h
#include "ArenaBattle.h"
protected:
virtual void InitializeComponent() override;
//ABCharacterStatComponent.cpp
UABCharacterStatComponent::UABCharacterStatComponent()
{
PrimaryComponentTick.bCanEverTick = false;
bWantsInitializeComponent = true;
}
void UABCharacterStatComponent::InitializeComponent()
{
Super::InitializeComponent();
}
- 레벨, HP 등 모든 스탯을 스탯 컴포넌트에서 관리하도록 한다.
- 게임 인스턴스에서 데이터를 초기화하고 레벨이 변경되면 해당 스탯이 바뀌도록 한다.
- 캐릭터가 대미지를 받은 만큼 CurrentHP에서 차감하고 0 이하가 되면 캐릭터가 죽는 애니메이션 재생.
- 캐릭터의 TakeDamge 함수에서 처리하지 말고, ABCharacterStatComponent에 SetDamage 함수를 설정하고 TakeDamage함수를 호출해 액터 컴포넌트가 처리하도록 구성을 변경한다.
<3>. 캐릭터 위젯 UI 제작
캐릭터의 HP값이 시각적으로 보이도록 UI위젯을 제작, 캐릭터에 부착해 보자.
→ '위젯 블루프린트'에서 "UI_HPBar"라는 UI에셋을 생성한다.
1. Canvas Panel을 제거한다. 이 패널은 위젯을 전체 스크린 기준으로 부착할 때 좋다.
2. Progress Bar를 계층 구조에 드래그하고 "PB_HPBar"라고 해준다.
3. Vertical Box로 감싸준다.
4. 윈도 프리미티브 그룹에서 Spacer 컨트롤을 Vertical Box로 드래그해, HPBar 위아래에 추가해 준다.
5. 3개의 영역을 40%, 20%, 40%로 배분해 준다.
<4>. 모듈과 빌드 설정
UI를 캐릭터에 부착하는 기능을 구현해 보자.
액터에 UI 위젯을 부착할 수 있도록 'UWidgetComponent' 클래스가 제공된다.
→위젯 컴포넌트 기능은 'ArenaBattle.Build.cs 파일의 기본 모듈에는 없기 때문에, UMG 모듈을 추가해줘야 한다.
위젯 컴포넌트가 캐릭터 머리 위로 오도록 하고, 항상 플레이어를 향해 보도록 Screen 모드로 지정한다.
//ABCharacter.cpp
#include "Components/WidgetComponent.h"
AABCharacter::AABCharacter()
{
HPBarWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPBARWIDGET"));
HPBarWidget->SetAttachment(GetMesh());
HPBarWidget->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f)); // 머리 위
HPBarWidget->SetWidgetSpace(EWidgetSpace::Screen); //플레이어를 향해 보도록
static ConstructorHelpers::FClassFinder<UUserWidget>UI_HUD(TEXT("(경로)_C"));
if(UI_HUD.Succeeded())
{
HPBarWidget->SetWidgetClass(UI_HUD.Class);
HPBarWidget->SetDrawSize(FVector2D(150.0f, 50.0f));
}
}
<5>. UI와 데이터의 연동
캐릭터의 스탯이 변경되면 이를 UI에 전달해 ProgressBar가 변경되도록 구현한다.
애니메이션 설계 작업을 애님 그래프에서 한 것처럼, UI 작업은 디자이너라는 공간에서 진행한다.
하지만, UI의 로직은 애님 인스턴스와 유사하게 C++ 클래스에서 만들 수 있다.
※ 위젯 블루프린트가 사용하는 C++ 클래스는 UserWidget이다. → 생성된 "ABCharacterWidget" 클래스는 "ABCharacterStatComponent"와 연동하여 HP 바를 업데이트할 것이다. → 의존성을 가지지 않게 델리게이트를 선언하자.
UI 생성은 플레이어 컨트롤러의 BeginPlay에서 호출됨.
→ 앞에서 위젯 초기화 시점을 PostInitializeComponents로 함.
→ 이 초기화 명령은 적용되지 않음 → UI 시스템이 준비되면 호출되는 NativeConstruct 함수에서 위젯 업데이트 로직을 구현하자.
//ABCharacterWidget.h
UCLASS()
class ARENABATTLE_API UABCharacterWidget : public UUserWidget
{
protected:
virtual void NativeConstruct() override;
void UpdateHPWidget();
private:
UPROPERTY()
class UProgressBar* HPProgressBar;
//ABCharacterWidget.cpp
#include "Components/ProgressBar.h" //UMG 모듈의 Public/Components 폴더에 있음
void UABCharacterWidget::BindCharacterStat(UABCharacterStatComponent* 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());
}
}
}
'게임 개발 > 이득우의 언리얼 C++' 카테고리의 다른 글
[이득우의 언리얼 C++ 게임개발의 정석] #13. 프로젝트의 설정과 무한 맵의 제작 (0) | 2024.07.02 |
---|---|
[이득우의 언리얼 C++ 게임개발의 정석] #12. AI 컨트롤러와 비헤이비어 트리 (0) | 2024.06.28 |
[이득우의 언리얼 C++ 게임개발의 정석] #10. 아이템 상자와 무기 제작 (0) | 2024.06.25 |
[이득우의 언리얼 C++ 게임개발의 정석] #9. 충돌 설정과 대미지 전달 (0) | 2024.06.22 |
[이득우의 언리얼 C++ 게임개발의 정석] #8. 애니메이션 시스템 활용 (0) | 2024.06.20 |