캐릭터 체력 시스템 구현
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의 가상함수로, 모든 액터가 가지고 있어 필요하면 자식에서 오버라이드 할 수 있음
- 실제 체력 감소 또는 특수 데미지 처리 로직이 구현됨
- UGameplayStatics::ApplyDamage
#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를 사용할 수 도 있는데 이는 전역적으로 공유해야 할 정보를 한군데서 관리하면 유지보수 관리가 더 편하기 때문에 몇개의 아이템이 스폰됐는지, 현재 게임 진행도가 어느정도인지 같은 데이터를 관리할 수 있음
- GameMode
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에서 점수가 증가하고, 코인은 사라져 한 번만 획득되도록 처리됨
'Unreal Bootcamp > Unreal C++' 카테고리의 다른 글
17. UI 위젯 설계와 실시간 데이터 (1) | 2025.02.12 |
---|---|
16.게임 루프 설계를 통한 게임 흐름 제어 (0) | 2025.02.11 |
14.아이템 스폰 및 레벨 데이터 관리 (0) | 2025.02.07 |
13.충돌 이벤트로 획득되는 아이템 구현 (0) | 2025.02.05 |
12.인터페이스 기반 아이템 클래스 (0) | 2025.02.04 |