C++과 Unreal Engine으로 3D 게임 개발 9

김여울·2025년 7월 18일

내일배움캠프

목록 보기
45/139

📍 3주차 4강

8. 캐릭터 체력 및 점수 관리 시스템 구현하기

- 필요한 것

  • 체력 시스템
    • 아이템 사용 효과 적용 가능
  • 점수 시스템
    • 코인 점수 관리

8.1 캐릭터 체력 시스템 구현하기

a. 캐릭터 클래스에 체력 변수 및 함수 선언

  • Single Play → 캐릭터에 체력 구현
  • 변수 2개 필요 → Health MaxHealth(100)

SpartaCharacter.h

public:
	// 2. 체력을 갖고 오는 함수
    UFUNCTION(Blueprintpure, Category = "Health")
   float GetHealth() const;

protected:
	// 1. 체력
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float MaxHealth;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float Health;

get 전용 = BlueprintPure

SpartaCharacter.cpp

ASpartaCharacter::ASpartaCharacter()
{ 
	// 1. 체력 변수들 초기화
	MaxHealth = 100.0f;
	Health = MaxHealth;
}

// ...

// 2. GetHealth 함수 갖고 오기
float ASpartaCharacter::GetHealth() const
{
	return Health;
}

b. 데미지 및 회복 처리

💣 데미지 처리

  • UGamePlayStatics::ApplyDamage() - 데미지 입힘
    Static 함수여서 객체 생성 없이도 호출 가능
    → 어떤 Actor에 데미지를 입혔는지 전달 받음
    → 그 Actor에서 TakeDamage 호출
  • AActor::TakeDamage() - 데미지 받음
    overriding 가능 → 데미지 처리 방식은 각 Actor에 따라 다르게 구현

SpartaCharacter.h

protected:
	// 1. 데미지 받는 함수 오버라이드
	virtual float TakeDamage(
	float DamageAmount,	// 데미지 얼마나 입었나
	struct FDamageEvent const& DamageEvent,	// 데미지 유형 - 스킬에 따라 어떤 데미지?
	AController* EventInstigator,	// 데미지 누가 입힘? (지뢰를 심은 사람)
	AActor* DamageCasuer) override;	// 데미지를 일으킨 오브젝트 (지뢰)
    
    void OnDeath();	// 2. 사망처리 함수

SpartaCharacter.cpp

// 1. 데미지 받는 함수
float ASpartaCharacter::TakeDamage(
	float DamageAmount,
	struct FDamageEvent const& DamageEvent,
	AController* EventInstigator,
	AActor* DamageCasuer)
{
	// 부모 클래스 거 호출
	float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCasuer);

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

	if (Health <= 0.0f)
	{
		OnDeath();	// 사망 
	}
	
	return ActualDamage;
}

// 2. 사망 처리 함수
void ASpartaCharacter::OnDeath()
{
	// 게임 종료 로직 
}

Damage 차이

  • DamageAmount 나한테 들어온 데미지 (방어력 없다고 생각하면 됨, 찐 데미지)
  • ActualDamage 내가 입은 데미지 (방어구 착용했거나 저항시스템 있으면 데미지 덜 입음)
  • 지금 게임은 간단해서 방어구 없음
    그냥 Health - DamageAmount 해도 됨

Clamp 함수

  • 값을 일정 범위 안으로 제한해주는 함수
  • 마이너스가 됐더라도 0보다 더 떨어질 일 없음
  • MaxHealth 상태에서 더 올라갈 일 없음
// 구조
타입 FMath::Clamp(타입 Value, 타입 Min, 타입 Max);

// 예시
float Result = FMath::Clamp(Score, 0.0f, 100.0f);

❤️‍🩹 회복 처리

SpartaCharacter.h

// 얼마 회복 받을지
UFUNCTION(BlueprintCallable, Category = "Heal")
void AddHealth(float Amount);

SpartaCharacter.cpp

void ASpartaCharacter::AddHealth(float Amount)
{
	Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Warning, TEXT("Health increased to: %f"), Health);
}

c. 지뢰 아이템 데미지 함수 수정

MineItem.h

#include "SpartaCharacter.h"

void AHealingItem::ActivateItem(AActor* Activator)
{
	// DestroyItem();
	if (Activator && Activator->ActorHasTag("Player"))
	{
		// Activator가 Actor 클래스이므로 얘를 SpartaCharacter로 캐스팅 해야함
		if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator))
		{
			PlayerCharacter->AddHealth(HealAmount);
		}
		DestroyItem();
	}
}

MineItem.cpp

#include "Kismet/GameplayStatics.h"

// ...

