[ Unreal Engine 5 / #29 TCG Card Game #2 ]

SeungWoo·2024년 10월 30일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
31/31
post-thumbnail

다양한 효과들을 상속구조를 통해 해결하려면??

USpellCardData.h

#pragma once

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

// 효과 유형을 정의 하는 열거형 입니다.
UENUM(BlueprintType)
enum class ESpellEffectType : uint8
{
	Damage,
	Heal,
	Buff,
	Debuff
};

// 타겟 유형을 정의하는 열거형 입니다.
UENUM(BlueprintType)
enum class ESpellTargetType : uint8
{
	SingleEnemy,
	SingleAlly,
	AllEnemise,
	AllAllies,
	Self
};


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

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spell")
	ESpellEffectType EffectType;  // 효과 유형 ( 피해, 회복 등 )

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spell")
	ESpellTargetType TargetType; // 타겟 유형 ( 단일 적, 전체 아군등 )

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spell")
	int32 EffectAmount; // 효과의 강도 ( 피해량, 회복량 등 )


	virtual void ApplyEffect_Implementation(AActor* Target) override;

protected:
	void ApplyDamageEffect(AActor* Target);
	void ApplyHealEffect(AActor* Target);
	void ApplyBuffEffect(AActor* Target);
	void ApplyDebuffEffect(AActor* Target);
};

USpellCardData.cpp

#include "SpellCardData.h"

void USpellCardData::ApplyEffect_Implementation(AActor* Target)
{
	switch (EffectType)
	{
	case ESpellEffectType::Damage:
		ApplyEffect(Target);
		break;
	case ESpellEffectType::Heal:
		ApplyHealEffect(Target);
		break;
	case ESpellEffectType::Buff:
		ApplyBuffEffect(Target);
		break;
	case ESpellEffectType::Debuff:
		ApplyDebuffEffect(Target);
		break;
	default:
		UE_LOG(LogTemp, Warning, TEXT("EffectType is not Defind"));
		break;
	}
}

void USpellCardData::ApplyDamageEffect(AActor* Target)
{
	switch (TargetType)
	{
	case ESpellTargetType::SingleEnemy:
		// 단일 적에게 피해를 입힌다.
		if (Target)
		{
			// Target에 EffectAmount만큼 피해 적용
			UE_LOG(LogTemp, Log, TEXT("Applying %d Damage to single enemy"), EffectAmount);
		}
		break;
	case ESpellTargetType::AllEnemise:
		// 모든 적에게 피해를 입힌다
		UE_LOG(LogTemp, Log, TEXT("Applying %d Damage to all enemy"), EffectAmount);
		// 모든 적을 대상으로 EffectAmount 만큼 피해 적용 로직 추가
		break;
	default:
		UE_LOG(LogTemp, Log, TEXT("TargetType is not Compatible with Damage effect."));
		break;
	}
}

void USpellCardData::ApplyHealEffect(AActor* Target)
{
	switch (TargetType)
	{
	case ESpellTargetType::SingleAlly:
		// 단일 아군 회복
		if (Target)
		{
			// Target에 EffectAmount만큼 회복 적용
			UE_LOG(LogTemp, Log, TEXT("Applying %d heal to single ally"), EffectAmount);
		}
		break;
	case ESpellTargetType::AllAllies:
		// 모든 아군을 회복
		UE_LOG(LogTemp, Log, TEXT("Applying %d heal to all alles"), EffectAmount);
		// 모든 아군의 체력을 EffectAmount 만큼 회복 로직 추가
		break;
	default:
		UE_LOG(LogTemp, Log, TEXT("TargetType is not Compatible with Damage effect."));
		break;
	}
}

void USpellCardData::ApplyBuffEffect(AActor* Target)
{
	switch (TargetType)
	{
	case ESpellTargetType::SingleAlly:
		if (Target)
		{
			// 단일 아군에게 버프 적용
			// Target에 EffectAmount만큼 공격력 방어력 증가
			UE_LOG(LogTemp, Log, TEXT("Applying buff to single ally : +%d Attack"), EffectAmount);
		}
		break;
	case ESpellTargetType::AllAllies:
		// 모든 아군에게 버프 적용
		UE_LOG(LogTemp, Log, TEXT("Applying buff to all alles : +%d Attack"), EffectAmount);
		// 아군 전체에 공격력 방어력 증가 로직
		break;
	case ESpellTargetType::Self:
		// 자기 자신에게 버프 적용
		UE_LOG(LogTemp, Log, TEXT("Applying buff to self : +%d Attack"), EffectAmount);
		break;
	default:
		UE_LOG(LogTemp, Log, TEXT("TargetType is not Compatible with Damage effect."));
		break;
	}
}

