데미지 시스템 1 - 기본 설계
이전 프로젝트의 데미지 시스템과 비교하기
개요
- 이번 프로젝트에서 사용할 데미지 시스템에 대해 설명한다.
- 데미지 시스템에 사용되는 스탯 구조에 대해서도 설명한다.
이전 프로젝트의 데미지 시스템
기본 흐름
위의 이미지는 이전 프로젝트인 Explosion의 데미지 시스템 작동 방식을 나타낸 것이다.
- 폭탄은 일정 시간이 지나면 폭발하면서 범위 내의 액터에게 데미지를 가한다. 이 때,
GameplayStatics::ApplyRadialDamage
를 사용한다. - 캐릭터가 폭발 범위 내에 있다면
AActor::TakeDamage
를 통해 데미지 정보를 전달받는다. 그리고 즉시 스탯 컴포넌트에 이 정보를 전달한다. - 스탯 컴포넌트에는 사망 및 체력 변경 관련 대리자가 존재한다. 해당 대리자에는 Character, GameMode 클래스에 정의된 함수들을 바인딩 해두었다. 스탯 컴포넌트의 스탯 판단 결과에 따라 해당 대리자가 호출된다.
- 대리자에 바인딩된 함수들이 실행되는 것으로 데미지 전달 및 후속처리가 완료된다.
스탯 컴포넌트 대리자 연결 방식
// EPCharacterStatComponent.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHpChangedDelegate, float /*CurrentHp*/, float /*MaxHp*/);
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnHpZeroThreeParamsDelegate, AController* /*KillerPlayer*/, AController* /*KilledPlayer*/, AActor* /*DamageCauser*/);
DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
스탯 컴포넌트에 대리자를 선언해두고 관련 함수들을 연결하는 방식은 굉장히 직관적이다. 체력 스탯 소유자나 사망 주체가 캐릭터인 만큼, 캐릭터 클래스에서 많은 연결이 이루어지기 때문이다.
- 캐릭터 → 스탯 컴포넌트 소유. 사망 처리 함수 바로 바인딩.
- 게임 모드 → 캐릭터에서 호출 가능. 사망 처리 함수 바인딩.
- 위젯 & HUD → HUD를 플레이어 컨트롤러가 아닌 캐릭터에 부착. 스탯 표시 변경 함수 바인딩.
멀티 플레이 환경에서는 캐릭터와 플레이어 컨트롤러를 중심으로 서버 통신이 이루어졌기에, 이러한 구조가 도움이 되었다.
이번 프로젝트의 데미지 시스템
글로벌 이벤트 시스템
이번 프로젝트는 이전 프로젝트와 장르가 다른 만큼, 데미지 시스템에도 변화를 주었다. 단순히 캐릭터와 게임모드에만 연결되는 것을 넘어, 몬스터의 사망 판정이나 퀘스트 시스템과의 연계 등 확장성을 중점적으로 고려했다.
글로벌 이벤트 시스템은 캐릭터가 아닌 GameInstanceSubsystem
을 중심으로 대리자를 연결하는 방식이다. 일종의 인터페이스로써, 이벤트 발생자와 수신자가 서로를 알 필요가 없으며 새로운 수신자를 추가하기 쉽다는 장점이 있다. 싱글 플레이라서 플레이어 이벤트가 겹칠 일도 없다! 싱글 플레이 만세!
- IVDeathEventSubsystem에는 캐릭터 사망, 부활, 몬스터 사망 정보를 전달하는 대리자가 선언되어있다.
- CharacterStatComponent는 해당 이벤트 발생 시 Broadcast를 통해 대리자를 호출한다.
- GameMode, PlayerController, Character, HUD 등은 캐릭터나 몬스터에 대해 알 것 없이 이벤트를 수신받아 처리하면 된다.
관련 컴포넌트
컴포넌트 또한 기능 분리와 확장성을 중점적으로 고려했다. 이전 프로젝트에서 캐릭터나 일부 컴포넌트가 담당했던 기능들을 4가지 컴포넌트로 분리했다. 컴포넌트간 연결을 피하기 위해 필요한 정보 교환은 관련 인터페이스들을 구현한 캐릭터를 통해 진행했다.
- CharacterStatComponent → 캐릭터의 상태와 스탯을 관리한다.
- AttackComponent → 공격과 관련된 몽타주 재생, 콤보 관리 등을 수행한다.
- HitReactionComponent → 피격 및 사망 리액션을 관리한다.
- EquipComponent → 장착중인 장비를 관리한다.
각 컴포넌트에 대한 세부 내용은 다음 포스팅에서 설명하겠다.
스탯 구조
기본 스탯 & 데미지 스탯
본 게임에서 사용되는 스탯은 크게 기본 스탯과 데미지 스탯으로 구분되며, 구조체 형태로 저장된다.
// IVGenericStructs.h
// 캐릭터, 아이템이 공통으로 사용하는 기본 스텟 구조체
USTRUCT(BlueprintType)
struct FBaseStat
{
GENERATED_USTRUCT_BODY()
/* 체력*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxHP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float CurrentHP;
/* 스테미나 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float CurrentStamina;
FBaseStat()
{
MaxHP = 100.0f;
CurrentHP = 100.0f;
MaxStamina = 100.0f;
CurrentStamina = 100.0f;
}
FBaseStat(float InMaxHP, float InCurrentHP, float InMaxStamina, float InCurrentStamina)
{
MaxHP = InMaxHP;
CurrentHP = InCurrentHP;
MaxStamina = InMaxStamina;
CurrentStamina = InCurrentStamina;
}
/* 연산자 오버로딩 */
FBaseStat operator+(const FBaseStat& Other) const
{
FBaseStat Result;
Result.MaxHP = MaxHP + Other.MaxHP;
Result.CurrentHP = CurrentHP + Other.CurrentHP;
Result.MaxStamina = MaxStamina + Other.MaxStamina;
Result.CurrentStamina = CurrentStamina + Other.CurrentStamina;
return Result;
}
FBaseStat operator-(const FBaseStat& Other) const
{
FBaseStat Result;
Result.MaxHP = MaxHP - Other.MaxHP;
Result.CurrentHP = CurrentHP - Other.CurrentHP;
Result.MaxStamina = MaxStamina - Other.MaxStamina;
Result.CurrentStamina = CurrentStamina - Other.CurrentStamina;
return Result;
}
};
기본 스탯은 캐릭터, 몬스터, 그리고 아이템 등이 공통으로 가지는 고유의 스탯이다. 현재는 체력과 스테미나만을 사용한다. 스탯 연산 시 연산자 오버로딩을 통하여 구조체 단위로 연산을 진행한다.
// IVGenericStructs.h
/* 해당 액터가 가진 기본 공격력 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float BaseDamage;
/* 특수 효과로 인해 일시적으로 증가한 추가 공격력 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float AdditionalDamage;
/* 방어력. 피격 시 데미지를 경감 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float DamageReduction;
데미지 스텟은 기본 스텟 이외에 별도로 데미지 관련 정보가 필요한 캐릭터, 무기, 방어구 등이 가지는 스탯이다. 기본 데미지, 일시적인 추가 데미지, 방어력을 멤버로 가진다.
캐릭터 상태
// IVCharacterStateEnums.h
// 캐릭터 이동 상태. 모션 매칭의 Chooser를 위해 사용된다.
UENUM(BlueprintType)
enum class EMovementState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Move UMETA(DisplayName = "Move"),
};
// Gait는 걸음걸이를 의미함
UENUM(BlueprintType)
enum class EGaitState : uint8
{
Walk UMETA(DisplayName = "Walk"),
Run UMETA(DisplayName = "Run")
};
// 캐릭터 점프 상태
UENUM(BlueprintType)
enum class EJumpState : uint8
{
InAir UMETA(DisplayName = "In Air"),
OnGround UMETA(DisplayName = "On Ground")
};
// 캐릭터 타겟팅 상태
UENUM(BlueprintType)
enum class ETargetingState : uint8
{
OnTargeting UMETA(DisplayName = "On Targeting"),
NonTargeting UMETA(DisplayName = "Non-Targeting")
};
// 특수 움직임 상태
UENUM(BlueprintType)
enum class ESpecialMovementState : uint8
{
None UMETA(DisplayName = "None"),
Rolling UMETA(DisplayName = "Rolling"),
Dodging UMETA(DisplayName = "Dodging"),
Attacking UMETA(DisplayName = "Attacking"), // 공격 중
HitStunned UMETA(DisplayName = "Hit Stunned") // 피격 중
};
// 캐릭터 생존 상태
UENUM(BlueprintType)
enum class ELifeState : uint8
{
Alive UMETA(DisplayName = "Alive"),
Dead UMETA(DisplayName = "Dead")
};
스탯 컴포넌트에서는 기본 스탯, 데미지 스탯 뿐만 아니라 캐릭터의 상태 정보 또한 관리한다. 이러한 정보는 공격, 회피와 같은 상태 전환 가능 여부 판별, 모션 매칭 데이터 테이블 선택 등에 사용된다.
스탯 컴포넌트
스탯 컴포넌트는 아래와 같은 작업을 담당한다.
- 사망 여부 판별
- 사망 및 부활 이벤트 제어 (글로벌 이벤트 서브시스템 사용)
- 매 틱마다 캐릭터 움직임 정보 갱신
- 기본 스탯 및 데미지 스탯 연산
// IIVCharacterComponentProvider.h
public:
virtual UCharacterMovementComponent* GetCharacterMovementComponent() const = 0;
virtual UCharacterTrajectoryComponent* GetCharacterTrajectoryComponent() const = 0;
virtual UIVCharacterStatComponent* GetCharacterStatComponent() const = 0;
컴포넌트 특성상 다른 컴포넌트에서 상태 정보를 자주 요구하므로, CharacterComponentProvider
인터페이스를 통해 오너의 스탯 컴포넌트에 접근할 수 있도록 한다.