1. 캐릭터 체력 시스템
이번에는 캐릭터에게 최대 체력과 현재 체력을 만들어주었다.
Header
// 최대 체력
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
float MaxHealth;
// 현재 체력
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Health")
float Health;
내가 이해한 것
MaxHealth
→ 캐릭터가 가질 수 있는 최대 체력
Health
→ 현재 캐릭터가 가지고 있는 실제 체력
예를 들어:
MaxHealth = 100
Health = 100
상태에서 데미지를 30 받으면:
MaxHealth = 100
Health = 70
이 된다.
2. 생성자에서 체력 초기화
MaxHealth = 100.0f;
Health = MaxHealth;
처음에는 최대 체력을 100으로 설정하고,
Health = MaxHealth;
현재 체력을 최대 체력과 동일하게 만들어준다.
즉:
게임 시작
↓
MaxHealth = 100
↓
Health = MaxHealth
↓
Health = 100
왜 Health = 100.0f라고 직접 쓰지 않고 MaxHealth를 넣었는가?
나중에 최대 체력을 변경하더라도 현재 체력을 자동으로 최대 체력에 맞출 수 있기 때문이다.
MaxHealth = 150.0f;
Health = MaxHealth;
로 바꾸면 현재 체력도 150이 된다.
3. 체력 조회 함수
UFUNCTION(BlueprintPure, Category = "Health")
int32 GetHealth() const;
현재 체력을 가져오는 함수이다.
CPP:
float AJunCharacter::GetHealth() const
{
return Health;
}
여기서 조금 특이했던 점은 Header에서는:
int32 GetHealth() const;
로 되어 있었는데 실제 Health는:
float Health;
이다.
따라서 현재 코드에서는 반환형을 float로 맞추는 것이 더 자연스럽다.
float GetHealth() const;
그리고 CPP도:
float AJunCharacter::GetHealth() const
{
return Health;
}
로 맞추는 게 좋다.
const를 붙이는 이유
이 함수는 현재 체력을 가져오기만 하고 캐릭터의 값을 변경하지 않기 때문이다.
GetHealth()
→ 읽기만 함
그래서:
const
를 붙인다.
4. 체력 회복 함수
UFUNCTION(BlueprintCallable, Category = "Health")
void AddHealth(float Amount);
CPP:
void AJunCharacter::AddHealth(float Amount)
{
Health = FMath::Clamp(
Health + Amount,
0.0f,
MaxHealth
);
UE_LOG(
LogTemp,
Log,
TEXT("Health increased to: %f"),
Health
);
}
FMath::Clamp()가 핵심
FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
의 의미는:
Health + Amount
↓
0보다 작으면 → 0
MaxHealth보다 크면 → MaxHealth
그 사이면 → 계산된 값
예를 들어 현재 체력이:
Health = 70
이고 회복량이:
Amount = 20
이면:
70 + 20 = 90
→ Health = 90
하지만:
Health = 90
Amount = 30
이면:
90 + 30 = 120
MaxHealth가 100이므로:
Health = 100
이 된다.
즉 체력이 최대 체력을 초과하지 않도록 방어하는 코드이다.
5. 데미지 처리 — TakeDamage()
이번에 새롭게 배운 중요한 부분.
virtual float TakeDamage(
float DamageAmount,
FDamageEvent const& DamageEvent,
AController* EventInstigator,
AActor* DamageCauser
) override;
AActor에 이미 존재하는 TakeDamage()를 오버라이드했다.
즉 외부에서:
ApplyDamage()
등을 통해 데미지가 들어오면 캐릭터가 이 함수를 통해 데미지를 처리할 수 있다.
CPP
float AJunCharacter::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: %f"),
Health
);
if (Health <= 0.0f)
{
OnDeath();
}
return ActualDamage;
}
흐름
외부에서 데미지 발생
↓
TakeDamage()
↓
Health - DamageAmount
↓
Clamp로 0~MaxHealth 제한
↓
Health <= 0 ?
↓ ↓
YES NO
↓ ↓
OnDeath() 계속 플레이
6. 사망 처리
virtual void OnDeath();
CPP:
void AJunCharacter::OnDeath()
{
UE_LOG(
LogTemp,
Error,
TEXT("Character is Dead!")
);
}
현재는 실제 사망 처리가 아니라 로그만 출력하도록 만들었다.
Health = 0
↓
OnDeath()
↓
"Character is Dead!"
나중에는 여기에:
- 캐릭터 Destroy
- 사망 애니메이션
- Game Over UI
- 리스폰
등을 넣을 수 있다.
7. HealingItem — 아이템이 플레이어의 체력을 회복
HealingItem에서는 아이템이 플레이어와 상호작용하도록 만들었다.
void AHealingItem::ActivateItem(AActor* Activator)
{
if (Activator && Activator->ActorHasTag("Player"))
{
if (AJunCharacter* PlayerCharacter =
Cast<AJunCharacter>(Activator))
{
PlayerCharacter->AddHealth(HealAmount);
}
DestroyItem();
}
}
흐름
플레이어가 아이템과 충돌
↓
ActivateItem()
↓
Activator가 존재하는가?
↓
Player 태그가 있는가?
↓
JunCharacter로 Cast 가능한가?
↓
AddHealth()
↓
체력 회복
↓
DestroyItem()
여기서 중요한 것은 아이템이 직접 Health 변수에 접근하지 않는다는 것이다.
PlayerCharacter->AddHealth(HealAmount);
라는 함수를 호출한다.
즉:
HealingItem
↓
AddHealth()
↓
Character가 자기 Health를 변경
이 구조가 된다.
8. CoinItem — 아이템과 GameState의 상호작용
CoinItem에서는 플레이어가 코인을 획득했을 때 점수를 증가시키는 구조를 만들었다.
if (Activator && Activator->ActorHasTag("Player"))
{
if (UWorld* World = GetWorld())
{
if (ASpartaGameStateBase* GameState =
World->GetGameState<ASpartaGameStateBase>())
{
GameState->AddScore(PointValue);
}
}
DestroyItem();
}
흐름은:
플레이어가 코인 획득
↓
Player 태그 확인
↓
World 가져오기
↓
GameState 가져오기
↓
AddScore(PointValue)
↓
점수 증가
↓
코인 Destroy
그런데 여기서 트러블슈팅 발생
현재 내 프로젝트에는:
SpartaGameStateBase.h
가 존재하지 않았다.
그래서:
C1083
Cannot open include file:
'SpartaGameStateBase.h'
에러가 발생했다.
강의 프로젝트에서 사용하는 클래스 이름을 내 프로젝트에 그대로 가져왔기 때문이다.
현재 아직 GameState를 구현하지 않았다면 해당 부분을 잠시 제외하고:
if (Activator && Activator->ActorHasTag("Player"))
{
DestroyItem();
}
정도로 아이템 획득/삭제까지만 구현할 수 있다.
9. MineItem — 플레이어에게 데미지를 주는 아이템
지뢰는 HealingItem과 반대다.
HealingItem:
아이템 → Player → AddHealth()
MineItem:
아이템 → Player → TakeDamage()
지뢰의 핵심은 Timer를 이용한 지연 폭발이다.
ExplosionDelay = 5.0f;
그리고:
GetWorld()->GetTimerManager().SetTimer(
ExplosionTimerHandle,
this,
&AMineItem::Explode,
ExplosionDelay,
false
);
이 코드는:
5초 후 Explode() 실행
이라는 뜻이다.
특히:
false
이므로 반복 타이머가 아니라 한 번만 실행된다.
10. 지뢰 폭발 → ApplyDamage()
UGameplayStatics::ApplyDamage(
Actor,
ExplosionDamage,
nullptr,
this,
UDamageType::StaticClass()
);
이 부분이 오늘 배운 아이템 → 캐릭터 데미지 전달의 핵심이다.
지뢰가 직접:
Health -= 30;
하는 것이 아니다.
대신:
MineItem
↓
ApplyDamage()
↓
Player의 TakeDamage()
↓
Health 감소
↓
Health <= 0 ?
↓
OnDeath()
라는 구조를 만든다.
이렇게 하면 데미지를 주는 쪽과 데미지를 받는 쪽을 분리할 수 있다.
11. 아이템 시스템 전체 구조
오늘 배운 내용을 연결하면 이런 구조가 된다.
┌──────────────┐
│ Player │
│ JunCharacter │
└──────┬───────┘
│
아이템과 상호작용
│
┌─────────────┼─────────────┐
↓ ↓ ↓
HealingItem CoinItem MineItem
│ │ │
↓ ↓ ↓
AddHealth() AddScore() ApplyDamage()
│ │ │
↓ ↓ ↓
Health Score TakeDamage()
│
↓
Health 감소
│
↓
OnDeath()
이 구조가 오늘 배운 것 중에서 가장 중요한 부분이라고 생각한다.
12. 오늘의 트러블슈팅 🔧
① 유니코드 지옥
코드를 복사하는 과정에서:
-> →
=
(
)
<
등으로 깨지는 현상이 발생했다.
예를 들어:
ExplosionCollision→SetupAttachment(Scene);
정상적인 C++는:
ExplosionCollision->SetupAttachment(Scene);
이다.
해결
깨진 코드를 그대로 사용하지 않고 정상적인 C++ 기호로 복구했다.
13. MineItem.h와 .cpp가 섞인 문제
오늘 가장 중요한 실수 중 하나.
MineItem.h에 있어야 하는:
UCLASS()
class AMineItem : public ABaseItem
{
GENERATED_BODY()
...
};
가 사라지고 대신:
AMineItem::AMineItem()
{
...
}
같은 CPP 구현 코드가 들어가 있었다.
그래서:
"어? 왜 MineItem.h에 UCLASS가 없지?"
라는 이상한 상황이 발생했다.
정상 구조
MineItem.h
클래스 선언
UPROPERTY
UFUNCTION
함수 원형
MineItem.cpp
생성자 구현
함수 구현
실제 로직
14. generated.h 에러
C1083
Cannot open include file:
'MineItem.generated.h'
이 에러도 처음에는 generated.h 파일 자체가 문제인 것처럼 보였다.
하지만 generated.h는 내가 직접 만드는 파일이 아니라 UnrealHeaderTool이 UCLASS, UPROPERTY, UFUNCTION 등을 기반으로 생성하는 파일이다.
따라서:
#include "MineItem.generated.h"
자체가 잘못된 것이 아니라,
그 전에 헤더 구조가 깨져서 UHT가 정상적으로 처리하지 못했을 가능성을 먼저 봐야 한다.
그리고 .generated.h는 헤더의 마지막 include로 둔다.
15. C1014 too many include files
C1014
too many include files: depth = 1024
이 에러도 발생했다.
이는 헤더가 서로 무한히 include되는 순환 참조가 발생했을 때 나타날 수 있다.
예:
MineItem.h
↓
BaseItem.h
↓
MineItem.h
↓
BaseItem.h
↓
...
따라서:
MineItem.cpp
#include "MineItem.h"
자체가 문제인 것이 아니라,
헤더끼리 서로 물고 있는 구조가 문제라는 것을 알게 되었다.
16. 오늘 배운 핵심 정리
체력
MaxHealth
→ 최대 체력
Health
→ 현재 체력
AddHealth()
→ 체력 회복
TakeDamage()
→ 데미지 처리
OnDeath()
→ 체력이 0이 되었을 때 사망 처리
아이템 상호작용
HealingItem
→ AddHealth()
→ 체력 증가
MineItem
→ ApplyDamage()
→ TakeDamage()
→ 체력 감소
CoinItem
→ GameState
→ AddScore()
→ 점수 증가
Unreal C++에서 중요한 구조
.h
→ 무엇이 존재하는지 선언
.cpp
→ 실제로 어떻게 동작하는지 구현
그리고:
#include "MineItem.generated.h"
은 직접 만드는 파일이 아니라 UHT가 생성하는 파일이며, 반드시! 헤더의 마지막 include로 둔다.
📝 오늘의 한 줄 회고
오늘은 단순히 체력과 아이템 기능을 구현한 것뿐만 아니라, HealingItem → AddHealth(), MineItem → ApplyDamage() → TakeDamage(), CoinItem → GameState → AddScore()처럼 "아이템이 플레이어의 데이터를 직접 건드리는 것이 아니라 적절한 함수를 통해 상호작용한다"는 구조를 이해했다. 또한 유니코드 깨짐, .h/.cpp 혼동, generated.h 생성 문제 등의 트러블슈팅을 통해 Unreal C++에서 파일 구조와 프로젝트 클래스 관계가 얼마나 중요한지도 경험했다.