void USpellCardData::ApplyDebuffEffect(AActor* Target)
{
	switch (TargetType)
	{
	case ESpellTargetType::SingleEnemy:
		if (Target)
		{
			// 단일 적에게 디버프 적용
			// Target에 EffectAmount만큼 공격력 방어력 감소
			UE_LOG(LogTemp, Log, TEXT("Applying Debuff to single enemy : -%d Attack"), EffectAmount);
		}
		break;
	case ESpellTargetType::AllEnemise:
		// 모든 적에게 디버프 적용
		UE_LOG(LogTemp, Log, TEXT("Applying Debuff to all enemies : -%d Attack"), EffectAmount);
		break;
	default:
		UE_LOG(LogTemp, Warning, TEXT("TargetType is not Compatible with Debuff Effect. "));
		break;
	}
}
  • 이런 식으로 해당 Enum값으로 능력과 효과를 나눠서 분리 할 수 있다.

특정 조건 효과 처리는??

  • 특정 조건
    • 하스스톤의 비밀(Secret) 카드는 특정 이벤트가 발생할 때 발동되는 카드
    • 조건 확인을 Tick 함수로 지속적으로 체크하지 않고도 구현
    • 이벤트 기반 시스템을 활용하여 발동 조건을 감지
    • 델리게이트(Delegate)와 게임 상태 매니저를 통해 이벤트가 발생할 때마다 특정 함수를 호출
    • 주요 이벤트 예시:
      • 영웅이 공격받음
      • 특정 카드가 소환됨
      • 적이 특정 마나 비용의 카드를 사용함
      • 이벤트가 발생하면, 비밀 카드가 해당 이벤트를 수신하여 조건이 충족되면 즉시 발동
  • 이벤트 기반 시스템 설계
    • 먼저, 비밀 카드의 발동 조건(예: 적이 영웅을 공격, 특정 카드가 소환 등)이 발생할 수 있는 게임의 주요 이벤트를 정의
    • 각 이벤트가 발생할 때 델리게이트를 사용해 비밀 카드 발동 여부를 확인
    • SecretManager: 비밀 카드 관리 클래스
    • ASecretManager 클래스는 모든 비밀 카드를 관리하고, 조건이 충족되었을 때 적절한 비밀 카드를 발동

  • USecretCardData는 각각의 비밀 카드가 고유의 발동 조건과 효과를 가지고 있도록 합니다.
  • IsConditionMet 함수를 사용해 해당 비밀 카드의 발동 조건이 충족되는지 확인하고,
  • 조건이 충족되면 ActivateSecret으로 비밀 카드의 효과를 발동

SecretCardData.h

#pragma once

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

UENUM(BLueprintType)
enum class ESecretEffectType : uint8
{
	DamageEnemyHero,		// 적 영웅에게 피해
	SummonMinions,			// 아군 미니언 소환 
	CounterSpell,			// 마법 카드 무효화
	ProtectFriendlyHero,	// 아군 영웅 보호
	DamageAllEnemies		// 모든 적에게 피해
};

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

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

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Secret")
	ESecretEffectType EffectType;	// 비밀 카드 효과 유형

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Secret")
	int32 EffectAmount; // 효과의 강도 ( 피해량, 소환할 미니언 수 등 ) 

	// 비밀 발동 조건을 판별하는 함수
	UFUNCTION(BlueprintCallable, Category = "Secret")
	bool IsConditionMet(const FString& EventName) const;

	// 비밀 발동 함수
	UFUNCTION(BlueprintCallable, Category = "Secret")
	void ActivateSecret(AActor* Target);
};

SecretCardData.cpp

#include "SecretCardData.h"

bool USecretCardData::IsConditionMet(const FString& EventName) const
{
    // 비밀 카드의 조건을 EventName 으로 체크 ( 예 : "HeroAttacjed", "MinionSummoned"
    if (EventName == "HeroAttacked" && CardName == "Explosive Trap")
    {
        return true;
    }

    if (EventName == "MinionSummoned" && CardName == "Snake Trap")
    {
        return true;
    }

    // 추가 조건을 여기에 작성
    return false;
}

