오늘은 지난번 구현했던 보스 몬스터의 기능들을 바탕으로 전체적인 코드 구조를 다듬는 리팩토링 작업을 진행했다. 또한, 게임의 깊이를 더하기 위해 플레이어 캐릭터와 통일성 있는 방어력(Defense) 시스템을 몬스터에게 구현했다. 이 과정에서 virtual과 override를 활용한 함수 재정의의 중요성을 다시 한번 체감했으며, 버프 시스템을 더 안정적으로 만드는 방법에 대해 고민할 수 있었다. 👷♂️
Defense 스탯 구현TakeDamage 함수에서 '공격 데미지 - 방어력' 공식 적용하기AIMonsterBase, AIZombie, AIBoss, Rampage 등 AI 관련 클래스들의 역할을 명확히 하고 코드 구조 개선하기부모 클래스에 virtual 키워드로 선언된 함수는, 자식 클래스에서 override 키워드를 사용하여 자신만의 고유한 내용으로 재작성할 수 있다. 이번 프로젝트에서는 AIMonsterBase의 Die() 함수를 래그돌 방식으로 구현했지만, 보스인 Rampage는 래그돌 버그가 있어 애니메이션 기반의 죽음 처리가 필요했다. 이때 Rampage 클래스에서 Die() 함수를 override하여 래그돌 로직을 빼고 애니메이션 재생 로직만 남기는 방식으로 문제를 해결했다.
리팩토링은 단순히 코드를 예쁘게 만드는 것이 아니라, 겉으로 보이는 동작은 바꾸지 않으면서 코드의 내부 구조를 개선하는 작업을 의미한다. 이번에는 각 클래스의 역할에 맞게 변수와 함수의 위치를 재배치하고, 상세한 주석을 추가하여 가독성과 유지보수성을 크게 향상시켰다. 잘 정리된 코드는 미래에 새로운 기능을 추가하거나 버그를 수정할 때 드는 시간을 크게 줄여준다.
AIMonsterBase에 Defense 변수를 추가하고, TakeDamage 함수에서 방어력 공식을 적용하도록 수정했다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Engine/TimerHandle.h"
#include "AIMonsterBase.generated.h"
// ... (전방 선언)
UCLASS()
class SPARTA_TPROJECT_02_API AAIMonsterBase : public ACharacter
{
// ...
// --- 2.1. Combat Stats (전투 스탯) ---
// ...
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Stats")
float AttackDamage;
/** 몬스터의 기본 방어력. 받는 데미지를 이 값만큼 감소시킨다. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Stats")
float Defense;
// ...
/**
* @brief 데미지를 받는 로직을 처리하는 함수입니다. (엔진 기본 함수 재정의)
* @param DamageAmount 받은 기본 데미지 양
* @return 실제로 적용된 최종 데미지 양
*/
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
// ...
};
#include "AIMonsterBase.h"
// ...
float AAIMonsterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// 1. 방어력을 적용하여 실제 데미지(ActualDamage)를 계산한다. 최소 1의 데미지를 보장한다.
float ActualDamage = FMath::Max(1.0f, DamageAmount - Defense);
// 2. 계산된 데미지가 0보다 클 때만 후속 처리를 진행한다.
if (ActualDamage > 0.0f)
{
if (bIsDead) return 0.0f;
// 3. 체력을 감소시킨다.
CurrentHealth -= ActualDamage;
// ... (Perception 보고, 죽음/피격 처리, 체력바 업데이트 등)
}
// 실제로 적용된 최종 데미지 값을 반환한다.
return ActualDamage;
}
Rampage의 Energize 스킬에 방어력 증가 로직을 추가했다. 안정적인 버프 관리를 위해 OriginalDefense 변수를 사용했다.
// Rampage.h
#pragma once
#include "CoreMinimal.h"
#include "AIMonsterBase.h"
#include "Rampage.generated.h"
// ...
class SPARTA_TPROJECT_02_API ARampage : public AAIMonsterBase
{
// ...
private:
// --- Buff Management (버프 관리) ---
/** 분노 버프의 지속시간을 제어하기 위한 타이머 핸들입니다. */
FTimerHandle EnergizeTimerHandle;
/** 분노 버프 적용 전의 원래 이동 속도를 저장하기 위한 변수입니다. */
float OriginalWalkSpeed;
/** 분노 버프 적용 전의 원래 방어력을 저장하기 위한 변수입니다. */
float OriginalDefense;
// ...
};
#include "Rampage.h"
// ...
ARampage::ARampage()
{
// ...
// Rampage의 기본 방어력을 5로 설정한다.
Defense = 5.0f;
}
void ARampage::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시, 현재 스탯을 'Original' 변수에 백업한다.
if (GetCharacterMovement())
{
OriginalWalkSpeed = GetCharacterMovement()->MaxWalkSpeed;
}
OriginalDefense = Defense;
// ...
}
void ARampage::Energize()
{
if (bIsDead || GetWorldTimerManager().IsTimerActive(EnergizeTimerHandle)) return;
// ... (블랙보드, 이동속도, 색상 변경 로직) ...
// '원본' 방어력의 2배로 현재 방어력을 설정한다.
Defense = OriginalDefense * 2.0f;
GetWorldTimerManager().SetTimer(EnergizeTimerHandle, this, &ARampage::EndEnergize, 15.0f, false);
}
void ARampage::EndEnergize()
{
// ... (블랙보드, 이동속도, 색상 복구 로직) ...
// 현재 방어력을 저장해 둔 '원본' 방어력으로 되돌린다.
Defense = OriginalDefense;
}
AIZombie의 생성자에서 방어력을 0으로 명시적으로 설정했다.
// AIZombie.cpp
#include "AIZombie.h"
AAIZombie::AAIZombie()
{
// ... (다른 스탯)
Defense = 0.0f; // 좀비는 방어력이 없다.
}
// ...
TakeDamage를 수정할 때 단순히 DamageAmount - Defense로만 계산했다. 하지만 이렇게 하면 몬스터의 방어력이 공격력보다 높을 경우 데미지가 음수가 되어 오히려 체력이 회복되는 심각한 버그가 발생할 수 있었다. FMath::Max(1.0f, ...)를 사용하여 최소 1의 데미지는 항상 받도록 수정하여 해결했다. 💡Rampage의 분노 시 방어력 증가를 처음에는 Defense *= 2.0;, 버프 종료 시 Defense /= 2.0;으로 구현할까 생각했다. 하지만 이 방식은 부동소수점 오류가 누적될 수 있고, 나중에 다른 종류의 방어력 버프가 추가될 경우 계산이 매우 복잡해진다. BeginPlay 시점에 OriginalDefense 변수에 원본 값을 저장해두고, 버프 시에는 이 원본 값을 기준으로 계산하는 것이 훨씬 안정적이고 확장성 있는 구조라는 것을 깨달았다. 👍| 개념 | 설명 | 비고 |
|---|---|---|
| 방어력 시스템 | 몬스터의 TakeDamage 함수에서 데미지 공식을 직접 구현. FMath::Max를 활용해 최소 데미지를 보장하는 것이 중요. | Damage = Attack - Defense |
| 클래스별 스탯 관리 | AIMonsterBase에 공통 변수(Defense)를 선언하고, 각 자식 클래스(AIZombie, Rampage)의 생성자에서 고유한 값을 설정. | 상속 구조의 장점 활용 |
| 안정적인 버프 구현 | 버프 적용 시 현재 값에 연산을 누적하기보다, 원본(Original) 값을 따로 저장해두고 그것을 기준으로 스탯을 변경하는 것이 안전. | Defense = OriginalDefense * 2.0 |
| 코드 리팩토링 | 주석 추가, UE_LOG 정리, 변수/함수 재배치를 통해 코드의 가독성과 유지보수성을 향상. | 미래의 나를 위한 투자! |