Unreal 개발 본 캠프 60일차

HappyCircle·2026년 2월 26일

Unreal 개발

목록 보기
77/163

📘 TIL — 특수 기믹 몬스터 로직 반영: Skeleton(광역 스킬) 및 Turtle(방어 기믹)


1. 개요: 몬스터 공통 구조와 특수 기믹의 분리

오늘은 몬스터 공통 구조 위에 특수 기믹을 유연하게 붙일 수 있도록 구조를 정리했습니다. 특히 Skeleton의 광역 스킬 공격과 TurtleHardenShell 방어 기믹을 반영하며, 본체 코드가 비대해지는 것을 막기 위해 역할을 분리했습니다.

  • APotatoMonster: 몬스터의 기본 생명주기(HP, UI, AI, 사망) 및 이벤트 허브 담당.
  • USpecialSkillComponent: 특수 스킬의 상태 머신(Telegraph -> Cast -> Execute) 및 실행 로직 담당.
  • UHardenShellComponent: 피격 시점의 데미지 감쇄 및 상태 전이 담당.

2. 문제 상황: 비대해지는 본체와 복잡해지는 트리거

2-1. 몬스터 본체 코드의 비대화 위험

기존 APotatoMonster는 체력 감소, 피격 리액션, UI 갱신 등 이미 많은 책임을 지고 있었습니다. 여기에 각 몬스터의 개별 기믹(광역기, 방어막, 분열 등)을 직접 구현하면 TakeDamage()나 공격 로직이 타입별 if-else 분기로 가득 차 유지보수가 불가능해질 위험이 있었습니다.

2-2. Skeleton: "사망 트리거"가 아닌 "시전형 광역 공격"

Skeleton의 핵심은 단순히 죽을 때 효과가 발생하는 것이 아니라, 특정 조건에서 시전 시간(Casting)을 거쳐 광역 피해(AoE)를 주는 것이었습니다. 즉, 일반 공격 로직이 아닌 별도의 SpecialSkill 파이프라인이 필요했습니다.

2-3. Turtle: 데미지 계산 파이프라인 개입

거북이 몬스터는 피격 시점에 데미지를 줄이는 기믹이 핵심입니다. 이는 단순한 수치 조정을 넘어, 피해 적용 전(감쇄)후(상태 갱신)에 정확히 개입해야 하는 구조적 요구사항이 있었습니다.


3. 해결 방안 및 구현 상세

3-1. 역할별 컴포넌트 분리와 데이터 기반 초기화

APotatoMonster는 각 기믹 컴포넌트를 보유하는 허브 역할을 수행하며, ApplyPresetsOnce()에서 FinalStats를 기반으로 필요한 컴포넌트를 활성화합니다.

// APotatoMonster.cpp - 데이터 기반 컴포넌트 초기화 예시
void APotatoMonster::ApplyPresetsOnce(const FPotatoMonsterStats& FinalStats)
{
    // 각 컴포넌트가 FinalStats를 참조하여 스스로를 설정
    if (CombatComp) CombatComp->InitFromStats(FinalStats);
    if (HardenShellComp) HardenShellComp->InitFromFinalStats(FinalStats);
    if (SpecialSkillComp) 
    {
        SpecialSkillComp->SkillPresetTable = SpecialSkillPresetTable;
        SpecialSkillComp->InitFromFinalStats(FinalStats);
    }
}

3-2. Skeleton: SpecialSkillComponent 상태 머신 활용

Skeleton의 광역 공격은 USpecialSkillComponent의 상태 머신 위에서 동작합니다. 단순히 데미지를 주는 것이 아니라 Telegraph(예고) -> Casting(시전) -> Execute(실행)의 단계를 거치며 안정성을 확보합니다.

// SpecialSkillComponent.cpp - 상태 전이 로직 (타이머 기반)
void USpecialSkillComponent::BeginTelegraph(const FSpecialSkillRow& Row)
{
    SetSkillState(ESkillState::Telegraphing);
    PlayPresentation_Telegraph(Row); // 이펙트/범위 표시
    
    // 타이머를 통해 다음 상태(Cast)로 전이
    GetWorld()->GetTimerManager().SetTimer(TelegraphTH, [this, Row]() {
        // 세션 유효성 검사 후 다음 단계 진행
        if (!IsSessionValid()) return;
        BeginCast(Row);
    }, Row.TelegraphDuration, false);
}

3-3. Turtle: TakeDamage 파이프라인 구축

Turtle의 방어 기믹을 위해 APotatoMonster::TakeDamage를 단순 HP 감소 함수가 아닌, 기믹이 개입할 수 있는 파이프라인으로 재설계했습니다.

// APotatoMonster.cpp - 확장된 피해 처리 파이프라인
float APotatoMonster::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, ...)
{
    float Incoming = DamageAmount;

    // 1. 방어형 기믹 개입 (피해 적용 전: 감쇄)
    if (HardenShellComp && HardenShellComp->IsActive()) {
        Incoming = HardenShellComp->ModifyIncomingDamage(Incoming);
    }

    // 2. 실제 피해 적용
    const float ActualApplied = ApplyHealthReduction(Incoming);

    // 3. 상태 갱신 기믹 (피해 적용 후: 스택 차감 등)
    if (HardenShellComp && HardenShellComp->IsActive()) {
        HardenShellComp->PostDamageCheck();
    }
    
    // 4. 후속 트리거 (분열, 피격 시 스킬 등)
    if (SplitComp) SplitComp->PostDamageCheck();
    TryStartSpecialSkill(ESkillTrigger::OnHit);

    return ActualApplied;
}

4. 핵심 정리 및 배운 점

4-1. 시전형 기믹과 연출의 분리

Skeleton 작업을 통해 Presentation(연출)Execution(실행)을 분리하는 것이 중요함을 깨달았습니다. 상태 머신과 타이머(SkillSessionId 체크 포함)를 활용하여 시전 중 취소되거나 대상이 사라지는 예외 상황에서도 안전한 로직을 구축했습니다.

4-2. 개입 위치(Hook)의 중요성

Turtle 기믹은 상태값 보유보다 "어느 시점에 코드가 끼어드는가"가 핵심이었습니다. 적용 전(ModifyIncomingDamage)과 적용 후(PostDamageCheck)로 지점을 나누니, 향후 흡수나 반사 같은 다른 방어 기믹도 쉽게 확장할 수 있게 되었습니다.

4-3. 몬스터 본체는 "조율자"

몬스터 본체는 직접적인 규칙을 가지기보다, 각 컴포넌트가 규칙을 수행할 수 있도록 이벤트를 전달하고 생명주기를 관리하는 중재자(Orchestrator) 역할로 남는 것이 유지보수에 유리함을 배웠습니다.


오늘의 정리

Skeleton은 스킬 상태 머신을 통한 시전형 광역 공격으로, Turtle은 피해 파이프라인에 개입하는 방어 컴포넌트로 구현하여 몬스터 기믹 확장의 유연한 기반을 마련했다.

profile
개발합시다!

0개의 댓글