[ Unreal Engine 5 / #28 TCG Card Game #1 ]

SeungWoo·2024년 10월 28일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
30/31
post-thumbnail

Trading Card Game

Trading Card Game. 카드를 갖고 정해진 규칙에 따라 자신만의 덱을 만들어 상대와 대전하고, 자유롭게 카드 소유자끼리 본인들이 원하는 조건하에 카드를 거래할 수 있는 게임. 흔히 약자인 TCG라 부른다.

  • C++ DataAsset 상속 받은 클래스를 하나 만든다

Data Asset

  • 개념
    • Data Asset은 Unreal Engine의 UDataAsset 클래스를 상속받아 만든 사용자 정의 데이터 클래스입니다. 이를 통해 아이템, 캐릭터 스탯, 레벨 파라미터 등 게임 내 다양한 속성을 외부 파일로 관리하고, 코드에서 해당 데이터를 간편하게 불러올 수 있습니다.
  • Data Asset의 주요 특징
    • 객체 지향 데이터 구조 : Data Asset은 UDataAsset 기반으로 생성되며, 다양한 속성을 정의할 수 있어 필요에 따라 자유롭게 확장 가능합니다.
    • 직관적인 에디터 통합 : Unreal Editor의 ‘Data Asset’ 기능을 통해 쉽게 생성하고 관리할 수 있어 비개발자도 손쉽게 사용할 수 있습니다.
    • 다양한 데이터 타입 지원 : 텍스트, 숫자, 배열, 맵 등 다양한 데이터 타입을 지원하여 복잡한 데이터 구조를 구성할 수 있습니다.
    • 로드 및 캐싱 : Data Asset은 메모리에 로드된 상태에서 사용되므로, 필요한 시점에 즉시 데이터를 불러올 수 있습니다.

Data Table과 Data Asset의 차이점

  • Data Table : CSV나 JSON 파일로부터 데이터를 불러와 테이블 형태로 관리하며, 주로 일관된 구조의 대규모 데이터를 처리하는 데 유리합니다.
  • Data Asset : 개별 UObject를 기반으로 여러 개의 에셋을 생성하여 오브젝트 단위로 관리할 수 있으며, 복잡한 데이터 구조나 상속 구조가 필요할 때 더 유리합니다.
  • Data Asset
    • 장점
      • 코드와 데이터의 분리 : 데이터 중심 설계를 통해 코드와 데이터를 분리함으로써 데이터 변경 시 코드 수정 없이도 다양한 변경을 빠르게 반영할 수 있습니다. 이는 반복 작업이 많은 게임 밸런싱에 특히 유용합니다.
      • 유지보수와 협업의 용이성 : 개발자뿐 아니라 게임 디자이너나 아티스트도 에디터에서 Data Asset을 통해 데이터를 편리하게 수정할 수 있습니다. 이를 통해 비개발자의 협업이 쉬워지며 유지보수와 관리가 편리해집니다.
      • 재사용성 : Data Asset은 클래스 기반이므로 필요 시 상속을 통해 다른 클래스나 기능에서 재사용할 수 있어 효율적입니다.
      • 타입 안정성 : UDataAsset은 Unreal Engine의 클래스 시스템을 통해 타입 안정성을 보장하여 잘못된 데이터 사용을 방지합니다. 이는 특히 큰 규모의 프로젝트에서 데이터 오류를 줄이는 데 도움이 됩니다.
      • 메모리 관리 : Data Asset은 Unreal의 메모리 관리 방식에 의해 효율적으로 로드되고 캐시되어, 런타임 성능에 영향을 덜 미칩니다.
    • 단점
      • 복잡한 데이터 변경 시 비효율적 : 큰 규모의 데이터 변경이 필요한 경우 Data Asset을 하나씩 수정하는 과정이 번거로울 수 있으며, 때로는 외부 데이터 관리 도구(예: CSV, JSON)와 결합하여 사용하는 것이 더 유리할 수 있습니다.
      • 빌드 의존성 : Data Asset 변경 시 프로젝트를 다시 빌드해야 할 수도 있어, 개발 중 불편할 수 있습니다. 특히 상속 구조가 깊어질 경우 프로젝트의 복잡도가 증가합니다.
      • 실시간 업데이트 제한 : Data Asset은 런타임 중 실시간으로 데이터가 변경되기 어려우므로, 네트워크로 데이터가 실시간으로 변경되는 게임에는 적합하지 않습니다.
      • 큰 데이터의 메모리 부담 : 대규모의 Data Asset이 메모리에 로드될 경우 메모리 사용량이 증가할 수 있어, 메모리 관리가 중요한 경우 최적화가 필요합니다.

Data Asset을 사용해 카드 데이터를 만든 이유

  • 유연한 데이터 구조와 상속 :
    • Data Asset은 UObject 기반의 데이터 에셋이므로 상속과 확장이 가능해, 서로 다른 유형의 카드마다 독립적으로 클래스를 상속받아 고유한 속성이나 효과를 추가할 수 있습니다.
    • 예를 들어, 몬스터 카드와 마법 카드가 서로 다른 세부 속성을 가질 경우 각각의 UCardData 클래스를 상속한 UMinionCardData나 USpellCardData 같은 구조로 확장 가능합니다.
  • 편리한 데이터 관리와 블루프린트 통합 :
    • Data Asset은 에디터에서 객체처럼 생성하고 설정할 수 있어 데이터 구조가 직관적이고 관리가 용이합니다. 특히 블루프린트와 잘 통합되어 프로그래밍과 비프로그래밍 작업자 모두가 카드 데이터를 편리하게 수정할 수 있습니다.
    • 이를 통해 블루프린트에서 생성된 Data Asset을 바로 사용하고 수정할 수 있어 워크플로우가 단순해집니다.
  • 복잡한 데이터 및 오브젝트 간 연결 :
    • Data Asset을 사용하면 카드 데이터를 기반으로 각각의 카드 객체를 쉽게 생성하고 참조할 수 있습니다. 예를 들어, 카드 이미지, 효과 사운드, 애니메이션 같은 복잡한 데이터 연결도 가능해집니다.
    • 반면 Data Table은 단일 구조의 데이터를 단순히 테이블 형태로 나열해 관리하는 방식이므로, 이런 객체 지향적인 접근이 필요하지 않을 때 적합합니다.

즉,

  • 데이터 에셋에서는 UFUNCTION을 포함가능
  • 구조체에서는 불가능하다, 하지만 구조체에서도 일반 함수는 포함할 수 있다.
    → UFUNCTION 사용하여야 한다 -> Data Asset
    CSV나 JSON 파일로부터 데이터를 불러와 테이블 형태로 관리되어야 한다 -> Data Table

Card.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Card.generated.h"

class UCardData;

UCLASS()
class TCG_API ACard : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	ACard();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Card")
	UCardData* CardData;

	void InitalizeCard(UCardData* _cardData);

	void PrintCaedInfo();

	void LoadAndInitializeCard(const FString& AssetPath);
};