void AMineItem::Explode()
{
	TArray<AActor*> OverlappingActors;  // 범위 내 겹치는 액터 검색
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	// 범위 내 돌면서 태그 확인
	for (AActor* Actor : OverlappingActors)
	{
		if (Actor && Actor->ActorHasTag("Player")) // Actor가 nullptr이 아니고 플레이어 태그가 있는지 확인
		{
			UGameplayStatics::ApplyDamage(
				Actor,	// 데미지를 받을 액터
				ExplosionDamage,	// 데미지의 양
				nullptr,	// 데미지를 유발한 주체
				this,	// 데미지를 입힌 액터
				UDamageType::StaticClass()	// 데미지의 유형 - 데미지 타입의 기본 유형을 class로 넘겨줌
			);
		}
	}
    DestroyItem();
}

💥 MineItem 데미지가 특정 레벨에서만 작동하던 문제 해결

  • MineItem이 Basic 레벨에선 데미지 잘 들어가고 로그도 찍힘
  • 그런데 Intermediate / Advanced 레벨에선 데미지 로그 출력 안 됨

→ ActorHasTag("Player") 조건이 통과되지 않아서 ApplyDamage() 호출도 안 된 듯... 플레이어 블루프린트에서 Actor Tag는 제대로 되어 있었음

// 이전 코드
if (Actor && Actor->ActorHasTag("Player"))

// 수정 후
if (Cast<ASpartaCharacter>(Actor))

d. 힐링 아이템 체력 회복 함수 수정

HealingItem.cpp

#include "SpartaCharacter.h"
void AHealingItem::ActivateItem(AActor* Activator)
{
	// DestroyItem();
	if (Activator && Activator->ActorHasTag("Player"))
	{
		// Activator가 Actor 클래스이므로 얘를 SpartaCharacter로 캐스팅 해야함
		if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator))
		{
			PlayerCharacter->AddHealth(HealAmount);
		}
		DestroyItem();
	}
}

8.2 점수 관리 시스템 구현하기

a. GameMode와 GameState의 연계 이해하기

PlayerState

  • 각 플레이어 마다의 정보를 관리해주는 GameMode의 클래스
  • 싱글 플레이에선 사용 안 함 / 멀티 플레이에서 사용

GameState

  • 전역 정보를 저장하는 클래스
  • 게임 진행 단계, 타이머, 스폰된 아이템 개수들, 얼마나 진행됐는지 등 관리 가능
  • 본격적인 멀티 플레이어 환경을 고려한 상황에 사용

GameStateBase

  • 싱글 플레이어 게임에 사용

b. GameState에 점수 데이터 및 함수 추가

SpartaGameBase.h

#pragma once

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

UCLASS()
class SPARTAPROJECT_API ASpartaGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	

public:
	ASpartaGameStateBase();	// 생성자

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;

	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const;
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Amount);
};

SpartaGameBase.cpp

#include "SpartaGameStateBase.h"

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

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

void ASpartaGameStateBase::AddScore(int32 Amount)
{
	Score += Amount;
}

c. GameMode와 GameState 연동

CoinItem.cpp

#include "CoinItem.h"
#include "Engine/World.h"	// GameState를 가져오기 위해 World 먼저 갖고 오기
#include "SpartaGameStateBase.h"

ACoinItem::ACoinItem()  // 생성자
{
	PointValue = 0;  // 초기화, 부모클래스라서 0점, 자손클래스에서 본인에 해당하는 변수로 값을 넘겨 받음 <다형성>
	ItemType = "DefaultCoin";  // 아이템 타입 초기화
}

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

d. 코인 아이템 점수 획득 함수 수정

CoinItem.cpp

#include "CoinItem.h"
#include "Engine/World.h"	// GameState를 가져오기 위해 World 먼저 갖고 오기
#include "SpartaGameStateBase.h"

ACoinItem::ACoinItem()  // 생성자
{
	PointValue = 0;  // 초기화, 부모클래스라서 0점, 자손클래스에서 본인에 해당하는 변수로 값을 넘겨 받음 <다형성>
	ItemType = "DefaultCoin";  // 아이템 타입 초기화
}

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

💥 코인을 먹어도 점수 안 오르는 문제

코인 블루프린트 들어가서 값 직접 설정하기
짜잔~ 해결 완료


클래스 멤버(변수/함수)의 선언 순서

1️⃣ UPROPERTY 변수들 (리플렉션 변수)

2️⃣ 생성자 / BeginPlay / Tick 등 오버라이드 함수들 (가상함수 포함)

3️⃣ UFUNCTION 함수들 (리플렉션 함수)

4️⃣ 일반 함수들 (private helper 등)

UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    // 1. 리플렉션 변수
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Health;

    // 2. 가상 함수 (오버라이드)
    virtual void BeginPlay() override;

    // 3. 리플렉션 함수
    UFUNCTION(BlueprintCallable)
    void Heal(float Amount);

    // 4. 그냥 함수
    void LogHealth();
};

0개의 댓글