DX나 유니티의 경우 싱글톤 패턴이나 전역 클래스를 이용해서 어디서든 편하게 접근할 수 있는 데이터 매니저를 만들어서 관리하면 굉장히 편했으나, 언리얼의 경우 이미 구조가 어느정도 잡혀있기 때문에 데이터 매니저를 만든다고 하면 어디에 둘지 굉장히 고민이 된다.

물론, 언리얼은 이미 구비가 되어있다. C++ 클래스의 GameInstance를 상속받아 만들면 된다.

 

보통 기획 직군이 밸런스를 담당해서 붙여줄텐데 데이터(수치)들을 하드코딩해서 관리하면 편하게 작업할 수 없고 수정사항이 발생할 때마다 프로그래머가 고쳐주어야 하는 불편함이 있다.

뿐만 아니라 하드코딩을 해버리면 실행파일 자체에 묶여서 들어가기때문에 수치만 조정하고 싶어도 다시 빌드해서 배포하는 수밖에 없다.

그러므로 데이터와 코드는 분리하는게 여러모로 편하다

 

 

// MyGameInstance.h

#include "Engine/DataTable.h"

USTRUCT()
struct  FMyCharacterData : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Level;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 MaxHp;
};

 

 

행 구조 선택에서 C++ 코드로 작성해둔 구조체를 선택해주면 된다.

 

 

 

이제 이 데이터 테이블은 바이너리에 묶여서 나가는 방식이 아닌 외부 파일로 빠지게 된다.

행을 추가해서 각각의 데이터를 정해놓을 수 있다. 물론 이 방법뿐만 json이나 xml등을 파싱해서 사용하는것도 하나의 방법이다.

 

 

// MyGameInstance.h
class TESTUNREALENGINE_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	UMyGameInstance();

	virtual void Init() override;

	FMyCharacterData* GetStatData(int32 Level);

private:
	UPROPERTY()
	class UDataTable* MyStats;
	
};

// MyGameInstance.cpp
UMyGameInstance::UMyGameInstance()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> DATA(TEXT("DataTable'/Game/Data/StatTable.StatTable'"));
	
	MyStats = DATA.Object;
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Warning, TEXT("MyGameInstance %d"), GetStatData(1)->Attack);
}

FMyCharacterData* UMyGameInstance::GetStatData(int32 Level)
{
	return MyStats->FindRow<FMyCharacterData>(*FString::FromInt(Level), TEXT(""));
}

 

 

만들어둔 게임 인스턴스 클래스를 사용하려면 [세팅 - 프로젝트 세팅 - 맵&모드] 에서 게임 인스턴스 클래스를 바꿔주어야 한다.

 

 

A캐릭터와 B캐릭터간의 전투 시스템이 구현되고 A가 B를 공격했을 때, A클래스에서 B클래스에 접근해서 체력을 깎아야 하는지, 혹은 중간의 매니저쪽에서 처리해야 하는지가 고민이 된다.

대부분의 경우에서는 데미지를 받는 쪽에서 피격 함수를 만들어서 처리하는게 여러 상황들이 발생했을 때를 고려하면 좀 더 깔끔하다.

 

데이터 테이블로 데이터를 외부로 빼서 관리한다고 해도 캐릭터의 현재 체력같은 것들은 결국 클래스에서 관리를 해야하는 부분이다. 그렇다고 해당 데이터들을 계속 추가하는 것은 많아지면 관리하기 어려우므로 스탯 컴포넌트를 따로 만들어서 스탯끼리 관리하고 캐릭터에 붙여놓는게 조금 더 편리한 방법일 수 있다.

 

// MyStatComponent.h
class TESTUNREALENGINE_API UMyStatComponent : public UActorComponent
{
	GENERATED_BODY()

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

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

public:
	void SetLevel(int32 Level);
	void OnAttacked(float DamageAmount);

	int32 GetLevel() { return Level; }
	int32 GetHp() { return Hp; }
	int32 GetAttack() { return Attack; }

private:
	UPROPERTY(EditAnywhere, Category=Stat, Meta=(AllowPrivateAccess=true))
	int32 Level;

	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Hp;
	
	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Attack;
};

// MyStatComponent.cpp
#include "MygameInstance.h"
#include "Kismet/GameplayStatics.h"

// Sets default values for this component's properties
UMyStatComponent::UMyStatComponent()
{
	// 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; // InitializeComponent 함수 호출을 위해 true
	Level = 1;

	// ...
}


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

	// ...
	
}

void UMyStatComponent::InitializeComponent()
{
	Super::InitializeComponent();

	SetLevel(Level);
}

void UMyStatComponent::SetLevel(int32 NewLevel)
{
	// GameInstance를 받아와서 안의 값을 가져옴
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		auto StatData = MyGameInstance->GetStatData(NewLevel);
		if (StatData)
		{
			Level = StatData->Level;
			Hp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}

}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	Hp -= DamageAmount;
	if (Hp < 0)
		Hp = 0;

	UE_LOG(LogTemp, Warning, TEXT("OnAttacked %d"), Hp);
}

// MyCharacter.h
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
	
    UPROPERTY(VisibleAnywhere)
    class UMyStatComponent* Stat;
    
// MyCharacter.cpp
#include "MyStatComponent.h"

AMyCharacter::AMyCharacter()
{
	// .. 생략
	Stat = CreateDefaultSubobject<UMyStatComponent>(TEXT("STAT"));
}

void AMyCharacter::AttackCheck()
{
	// .. 생략
    if (bResult && HitResult.Actor.IsValid())
    {
        FDamageEvent DamageEvent;
        UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.Actor->GetName());
        // 데미지를 받는쪽의 TakeDamage를 호출한다
        HitResult.Actor->TakeDamage(Stat->GetAttack(), DamageEvent, GetController(), this);
    }
}

float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Stat->OnAttacked(DamageAmount);

	return DamageAmount;
}

 

 

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 아이템 줍기  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02
[UE4 입문] 충돌 기초  (0) 2022.09.02

+ Recent posts