Card.cpp

#include "Card.h"
#include "CardData.h"
#include "MinionCardData.h"
#include "WeaponCardData.h"
#include "SpellCardData.h"
#include "SecretCardData.h"
#include "HeroPowerCardData.h"

// Sets default values
ACard::ACard()
{
	PrimaryActorTick.bCanEverTick = false;

	FString AssetPath = "/Script/TCG.MinionCardData'/Game/CardData/DA_MinionCard1.DA_MinionCard1'";
	LoadAndInitializeCard(AssetPath);
}

void ACard::InitalizeCard(UCardData* _cardData)
{
	CardData = _cardData;
	// 필요한 경우 초기화 시, CardData에서 속성을 복사
}

void ACard::PrintCaedInfo()
{
	if (!CardData)
	{
		UE_LOG(LogTemp, Warning, TEXT(" Card Data is not initialized "));
	}

	if (const UMinionCardData* MinionCard = Cast<UMinionCardData>(CardData))
	{
		UE_LOG(LogTemp, Log, TEXT("Minion Card - Name : %s , Mana : %d, Attack : %d, Health : %d "), *MinionCard->CardName, MinionCard->ManaCost, MinionCard->AttackPower, MinionCard->Health);
	}
	else if (const UWeaponCardData* WeaponCard = Cast<UWeaponCardData>(CardData))
	{
		UE_LOG(LogTemp, Log, TEXT("Weapon Card - Name : %s , Mana : %d, Attack : %d, Durability : %d "), *WeaponCard->CardName, WeaponCard->ManaCost, WeaponCard->AttackPower, WeaponCard->Durability);
	}
	else if (const USpellCardData* SpellCard = Cast<USpellCardData>(CardData))
	{
		UE_LOG(LogTemp, Log, TEXT("Minion Card - Name : %s , Mana : %d, Effect : %s "), *SpellCard->CardName, SpellCard->ManaCost, *SpellCard->EffectDescription);
	}
	else if (const USecretCardData* SecretCard = Cast<USecretCardData>(CardData))
	{
		UE_LOG(LogTemp, Log, TEXT("Minion Card - Name : %s , Mana : %d, Triiger : %s, Effect : %s "), *SecretCard->CardName, SecretCard->ManaCost, *SecretCard->TriggerCondition, *SecretCard->EffectDescription);
	}
	else if (const UHeroPowerCardData* HeroPowerCardData = Cast<UHeroPowerCardData>(CardData))
	{
		UE_LOG(LogTemp, Log, TEXT("Minion Card - Name : %s , Mana : %d, Effect : %s"), *HeroPowerCardData->CardName, HeroPowerCardData->ManaCost, *HeroPowerCardData->EffectDescription);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Unknow Card Type"));
	}

}