void USecretCardData::ActivateSecret(AActor* Target)
{
    switch (EffectType)
    {
    case ESecretEffectType::DamageEnemyHero:
        if (Target)
        {
            // 예시 : 적 영웅에게 EffectAmount 만큼 피해를 입힘
            UE_LOG(LogTemp, Log, TEXT("Secret Activated : %s deals %d Damage to the enemy hero"), *CardName, EffectAmount);
            // 예 : Target->TakeDamage(EffectAmount, ... );
        }
        break;
    case ESecretEffectType::SummonMinions:
        // 아군 미니언을 EffectAmount 수 만큼 소환
        UE_LOG(LogTemp, Log, TEXT("Secret Activated : %s Summons %d minions"), *CardName, EffectAmount);
        // 예 : 미니언 소환 함수 호출
        break;
    case ESecretEffectType::CounterSpell:
        // 적의 마법 카드를 무효화
        UE_LOG(LogTemp, Log, TEXT("Secret Activated : %s counter the enemy's spell"), *CardName);
        // 마법 카드 무효화 로직
        break;
    case ESecretEffectType::ProtectFriendlyHero:
        // 아군 영웅 보호 로직
        UE_LOG(LogTemp, Log, TEXT("Secret Activated : %s protects the friendly hero"), *CardName);
        // 보호 효과 적용
        break;
    case ESecretEffectType::DamageAllEnemies:
        // 모든 적에게 EffectAmount 만큼 피해를 줌
        UE_LOG(LogTemp, Log, TEXT("Secret Activated : %s deals %d Damage to all enemies"), *CardName, EffectAmount);
        break;
    default:
        UE_LOG(LogTemp, Log, TEXT("Unknown Secret Effect Type for %s "), *CardName);
        break;
    }

    UE_LOG(LogTemp, Log, TEXT("Secret Activate : %s"), *CardName);
    // 비밀 카드의 고유 효과를 여기에 구현
}

  • 비밀 카드를 관리하는 Manager를 하나 만든다

SecretManager.h

#pragma once

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

// 비밀 카드의 이벤트가 발생했을떄 실행할 델리게이트를 정의 합니다
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnHeroAttacked);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMinionSummoned);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnManaCostCardPlayed);

class USecretCardData;

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

	// 비밀 카드 리스트
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Secret")
	TArray<USecretCardData*> ActiveSecrets;

	// 델리게이트 선언 ( 이벤트 기반으로 발동 )
	UPROPERTY(BlueprintAssignable, Category = "Secret Events")
	FOnHeroAttacked OnHeroAttacked;

	UPROPERTY(BlueprintAssignable, Category = "Secret Events")
	FOnMinionSummoned OnMinionSummoned;

	UPROPERTY(BlueprintAssignable, Category = "Secret Events")
	FOnManaCostCardPlayed OnManaCostCardPlayed;

	// 비밀 카드 조건 검사 함수
	void CheckAndTriggerSecret(FString EventName, AActor* Target);
};

SecretManager.cpp

#include "SecretManager.h"
#include "SecretCardData.h"

// Sets default values
ASecretManager::ASecretManager()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

void ASecretManager::CheckAndTriggerSecret(FString EventName, AActor* Target)
{
	for (USecretCardData* Secret : ActiveSecrets)
	{
		if (Secret && Secret->IsConditionMet(EventName))
		{
			Secret->ActivateSecret(Target);
			ActiveSecrets.Remove(Secret);
			break;	// 조건을 만족하는 첫 비밀 카드 발동 후 종료
		}
	}
}

카드 게임은 턴이라는 개념이 존재하는데

  • 주요 Phase와 이벤트 처리
    • 게임 시작 시 (카드 선택 및 초기화)
    • 턴 시작 시 (마나 수정 충전, 카드 드로우, "턴 시작 시" 효과 발동 등)
    • 카드 드로우 (카드 10장 이상일 때 폐기 등)
    • 하수인 소환 (전투의 함성 효과 발동 등)
    • 주문 시전 (유효 대상 확인, 발동 등)
    • 턴 종료 시 ("턴 종료 시" 효과 발동 등)

