캐릭터 체력 시스템 구현

1️⃣캐릭터 클래스에 체력 변수 및 함수 선언

  • PlayerState를 사용하지 않는 이유
    • 엔진에서 PlayerState는 주로 멀티 플레이 환경에서 각 플레이어간 고유 데이터 동기화를 위해 사용
    • 하지만 싱글 플레이 게임에서는 이런 동기화 필요 없어 캐릭터 클래스 자체에 체력이나 스코어 변수를 넣어 관리할 예정
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;

UCLASS()
class SPARTAPROJECT_API ASpartaCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    ASpartaCharacter();

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
    USpringArmComponent* SpringArmComp;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
    UCameraComponent* CameraComp;

    // 현재 체력을 가져오는 함수
    UFUNCTION(BlueprintPure, Category = "Health")
    int32 GetHealth() const;
    // 체력을 회복시키는 함수
    UFUNCTION(BlueprintCallable, Category = "Health")
    void AddHealth(float Amount);

protected:
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    UFUNCTION()
    void Move(const FInputActionValue& value);
    UFUNCTION()
    void StartJump(const FInputActionValue& value);
    UFUNCTION()
    void StopJump(const FInputActionValue& value);
    UFUNCTION()
    void Look(const FInputActionValue& value);
    UFUNCTION()
    void StartSprint(const FInputActionValue& value);
    UFUNCTION()
    void StopSprint(const FInputActionValue& value);

    // 최대 체력
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
    float MaxHealth;
    // 현재 체력
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Health")
    float Health;
    // 사망 처리 함수 (체력이 0 이하가 되었을 때 호출)
    UFUNCTION(BlueprintCallable, Category = "Health")
    virtual void OnDeath();

		// 데미지 처리 함수 - 외부로부터 데미지를 받을 때 호출됨
    // 또는 AActor의 TakeDamage()를 오버라이드
    virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

private:
    float NormalSpeed;
    float SprintSpeedMultiplier;
    float SprintSpeed;
};
  • MaxHealth : 캐릭터의 최대 체력을 나타냄
  • Health : 캐릭터의 현재 체력을 나타냄
  • AddHealth() : 아이템등을 통해 체력을 회복할 때 호출하는 함수로, 내부에선 체력을 회복 시킴
  • OnDeath(): 체력이 0 이하가 되었을 때 사망 처리

2️⃣데미지 및 회복 처리

  • 엔진내에 데미지 시스템
    • UGameplayStatics::ApplyDamage(데미지 발생)와 AActor::TakeDamage(데미지 처리) 함수가 제공됨
  • 데미지 처리 흐름(ApplyDamage -> ApplyDamage)
    • UGameplayStatics::ApplyDamage
      • 공격하는 대상이 데미지를 줄 대상 액터와 데미지 양, 데미지를 유발한 주체가 누구인지 등을 인자로 넘겨 호출
      • 내부적으로 대상 액터의 TakeDamage() 함수를 호출하려 시도
    • AActor::TakeDamage
      • AActor의 가상함수로, 모든 액터가 가지고 있어 필요하면 자식에서 오버라이드 할 수 있음
      • 실제 체력 감소 또는 특수 데미지 처리 로직이 구현됨
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Actor.h"
#include "Kismet/GameplayStatics.h"