void ACard::LoadAndInitializeCard(const FString& AssetPath)
{
	// FString을 FSoftObjectPath로 변환 
	FSoftObjectPath SoftObjectPath(AssetPath);
	TSoftObjectPtr<UCardData> CardDataAsset(SoftObjectPath);

	// 동기적으로 로드합니다 ( 비동기 로드 필요 시 LoadAsync 사용 가능 )
	if (CardDataAsset.IsValid() || CardDataAsset.LoadSynchronous())
	{
		UCardData* LoadedCardData = CardDataAsset.Get();
		InitalizeCard(LoadedCardData);	// 카드 데이터를 초기화
		UE_LOG(LogTemp, Log, TEXT("Card initialized with data asset : %s"), *LoadedCardData->CardName);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Failed to load card data from asset path : %s"), *AssetPath);
	}
}

Deck.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Deck.generated.h"

class ACard;

UCLASS()
class TCG_API ADeck : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ADeck();

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Deck")
	TArray<ACard*> Cards;

	// 덱을 셔플하는 함수입니다
	void ShuffleDeck();

	// 덱에서 카드를 드로우하여 손패에 추가하는 함수 입니다 
	ACard* DrawCard(class AHand* PlayerHand);

	// 이름을 통해 특정 카드를 찾는 함수입니다
	ACard* FindCardByName(const FString& Name) const;

	// 덱의 현재 상태를 출력하는 디버깅용 함수 입니다 
	void PrintDectInfo() const;
};

Deck.cpp

#include "Deck.h"
#include "Hand.h"
#include "Card.h"
#include "CardData.h"

// Sets default values
ADeck::ADeck()
{
	PrimaryActorTick.bCanEverTick = false;

}

void ADeck::ShuffleDeck()
{
	const int32 NumCards = Cards.Num();
	for (int32 i = NumCards - 1; i > 0; --i)
	{
		int32 j = FMath::RandRange(0, i);
		if (Cards.IsValidIndex(i) && Cards.IsValidIndex(j))
		{
			Cards.Swap(i, j);
		}
	}
}

