본문 바로가기

언리얼 + cpp

언리얼C++ 8일차(TakeDamage와 ApplyDamage, GameState)

지금까지 아이템들을 구현했고 이제 아이템이 어떤 기능을 하는지 실제로 구현을 해야한다.

캐릭터

캐릭터에 체력 관련 변수와 함수를 추가하고 데미지를 받는 함수 TakeDamage를 추가한다.

// JinCharacter.h
public:
	UFUNCTION(BlueprintPure, Category = "Health")
	float GetHealth() const; // 현재 체력을 가져오는 함수
	UFUNCTION(BlueprintCallable, Category = "Health")
	void AddHealth(float Amount); // 체력 회복 함수
    
protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float MaxHealth; // 최대 체력
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float Health; // 현재 체력
	void OnDeath(); // 체력이 0 이하가 될 때 사망 처리 함수
    
    // 데미지 처리 함수 - 외부로부터 데미지를 받을 때 호출됨
    // 또는 AActor의 TakeDamage()를 오버라이드
	virtual float TakeDamage(
		float DamageAmount, // 입은 데미지 양
		struct FDamageEvent const& DamageEvent, // 어떤 유형으로 데미지를 입혔는지(여기선 필요없음)
		AController* EventInstigator, // 데미지를 발생시킨 주체(여기선 필요없음)
		AActor* DamageCauser) override; // 데미지를 일으킨 오브젝트(지뢰)
// JinCharacter.cpp
// 생성자
    MaxHealth = 100.0f;
    Health = MaxHealth;
    
float AJinCharacter::GetHealth() const
{
	return Health;
}

void AJinCharacter::AddHealth(float Amount) // HealthItem에서 사용할 함수
{	// Clamp함수로 최솟값과 최댓값을 제한한다.
	Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Warning, TEXT("Health increased to: %.1f"), Health);
}

float AJinCharacter::TakeDamage(
	float DamageAmount,
	FDamageEvent const& DamageEvent,
	AController* EventInstigator,
	AActor* DamageCauser)
{
	float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Warning, TEXT("Health decreased to: %.1f"), Health);

	if (Health <= 0.0f)
	{
		OnDeath();
	}

	return ActualDamage;
}

void AJinCharacter::OnDeath() // 사망 로직은 미구현, 로그 출력만
{
	UE_LOG(LogTemp, Error, TEXT("Character is Dead!"));
}
AActor::TakeDamage - 데미지를 받는 함수 (여기선 Character)
Actor의 가상 함수, 모든 액터가 기본적으로 이 함수를 가지고 있으며, 필요하다면 자식 클래스(캐릭터 등)에서 오버라이드할 수 있다.
실제로 체력 감소 또는 특수한 데미지 처리 로직을 이 안에서 구현하게 된다.

UGamePlayStatics::ApplyDamage() - 데미지를 주는 함수 (여기선 MineItem, 지뢰에서 구현함)
공격자(또는 폭발물 등)가 데미지를 줄 대상 액터와 데미지 양, 데미지를 유발한 주체 등을 인자로 넘겨 호출한다.
내부적으로 대상 액터의 TakeDamage() 함수를 호출하려 시도한다.

UGameplayStatics::ApplyDamage(
Actor,
ExplosionDamage,
Instigator, // EventInstigator (Controller)
this, // DamageCauser (지뢰 액터)
UDamageType::StaticClass() // DamageType
);
=> ApplyDamage에 넣은 인자들이 자동으로 TakeDamage의 인자로 전달된다.
float TakeDamage(
float DamageAmount, // = ExplosionDamage
const FDamageEvent& DamageEvent, // = DamageTypeClass로 만든 이벤트
AController* EventInstigator, // = Instigator
AActor* DamageCauser // = this (지뢰)
);

지뢰 아이템

지뢰 아이템 (MineItem)이 폭발할 때, 주변 액터에게 데미지를 주려면 UGameplayStatics::ApplyDamage 함수를 호출해 해당 액터의 TakeDamage()가 실행되도록 하면 됩니다.