ASpartaCharacter::ASpartaCharacter()
{
    PrimaryActorTick.bCanEverTick = false;

    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArmComp->SetupAttachment(RootComponent);
    SpringArmComp->TargetArmLength = 300.0f;
    SpringArmComp->bUsePawnControlRotation = true;

    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
    CameraComp->bUsePawnControlRotation = false;

    NormalSpeed = 600.0f;
    SprintSpeedMultiplier = 1.7f;
    SprintSpeed = NormalSpeed * SprintSpeedMultiplier;

    GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;

		// 초기 체력 설정
    MaxHealth = 100.0f;
    Health = MaxHealth;
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        if (ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController()))
        {
            if (PlayerController->MoveAction)
            {
                EnhancedInput->BindAction(
                    PlayerController->MoveAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::Move
                );
            }

            if (PlayerController->JumpAction)
            {
                EnhancedInput->BindAction(
                    PlayerController->JumpAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::StartJump
                );

                EnhancedInput->BindAction(
                    PlayerController->MoveAction,
                    ETriggerEvent::Completed,
                    this,
                    &ASpartaCharacter::StopJump
                );
            }

            if (PlayerController->LookAction)
            {
                EnhancedInput->BindAction(
                    PlayerController->LookAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::Look
                );
            }

            if (PlayerController->SprintAction)
            {
                EnhancedInput->BindAction(
                    PlayerController->SprintAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::StartSprint
                );

                EnhancedInput->BindAction(
                    PlayerController->SprintAction,
                    ETriggerEvent::Completed,
                    this,
                    &ASpartaCharacter::StopSprint
                );
            }
        }
    }
}

void ASpartaCharacter::Move(const FInputActionValue& value)
{
    if (!Controller) return;

    const FVector2D MoveInput = value.Get<FVector2D>();

    if (!FMath::IsNearlyZero(MoveInput.X))
    {
        AddMovementInput(GetActorForwardVector(), MoveInput.X);
    }

    if (!FMath::IsNearlyZero(MoveInput.Y))
    {
        AddMovementInput(GetActorRightVector(), MoveInput.Y);
    }
}

void ASpartaCharacter::StartJump(const FInputActionValue& value)
{
    if (value.Get<bool>())
    {
        Jump();
    }
}

void ASpartaCharacter::StopJump(const FInputActionValue& value)
{
    if (!value.Get<bool>())
    {
        StopJumping();
    }
}

void ASpartaCharacter::Look(const FInputActionValue& value)
{
    FVector2D LookInput = value.Get<FVector2D>();

    AddControllerYawInput(LookInput.X);
    AddControllerPitchInput(LookInput.Y);
}

void ASpartaCharacter::StartSprint(const FInputActionValue& value)
{
    if (GetCharacterMovement())
    {
        GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
    }
}

void ASpartaCharacter::StopSprint(const FInputActionValue& value)
{
    if (GetCharacterMovement())
    {
        GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
    }
}

// 체력 회복 함수
void ASpartaCharacter::AddHealth(float Amount)
{
		// 체력을 회복시킴. 최대 체력을 초과하지 않도록 제한함
    Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
    UE_LOG(LogTemp, Log, TEXT("Health increased to: %f"), Health);
}

// 데미지 처리 함수
float ASpartaCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
		// 기본 데미지 처리 로직 호출 (필수는 아님)
    float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

		// 체력을 데미지만큼 감소시키고, 0 이하로 떨어지지 않도록 Clamp
    Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
    UE_LOG(LogTemp, Warning, TEXT("Health decreased to: %f"), Health);

		// 체력이 0 이하가 되면 사망 처리
    if (Health <= 0.0f)
    {
        OnDeath();
    }

		// 실제 적용된 데미지를 반환
    return ActualDamage;
}

// 사망 처리 함수
void ASpartaCharacter::OnDeath()
{
    UE_LOG(LogTemp, Error, TEXT("Character is Dead!"));

    // 사망 후 로직
}
  • AddHelath(float Amount)
    •  체력을 일정량 회복
    • FMath::Clamp를 통해 최대 체력을 초과하지 않도록 제한
  • TakeDamage(....)
    • 엔진에서 제공하는 기본 데미지 시스템 함수
    • DamageAmount : 데미지 값
    • EventInstigator : 데미지를 유발한 주체(Controller)
    • DamageCauser :데미지를 직접 발생시킨 오브젝트(총알, 폭발물등)
    • 반환 값 : 실제 적용된 데미지(기본 로직은 DamageAmount와 동일한 경우가 많지만, 게임 상황에 따라 감소 또는 증폭등을 처리할 수 있음)
    • OnDeath()
      • 체력이 0 이하로 떨어졌을 때 사마 로직을 처리
      • 흔히 입력 비활성화, Ragdoll 적용 , 사망 애니매이션 처리등을 함