ACard* ADeck::DrawCard(AHand* PlayerHand)
{
	if (Cards.Num() > 0)
	{
		ACard* DrawCard = Cards[0];
		Cards.RemoveAt(0);
		if (PlayerHand && PlayerHand->AddCardToHand(DrawCard))
		{
			return DrawCard;
		}
		else
		{
			// 드로우 실패 시 처리를 추가 가능
		}
	}
	return nullptr;
}

ACard* ADeck::FindCardByName(const FString& Name) const
{
	for (ACard* Card : Cards)
	{
		if (Card && Card->CardData && Card->CardData->CardName == Name)
		{
			return Card;
		}
	}
	return nullptr;
}

void ADeck::PrintDectInfo() const
{
	UE_LOG(LogTemp, Log, TEXT("Deck Contain %d cards : "), Cards.Num());
	for (const ACard* Card : Cards) 
	{
		if (Card && Card->CardData)
		{
			UE_LOG(LogTemp, Log, TEXT(" - %s ( Mana Cost : %d"), *Card->CardData->CardName, Card->CardData->ManaCost);
		}
	}
}

Hand.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Hand.generated.h"

class ACard; 
UCLASS()
class TCG_API AHand : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AHand();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hand")
	TArray<ACard*> CardsInHand;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hand")
	int32 MaxHandSize;

	// 손패에 카드를 추가하는 함수 
	bool AddCardToHand(ACard* NewCard);

	// 손패에서 특정 카드를 제거하는 함수 
	bool RemoveCardFromHand(ACard* CardToRemove);

	// 손패가 가득 찼는지 확인하는 함수 
	bool IsHandFull() const;

	// 손패에 카드를 시각적으로 정리하는 함수
	void ArrangeCardsInHand();

	// 손패에 현재 상태를 출력하는 디버깅용 함수
	void PrintHandInfo() const;

};

Hand.cpp

#include "Hand.h"
#include "Card.h"
#include "CardData.h"

// Sets default values
AHand::AHand()
{
	// 손패의 시작적 업데이트를 위해 Tick = true;
	PrimaryActorTick.bCanEverTick = true;
	MaxHandSize = 7;
}

bool AHand::AddCardToHand(ACard* NewCard)
{
	if (NewCard && !IsHandFull())
	{
		CardsInHand.Add(NewCard);
		ArrangeCardsInHand(); // 손패의 카드 배치 갱신
		return true;
	}
	return false;
}

bool AHand::RemoveCardFromHand(ACard* CardToRemove)
{
	if (CardToRemove && CardsInHand.Contains(CardToRemove)) 
	{
		CardsInHand.Remove(CardToRemove);
		ArrangeCardsInHand(); // 손패의 카드 배치 갱신
		return true;
	}
	return false;
}

bool AHand::IsHandFull() const
{
	return CardsInHand.Num() >= MaxHandSize;
}

void AHand::ArrangeCardsInHand()
{
	// 손패의 카드를 화면에 보기 좋게 배치하는 로직 구현
	const float CardSpacing = 100.0f; // 카드 간의 거리
	const FVector StartPosition = FVector(-CardSpacing * CardsInHand.Num() / 2, 0, 0);

	for (int32 i = 0; i < CardsInHand.Num(); ++i)
	{
		if (CardsInHand[i])
		{
			FVector NewPosition = StartPosition + FVector(i * CardSpacing, 0, 0);
			CardsInHand[i]->SetActorLocation(NewPosition);
		}
	}
}

void AHand::PrintHandInfo() const
{
	UE_LOG(LogTemp, Log, TEXT("Hand contaings %d cards : "), CardsInHand.Num());

	for (const ACard* Card : CardsInHand)
	{
		if (Card && Card->CardData)
		{
			UE_LOG(LogTemp, Log, TEXT(" - %s ( Mana Cost : %d "), *Card->CardData->CardName, Card->CardData->ManaCost);
		}
	}
}