void AMineItem::Explode()
{
	TArray<AActor*> OverlappingActors;
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors)
	{
		if (Actor && Actor->ActorHasTag("Player"))
		{
			UGameplayStatics::ApplyDamage(
				Actor, // 데미지를 입을 액터(이 액터의 TakeDamage 함수가 자동 호출됨)
				ExplosionDamage, // 입힐 데미지 양
				nullptr, // 데미지를 준 주체(컨트롤러, 지뢰를 심은 플레이어가 있다면 그 플레이어)
				this, // 데미지를 주는 오브젝트(지뢰)
				UDamageType::StaticClass() //데미지 타입은 기본 타입
			);
		}
	}
	DestroyItem();
}
  • ApplyDamage()
    1. 대상 액터(Actor)가 존재하는지 확인.
    2. 대상 액터의 TakeDamage() 함수를 호출합니다.
    3. DamageType은 여러 가지 파생 클래스를 만들어 물리/화염/독 등 다양한 데미지 유형을 정의할 수 있습니다. (지금은 기본값 사용).
  • 지뢰는 독립적으로 스폰된 뒤 폭발하므로 EventInstigator를 nullptr로 둡니다.
    • 멀티플레이에서 “누가 지뢰를 설치했느냐”를 추적하려면, 생성 시점에 InstigatorController 정보를 넣어줄 수도 있습니다.

힐링 아이템

힐링 아이템(HealingItem)이 플레이어를 회복시킵니다. AddHealth() 함수를 직접 호출해 체력을 증가시킵니다.

void AHealingItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (AJinCharacter* PlayerCharacter = Cast<AJinCharacter>(Activator))
		{
			PlayerCharacter->AddHealth(HealAmount);
		}
		DestroyItem();
	}
}

점수 관리 시스템 구현(GameState)

언리얼 엔진에서 GameMode와 GameState는 게임의 전역 정보를 유지하고, 필요할 경우 멀티플레이어 환경에서 해당 정보를 서버와 클라이언트 간에 동기화하는 역할을 한다.

  • GameMode
    • “게임의 규칙(룰)”을 정의하고 관리합니다.
    • 어떤 캐릭터를 스폰할지, 플레이어가 사망했을 때 어떻게 처리할지를 결정합니다.
    • 멀티플레이에서는 서버 전용으로 동작합니다(클라이언트에는 존재하지 않음).
  • GameState
    • 게임 플레이 전반에서 “공유되어야 하는 전역 상태”를 저장합니다. GameState는 기본적으로 “레벨당 1개” 존재하며, 엔진 내부에서 데이터 동기화를 고려해 설계되었기에 전역 데이터 관리용으로 적합합니다.
    • 대표적으로 점수, 남은 시간, 현재 게임 단계(Phase), 스폰된 오브젝트의 총 개수 등을 저장합니다.
    • 멀티플레이에서는 서버가 관리하고, 클라이언트는 이를 자동으로 동기화 받아볼 수 있습니다.

싱글 플레이에서는 전역적으로 공유해야 할 정보를 GameState를 사용해서 한군데서 관리하면 유지보수가 편해진다.

 

GameStateBase를 상속받는 클래스를 생성하고 이 클래스에 점수 변수와 점수 획득 함수를 구현한다.

// JinGameStateBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "JinGameStateBase.generated.h"

UCLASS()
class UNREAL_CPP_STUDY_API AJinGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	
public:
	AJinGameStateBase();

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score; // 전역 점수를 저장하는 변수
	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const; // 현재 점수를 읽는 함수
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int Amount); // 점수를 추가해주는 함수
};
// JinGameStateBase.cpp
#include "JinGameStateBase.h"

AJinGameStateBase::AJinGameStateBase()
{
	Score = 0;
}

int32 AJinGameStateBase::GetScore() const
{
	return Score;
}

void AJinGameStateBase::AddScore(int Amount)
{
	Score += Amount;
	UE_LOG(LogTemp, Warning, TEXT("Score: %d"), Score);
    // 필요하면 여기서 최대 점수 제한, 점수 획득 사운드 재생 등을 넣을 수 있다.
}

기존에 만든 GameMode에서 GameStateClass를 지금 만든 클래스로 설정한다.

// FirstGameMode.cpp
#include "FirstGameMode.h"
#include "JinCharacter.h"
#include "JinPlayerController.h"
#include "JinGameStateBase.h"

AFirstGameMode::AFirstGameMode()
{
	DefaultPawnClass = AFirstGameMode::StaticClass();
	PlayerControllerClass = AJinPlayerController::StaticClass();
	GameStateClass = AJinGameStateBase::StaticClass(); // 추가
}

코인 아이템

코인 아이템에서 JinGameStateBase에서 설정한 점수 획득 함수를 사용하여 점수가 오르도록 한다.

// CoinItem.cpp
void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (UWorld* World = GetWorld())
		{
			if (AJinGameStateBase* GameState = World->GetGameState<AJinGameStateBase>())
			{
				GameState->AddScore(PointValue);
			}
		}
		DestroyItem();
	}
}