3️⃣지뢰 아이템 데미지 함수 수정

  • 지뢰 아이템이 폭발하면 주변 액터에게 데미지를 주어야 하기 때문에 UGameplayStatics::ApplyDamage 함수를 호출해 해당 액터의 TakeDamage가 실행되도록 구현
#include "MineItem.h"
#include "Components/SphereComponent.h"

AMineItem::AMineItem()
{
		ExplosionDelay = 5.0f;
		ExplosionRadius = 300.0f;
		ExplosionDamage = 30.0f;
		ItemType = "Mine";
	
		ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
		ExplosionCollision->InitSphereRadius(ExplosionRadius);
		ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
		ExplosionCollision->SetupAttachment(Scene);
}

void AMineItem::ActivateItem(AActor* Activator)
{
		GetWorld()->GetTimerManager().SetTimer(
			ExplosionTimerHandle,
			this,
			&AMineItem::Explode,
			ExplosionDelay,
			false
		);
}

void AMineItem::Explode()
{
		TArray<AActor*> OverlappingActors;
		ExplosionCollision->GetOverlappingActors(OverlappingActors);
	
		for (AActor* Actor : OverlappingActors)
		{
				if (Actor && Actor->ActorHasTag("Player"))
				{
						// 데미지를 발생시켜 Actor->TakeDamage()가 실행되도록 함
	          UGameplayStatics::ApplyDamage(
	              Actor,                      // 데미지를 받을 액터
	              ExplosionDamage,            // 데미지 양
	              nullptr,                    // 데미지를 유발한 주체 (지뢰를 설치한 캐릭터가 없으므로 nullptr)
	              this,                       // 데미지를 유발한 오브젝트(지뢰)
	              UDamageType::StaticClass()  // 기본 데미지 유형
	          );
				}
		}
	
		// 폭발 이후 지뢰 아이템 파괴
		DestroyItem();
}
  • ApplynDamage()
    • 대상 액터가 존재하는지 확인
    • 대상 액터의 TakeDamage 함수 호출
    • DamageType은 여러가지 파생 클래스를 만들어 물리/화염/독 등 다양한 데미지 유형을 정의할 수 있음(현재는 기본값 사용)
  • 지뢰는 독립적으로 스폰된뒤 폭발하므로 EventInstigator를 nullptr로 둠
    • 멀티에서는 누가 설치했는지를 추적하려면, 생성 시점에 Instigator나 Controller 정보를 넣어줄 수도 있음

4️⃣힐링 아이템 체력 회복 함수

#include "HealingItem.h"
#include "SpartaCharacter.h"

AHealingItem::AHealingItem()
{
		HealAmount = 20.0f;
		ItemType = "Healing";
}

void AHealingItem::ActivateItem(AActor* Activator)
{
		if (Activator && Activator->ActorHasTag("Player"))
		{
				if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator))
				{
						// 캐릭터의 체력을 회복
						PlayerCharacter->AddHealth(HealAmount);
				}
			
				DestroyItem();
		}
}
  • ActorHasTag("Player") 로 플레이어인지 판별하는 단순한 로직
    • 캐릭터를 구분하는 더 안전한 방법(예: Cast<ASpartaCharacter>(Activator))을 쓰거나, Collision 체널을 이용하는 식으로 바꿀 수 있음
  • AddHealth()에서 FMath::Clamp를 사용해 최대 체력 이상으로 올라가지 않도록 제한함

점수 관리 시스템 구현