하스스톤의 턴 구조와 다양한 Phase를 효과적으로 관리하기 위해,

  • 상태 머신(State Machine)을 기반으로 게임 Phase를 단계별로 관리하는 구조 활용. 상태 머신을 활용하면 각 Phase 간의 전환과 각 Phase에 대한 조건 처리를 유연하고 구조적으로 구현가능
  • 하스스톤에서의 주요 Phase는 다음과 같이 나눌 수 있으며, 각 Phase에는 특정 조건이나 이벤트가 발생할 때 실행되는 상태 전이가 존재

  • GameModeBase를 상속받은 C++ 클래스를 하나 만든다
    • AGameMode에 상태 머신 구조 추가
    • 게임 진행을 관리하는 AGameMode 클래스에 상태 머신 로직을 구현하고, 각 Phase에서의 전환 및 이벤트 처리를 담당

  • GameMode-> 상태전이를 할 객체, 상태전이에 필요한 턴 개념을 정의할 None 형태에 C++ 클래스 하나를 더 만든다

GameRule.h

#pragma once

#include "CoreMinimal.h"

UENUM(BlueprintType)
enum class EGamePhase : uint8
{
	GameStart,
	TurnStart,
	CardDraw,
	SummonMinion,
	CastSpell,
	TurnEnd,
	GameEnd
};

class TCG_API GameRule
{
public:
	GameRule();
	~GameRule();
};

  • 이벤트 기반 처리 - 특정 Phase에 진입하면 그에 따른 카드 효과, 마나 충전 등을 트리거

TCGGameMode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "GameRule.h"
#include "TCGGameMode.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTurnStart);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTurnEnd);


UCLASS()
class TCG_API ATCGGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	// Phase에 대한 Getter
	EGamePhase GetCurrentPhase() const;

	// Phase에 대한 Setter ( 전원 로직 포함 ) 
	void SetCurrentPhase(EGamePhase NewPhase);

	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnTurnStart OnTurnStart;

	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnTurnEnd OnTurnEnd;

private:
	// 현재 Phase를 나타내는 변수 ( private으로 캡슐화 )
	EGamePhase CurrentPhase;

	void HandleGameStartPhase();
	void HandleTurnStartPhase();
	void HandleCardDrawPhase();
	void HandleSummonMinionPhase();
	void HandleCastSpellPhase();
	void HandleTurnEndPhase();
	void HandleGameEndPhase();

};

TCGGameMode.cpp

#include "TCGGameMode.h"

void ATCGGameMode::SetCurrentPhase(EGamePhase NewPhase)
{
	if (CurrentPhase != NewPhase)
	{
		// Phase가 실제로 변경된 때만 전환 처리
		CurrentPhase = NewPhase;
		UE_LOG(LogTemp, Log, TEXT("Phase switched to : %d"), static_cast<int32>(CurrentPhase));

		switch (CurrentPhase)
		{
		case EGamePhase::GameStart:
			HandleGameStartPhase();
			break;
		case EGamePhase::TurnStart:
			HandleTurnStartPhase();
			break;
		case EGamePhase::CardDraw:
			HandleCardDrawPhase();
			break;
		case EGamePhase::SummonMinion:
			HandleSummonMinionPhase();
			break;
		case EGamePhase::CastSpell:
			HandleCastSpellPhase();
			break;
		case EGamePhase::TurnEnd:
			HandleTurnEndPhase();
			break;
		case EGamePhase::GameEnd:
			HandleGameEndPhase();
			break;
		default:
			break;
		}

	}
}

void ATCGGameMode::HandleGameStartPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("Game Start Phase"));
	OnTurnStart.Broadcast(); // 턴 시작 시 효과 발동
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::TurnStart);
}

void ATCGGameMode::HandleTurnStartPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("turn Start Phase"));
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::CardDraw);
}

void ATCGGameMode::HandleCardDrawPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("Card Draw Phase"));
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::SummonMinion);
}

void ATCGGameMode::HandleSummonMinionPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("SummonMinion Minion Phase"));
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::CastSpell);
}

void ATCGGameMode::HandleCastSpellPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("Cast Spell Phase"));
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::TurnEnd);
}

void ATCGGameMode::HandleTurnEndPhase()
{
	// 선공 및 후공 카드 설정, 동전 제공등 초기화 작업 수행 
	UE_LOG(LogTemp, Log, TEXT("Turn End Phase"));
	OnTurnEnd.Broadcast(); // 턴 종료 시 효과 발동
	// 다음 Phase로 전환
	SetCurrentPhase(EGamePhase::TurnStart);
}

void ATCGGameMode::HandleGameEndPhase()
{
	// 게임 종료 및 승패 처리
	UE_LOG(LogTemp, Log, TEXT("Game end Phase"));
}
profile
This is my study archive

0개의 댓글