Trading Card Game. 카드를 갖고 정해진 규칙에 따라 자신만의 덱을 만들어 상대와 대전하고, 자유롭게 카드 소유자끼리 본인들이 원하는 조건하에 카드를 거래할 수 있는 게임. 흔히 약자인 TCG라 부른다.
객체 지향 데이터 구조
: Data Asset은 UDataAsset 기반으로 생성되며, 다양한 속성을 정의할 수 있어 필요에 따라 자유롭게 확장 가능합니다.직관적인 에디터 통합
: Unreal Editor의 ‘Data Asset’ 기능을 통해 쉽게 생성하고 관리할 수 있어 비개발자도 손쉽게 사용할 수 있습니다.다양한 데이터 타입 지원
: 텍스트, 숫자, 배열, 맵 등 다양한 데이터 타입을 지원하여 복잡한 데이터 구조를 구성할 수 있습니다.로드 및 캐싱
: Data Asset은 메모리에 로드된 상태에서 사용되므로, 필요한 시점에 즉시 데이터를 불러올 수 있습니다.Data Table과 Data Asset의 차이점
Data Table
: CSV나 JSON 파일로부터 데이터를 불러와 테이블 형태로 관리하며, 주로 일관된 구조의 대규모 데이터를 처리하는 데 유리합니다.Data Asset
: 개별 UObject를 기반으로 여러 개의 에셋을 생성하여 오브젝트 단위로 관리할 수 있으며, 복잡한 데이터 구조나 상속 구조가 필요할 때 더 유리합니다.
코드와 데이터의 분리
: 데이터 중심 설계를 통해 코드와 데이터를 분리함으로써 데이터 변경 시 코드 수정 없이도 다양한 변경을 빠르게 반영할 수 있습니다. 이는 반복 작업이 많은 게임 밸런싱에 특히 유용합니다.유지보수와 협업의 용이성
: 개발자뿐 아니라 게임 디자이너나 아티스트도 에디터에서 Data Asset을 통해 데이터를 편리하게 수정할 수 있습니다. 이를 통해 비개발자의 협업이 쉬워지며 유지보수와 관리가 편리해집니다.재사용성
: Data Asset은 클래스 기반이므로 필요 시 상속을 통해 다른 클래스나 기능에서 재사용할 수 있어 효율적입니다.타입 안정성
: UDataAsset은 Unreal Engine의 클래스 시스템을 통해 타입 안정성을 보장하여 잘못된 데이터 사용을 방지합니다. 이는 특히 큰 규모의 프로젝트에서 데이터 오류를 줄이는 데 도움이 됩니다.메모리 관리
: Data Asset은 Unreal의 메모리 관리 방식에 의해 효율적으로 로드되고 캐시되어, 런타임 성능에 영향을 덜 미칩니다.복잡한 데이터 변경 시 비효율적
: 큰 규모의 데이터 변경이 필요한 경우 Data Asset을 하나씩 수정하는 과정이 번거로울 수 있으며, 때로는 외부 데이터 관리 도구(예: CSV, JSON)와 결합하여 사용하는 것이 더 유리할 수 있습니다.빌드 의존성
: Data Asset 변경 시 프로젝트를 다시 빌드해야 할 수도 있어, 개발 중 불편할 수 있습니다. 특히 상속 구조가 깊어질 경우 프로젝트의 복잡도가 증가합니다.실시간 업데이트 제한
: Data Asset은 런타임 중 실시간으로 데이터가 변경되기 어려우므로, 네트워크로 데이터가 실시간으로 변경되는 게임에는 적합하지 않습니다.큰 데이터의 메모리 부담
: 대규모의 Data Asset이 메모리에 로드될 경우 메모리 사용량이 증가할 수 있어, 메모리 관리가 중요한 경우 최적화가 필요합니다.유연한 데이터 구조와 상속
:편리한 데이터 관리와 블루프린트 통합
:복잡한 데이터 및 오브젝트 간 연결
:즉,
- 데이터 에셋에서는 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는 Unreal Engine의 경로 표현 클래스입니다. 에셋의 경로를 문자열로 저장하여, 직접 로드하지 않고도 경로만을 유지할 수 있습니다.형식
: 경로 문자열을 "패키지경로.오브젝트명" 형식으로 저장합니다. 예를 들어, "/Game/DataAssets/CardData/MyCardDataAsset.MyCardDataAsset"처럼 패키지 경로와 객체 이름이 모두 포함된 형식입니다.사용 목적
: FSoftObjectPath는 경로를 통해 에셋을 참조하지만, 실제로 메모리에 로드하지는 않으므로 메모리 절약에 도움이 됩니다. 경로를 메모리에 저장해 두고 필요할 때 로드하거나, 다른 클래스와 연동할 수 있습니다.정의
: TSoftObjectPtr은 TWeakObjectPtr과 유사하지만, Unreal Engine에서 지연 로딩에 특화된 스마트 포인터입니다. TSoftObjectPtr는 객체가 아직 메모리에 로드되지 않아도 참조할 수 있으며, 필요 시 경로를 통해 객체를 메모리에 로드합니다.특징
: TSoftObjectPtr는 포인터처럼 사용할 수 있으며, IsValid나 LoadSynchronous 메서드를 통해 포인터의 유효성을 확인하거나 동기적으로 로드할 수 있습니다. 특히, LoadAsync를 통해 비동기 로드도 지원합니다.사용 목적
: TSoftObjectPtr는 참조하는 객체를 필요할 때 로드하여 지연 로딩(Lazy Loading)을 수행합니다. 이는 큰 데이터를 가진 에셋을 즉시 메모리에 로드하지 않고, 필요할 때 로드하는 방식으로 성능을 최적화할 수 있습니다.