1️⃣GameMode와 GameState의 연계 이해

  • 엔진내에 GameMode와 GameState는 게임의 전역 정보를 유지하고, 필요한경우 멀티 플레이 환경에서 해당 정보를서버와 클라리언트 간에 동기화 하는 역할을 함
    • GameMode
      • 게임의 규칙을 정의하고 관리
      • 어떤 캐릭터를 스폰할지, 플레이어가 사망했을 때 어떻게 처리할지 결정
      • 멀티플레이에서느 서버 전용으로 동작(클라이언트에는 존재하지 않음)
    • GameState
      • 게임 플레이 전반에서 공유되어야 하는 전역 상태를 저장
      • 기본적으로 레벨당 1개 존재하며, 엔진 내부에서 데이터 동기화를 고려해 설계되어 전역 데이터 관리용에 적합
      • 대표적으로 점수, 남은시간, 현재 게임 단계(Phase), 스폰된 오브젝트의 총 갯수 등 저장
      • 멀티 플레이에서 서버가 관리하고, 클라이언트는 이를 자동으로 동기화 받아볼 수 있음
    • 싱글플레이에서도 GameState를 사용할 수 도 있는데 이는 전역적으로 공유해야 할 정보를 한군데서 관리하면 유지보수 관리가 더 편하기 때문에 몇개의 아이템이 스폰됐는지, 현재 게임 진행도가 어느정도인지 같은 데이터를 관리할 수 있음

2️⃣GameState에서 점수 데이터 및 함수 추가

#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(VisibleAnywhre, BlueprintReadWrite, Category="Score")
    int32 Score;

    // 현재 점수를 읽는 함수
    UFUNCTION(BlueprintPure, Category="Score")
    int32 GetScore() const;
    // 점수를 추가해주는 함수
    UFUNCTION(BlueprintCallable, Category="Score")
    void AddScore(int32 Amount);
};

.cpp

#include "SpartaGameStateBase.h"

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

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

void ASpartaGameStateBase::AddScore(int32 Amount)
{
    Score += Amount;
}
  • Score : 현재 누적된 점수 나타냄
  • AddScore(int32 Amount) : 점수를 Amount만큼 증가시킴, 필요하면 최대 점수 제한, 점수 획득 사운드 등을 넣을 수 있음
  • GetSocre() : 현 점수 반환, 블루프린트에서 UI를 만들때도 이함수를 이용해 쉽게 점수를 가져올 수 있음

3️⃣GameMode와 GameState연동

  • 기존에 만들었던 GameMode에서 GameStateClass를 우리가 만든 SpartaGameState로 정

 

#include "SpartaGameMode.h"

ASpartaGameMode::ASpartaGameMode()
{
    PlayerControllerClass = ASpartaPlayerController::StaticClass();
    DefaultPawnClass = ASpartaCharacter::StaticClass();
    // 우리가 만든 GameState로 설정
    GameStateClass = ASpartaGameStateBase::StaticClass();
}
  • 이제 코드가 컴파일 되면 에디터에서 해당 클래스를 인식함
  • SpartaGameSateBase를 더확장하거나 블루프린트에서 사용하기 이해 블루프린트 클래스로 만들어 사용함
  • ProjectSettings적용
    • DafaultGameMode를 우리가 만든 GameMode로 바꿔 설정한다
    • 그 뒤 Game State Class도 우리가 만든 클래스로 맞처주면 설정이 적용된다
  • WorldSettings에서 설정(레벨별 오버라이드 가능)

    • World Settings에서 GameModeOverride를 설정하면, 특정 레벨에서만 다른 GameMode, Gamenstate를 사용할 수 있음

4️⃣코인 아이템 점수 획득 함수 수정

#include "CoinItem.h"
#include "Engine/World.h"
#include "SpartaGameStateBase.h"

ACoinItem::ACoinItem()
{
		PointValue = 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();
		}
}
  • GetWorld()->GetGameState<ASpartaGameStateBase>()로 게임 스테이트를 가져오고, AddScore(PointValue) 함수를 호출해 점수 올림
  • PointValue 는 이 코인 아이템이 제공하는 점수양이며, ACoinItem의 맴버 변수로 관리함
  • 이 과정을 통해 플레이어가 코인을 획득하면, 전역적으로 관리되는 GameState에서 점수가 증가하고, 코인은 사라져 한 번만 획득되도록 처리됨