CardData.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CardData.generated.h"

UCLASS()
class TCG_API UCardData : public UDataAsset
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Card Data")
	FString CardName;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Card Data")
	int32 ManaCost;
};

MinionCardData.h

#pragma once

#include "CoreMinimal.h"
#include "CardData.h"
#include "MinionCardData.generated.h"

UCLASS()
class TCG_API UMinionCardData : public UCardData
{
	GENERATED_BODY()
	
public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Minion")
	int32 AttackPower; // 공격력 

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Minion")
	int32 Health;		// 체력
};

WeaponCardData.h

#pragma once

#include "CoreMinimal.h"
#include "CardData.h"
#include "WeaponCardData.generated.h"

UCLASS()
class TCG_API UWeaponCardData : public UCardData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon")
	int32 AttackPower;		// 공격력 

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon")
	int32 Durability;		// 내구도 
};

SpellCardData.h

#pragma once

#include "CoreMinimal.h"
#include "CardData.h"
#include "SpellCardData.generated.h"

UCLASS()
class TCG_API USpellCardData : public UCardData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spell")
	FString EffectDescription; // 마법 효과 설명
};

SecretCardData.h

#pragma once

#include "CoreMinimal.h"
#include "CardData.h"
#include "SecretCardData.generated.h"

UCLASS()
class TCG_API USecretCardData : public UCardData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Secret")
	FString TriggerCondition; // 비밀 카드의 발도 조건 설명

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Secret")
	FString EffectDescription; // 비밀 카드의 효과 설명 
};

HeroPowerCardData.h

#pragma once

#include "CoreMinimal.h"
#include "CardData.h"
#include "HeroPowerCardData.generated.h"

UCLASS()
class TCG_API UHeroPowerCardData : public UCardData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "HeroPower")
	FString EffectDescription; // 영웅 능력의 효과 설명
};

FSoftObjectPath

  • 정의 : FSoftObjectPath는 Unreal Engine의 경로 표현 클래스입니다. 에셋의 경로를 문자열로 저장하여, 직접 로드하지 않고도 경로만을 유지할 수 있습니다.
  • 형식 : 경로 문자열을 "패키지경로.오브젝트명" 형식으로 저장합니다. 예를 들어, "/Game/DataAssets/CardData/MyCardDataAsset.MyCardDataAsset"처럼 패키지 경로와 객체 이름이 모두 포함된 형식입니다.
  • 사용 목적 : FSoftObjectPath는 경로를 통해 에셋을 참조하지만, 실제로 메모리에 로드하지는 않으므로 메모리 절약에 도움이 됩니다. 경로를 메모리에 저장해 두고 필요할 때 로드하거나, 다른 클래스와 연동할 수 있습니다.

TSoftObjectPtr

  • 정의 : TSoftObjectPtr은 TWeakObjectPtr과 유사하지만, Unreal Engine에서 지연 로딩에 특화된 스마트 포인터입니다. TSoftObjectPtr는 객체가 아직 메모리에 로드되지 않아도 참조할 수 있으며, 필요 시 경로를 통해 객체를 메모리에 로드합니다.
  • 특징 : TSoftObjectPtr는 포인터처럼 사용할 수 있으며, IsValid나 LoadSynchronous 메서드를 통해 포인터의 유효성을 확인하거나 동기적으로 로드할 수 있습니다. 특히, LoadAsync를 통해 비동기 로드도 지원합니다.
  • 사용 목적 : TSoftObjectPtr는 참조하는 객체를 필요할 때 로드하여 지연 로딩(Lazy Loading)을 수행합니다. 이는 큰 데이터를 가진 에셋을 즉시 메모리에 로드하지 않고, 필요할 때 로드하는 방식으로 성능을 최적화할 수 있습니다.

profile
This is my study archive

0개의 댓글