보스 페이즈 제어기: HP 백분율과 타이머로 몬스터 패턴 전환하기
보스의 HP 임계값과 페이즈 경과 시간을 함께 사용해 패턴을 안정적으로 전환하는 제어기 설계를 다룹니다. 우선순위, 전환 규칙, Unity C# 예제와 테스트 관점을 중심으로 설명합니다.

왜 HP 조건만으로는 부족한가
보스의 페이즈를 HP 백분율만으로 나누면 구현은 간단하다. 예를 들어 HP가 70% 이하일 때 2페이즈, 35% 이하일 때 3페이즈로 바꾸는 방식이다. 다만 실제 전투에서는 한 페이즈 안의 흐름이 단조로워지기 쉽다. 플레이어가 딜을 멈추면 보스는 같은 패턴을 오래 반복하고 반대로 강한 공격으로 임계값을 한 번에 넘기면 전환 연출과 중요한 패턴이 생략될 수 있다.
타이머 조건을 함께 두면 페이즈 진입 뒤 일정 시간이 지났을 때 강화 패턴을 추가하거나 전환 직후의 무적 연출이 끝난 뒤에만 공격을 시작하도록 제어할 수 있다. 핵심은 HP와 시간을 각각의 전환 조건으로 보되 어느 조건이 먼저 만족되었는지에 따라 상태가 흔들리지 않게 만드는 것이다.
설계 목표
페이즈 제어기는 다음 질문에 일관되게 답할 수 있어야 한다.
- 현재 HP 비율에서 진입 가능한 페이즈는 무엇인가?
- 현재 페이즈에 머문 시간이 충분한가?
- 전환 중에는 피격, 공격 선택, 추가 전환을 어떻게 막을 것인가?
- 한 프레임에 여러 임계값을 넘었을 때 어디로 전환할 것인가?
가장 관리하기 쉬운 구조는 BossPhaseController가 전환만 책임지고 각 페이즈의 공격 선택은 별도 패턴 선택기에 맡기는 방식이다. 제어기는 현재 페이즈와 진입 시각을 보관하며 패턴 선택기는 그 정보를 받아 가능한 공격을 고른다.
flowchart TD
A[매 프레임 상태 갱신] --> B{전환 연출 중인가?}
B -- 예 --> C[연출 종료 여부만 확인]
B -- 아니오 --> D[HP 비율 계산]
D --> E{다음 HP 임계값 통과?}
E -- 예 --> F[다음 페이즈 전환]
E -- 아니오 --> G{현재 페이즈 타이머 조건 충족?}
G -- 예 --> H[강화 패턴 또는 하위 상태 전환]
G -- 아니오 --> I[현재 패턴 선택 및 실행]
HP 비율은 현재 HP를 기준으로 계산한다
최대 HP가 변하지 않는 보스라면 HP 비율은 다음처럼 계산한다.
currentHp와 maxHp가 정수형이면 정수 나눗셈이 되지 않도록 반드시 실수형으로 변환해야 한다. Unity에서는 Mathf.Clamp01으로 예상 밖의 값을 방어하는 편이 안전하다.
private float GetHpRatio()
{
if (maxHp <= 0f)
return 0f;
return Mathf.Clamp01(currentHp / maxHp);
}
보호막이나 최대 HP 증가 효과가 있는 보스는 기준을 먼저 정해야 한다. 예를 들어 보호막을 별도 게이지로 취급할지, 전체 생존력에 포함할지에 따라 페이즈 전환 시점이 달라진다. 이런 규칙은 전투 기획과 코드 양쪽에서 같은 정의를 사용해야 한다.
전환 우선순위를 명확히 정한다
보통은 HP 기반의 페이즈 상승을 타이머 기반 강화보다 우선 처리한다. 큰 피해로 2페이즈와 3페이즈 임계값을 함께 넘은 경우에도 페이즈를 하나씩 순차 전환할지, 조건을 만족하는 가장 높은 페이즈로 즉시 이동할지 결정해야 한다.
순차 전환은 각 페이즈의 연출과 학습 기회를 보장한다. 반면 즉시 전환은 불필요한 연출 반복을 줄인다. 일반적인 보스전에서는 순차 전환이 플레이 경험을 예측하기 쉽다. 다만 전환 중에는 추가 HP 전환을 잠시 보류하고 연출이 끝난 뒤 다시 평가해야 한다.

다음 예제는 HP 조건으로 기본 페이즈를 올리고 현재 페이즈 체류 시간이 지나면 강화 상태를 활성화한다. PhaseDefinition은 인스펙터에서 조정할 수 있는 데이터다.
using System;
using UnityEngine;
[Serializable]
public class PhaseDefinition
{
[Range(0f, 1f)]
public float enterAtOrBelowHpRatio;
[Min(0f)]
public float enrageAfterSeconds;
}
public class BossPhaseController : MonoBehaviour
{
[SerializeField] private float maxHp = 1000f;
[SerializeField] private PhaseDefinition[] phases;
private float currentHp;
private int currentPhaseIndex;
private float phaseEnteredAt;
private bool isTransitioning;
private bool isEnraged;
public int CurrentPhaseIndex => currentPhaseIndex;
public bool IsEnraged => isEnraged;
private void Awake()
{
currentHp = maxHp;
currentPhaseIndex = 0;
phaseEnteredAt = Time.time;
}
private void Update()
{
if (isTransitioning)
return;
TryAdvancePhaseByHp();
TryEnableEnrageByTimer();
}
public void ApplyDamage(float damage)
{
if (isTransitioning || damage <= 0f)
return;
currentHp = Mathf.Max(0f, currentHp - damage);
TryAdvancePhaseByHp();
}
private void TryAdvancePhaseByHp()
{
if (currentPhaseIndex >= phases.Length - 1)
return;
float hpRatio = GetHpRatio();
int nextPhaseIndex = currentPhaseIndex + 1;
if (hpRatio <= phases[nextPhaseIndex].enterAtOrBelowHpRatio)
BeginPhaseTransition(nextPhaseIndex);
}
private void TryEnableEnrageByTimer()
{
if (isEnraged)
return;
float elapsed = Time.time - phaseEnteredAt;
float limit = phases[currentPhaseIndex].enrageAfterSeconds;
if (limit > 0f && elapsed >= limit)
EnableEnrage();
}
private void BeginPhaseTransition(int nextPhaseIndex)
{
isTransitioning = true;
isEnraged = false;
// 이동과 공격을 잠시 중단하고 전환 연출을 시작한다.
// 애니메이션 이벤트 또는 코루틴 완료 시 CompletePhaseTransition을 호출한다.
currentPhaseIndex = nextPhaseIndex;
}
public void CompletePhaseTransition()
{
phaseEnteredAt = Time.time;
isTransitioning = false;
}
private void EnableEnrage()
{
isEnraged = true;
// 패턴 선택기에 강화 플래그를 전달한다.
}
private float GetHpRatio()
{
return maxHp > 0f ? Mathf.Clamp01(currentHp / maxHp) : 0f;
}
}
패턴은 페이즈 정의와 분리한다
페이즈 제어기 안에서 직접 공격 애니메이션을 재생하면 조건이 늘어날수록 코드가 빠르게 복잡해진다. 대신 패턴 선택기는 현재 페이즈, 강화 여부, 최근 사용한 패턴, 플레이어 거리 같은 정보를 받아 후보를 좁히는 역할만 하게 만든다.
예를 들어 2페이즈에서는 돌진과 투사체를 사용하고 타이머 강화 이후에는 돌진 뒤 추가 폭발을 허용할 수 있다. 이때 강화는 새 페이즈가 아니라 패턴 풀을 바꾸는 보조 상태로 두면 HP 기반 페이즈 수를 불필요하게 늘리지 않아도 된다.
public bool CanUsePattern(AttackPattern pattern, BossPhaseController phase)
{
if (pattern.RequiredPhase > phase.CurrentPhaseIndex)
return false;
if (pattern.RequiresEnrage && !phase.IsEnraged)
return false;
return true;
}
패턴 실행 중에 HP 임계값을 넘겼을 때도 정책이 필요하다. 피하기 어려운 공격을 즉시 끊으면 플레이어에게 부당하게 느껴질 수 있다. 일반적으로는 현재 공격의 안전한 종료 지점까지 실행한 뒤 전환을 예약하거나 전환 시작 시 명확한 무적 시간과 시각 효과를 제공한다.
자주 발생하는 문제와 방어 방법
한 번의 피해로 여러 임계값을 넘는 경우
예제처럼 다음 페이즈만 확인하면 한 번의 갱신에 한 단계만 전환한다. 순차 전환이 의도라면 적절하다. 즉시 최종 페이즈로 보내고 싶다면 반복문으로 목표 페이즈를 계산하되 연출은 한 번만 실행하도록 분리해야 한다.
타이머가 전환 연출 시간까지 포함하는 경우
phaseEnteredAt을 전환 시작 시점이 아니라 CompletePhaseTransition에서 갱신해야 한다. 그래야 컷신이나 포효 애니메이션 시간이 강화 패턴 대기 시간에 섞이지 않는다.
페이즈를 되돌리는 경우
회복 기믹이 있어도 페이즈를 되돌릴지 신중하게 정해야 한다. 대부분의 액션 보스는 한 번 진입한 페이즈를 되돌리지 않는다. 전환 이력을 단조롭게 유지하면 패턴, 배경음, 연출 상태가 서로 어긋나는 문제를 줄일 수 있다.
프레임 단위로 중복 전환하는 경우
전환 시작 즉시 isTransitioning을 켜야 한다. 전환이 끝나기 전에는 공격 선택과 HP 기반 재평가를 막고 완료 시점에만 다시 갱신한다. 애니메이션 이벤트가 누락될 가능성이 있다면 일정 시간 뒤 복구하는 안전장치도 고려할 수 있다.
검증할 항목
수동 플레이만으로는 경계 조건을 놓치기 쉽다. 최소한 다음 시나리오는 단위 테스트 또는 플레이 모드 테스트로 확인하는 것이 좋다.
- HP가 정확히 70%, 35%일 때 전환되는가?
- 임계값보다 큰 피해를 한 번에 받아도 의도한 페이즈만 전환되는가?
- 전환 연출 중 피해를 받았을 때 중복 전환되지 않는가?
- 전환 완료 뒤 타이머가 0초부터 다시 계산되는가?
- 강화 상태에서 다음 HP 페이즈로 이동하면 강화 플래그가 초기화되는가?
- 마지막 페이즈에서는 더 이상 인덱스 범위를 벗어나지 않는가?
HP는 전투의 큰 흐름을 만들고 타이머는 그 흐름 안에 압박과 변주를 넣는다. 두 조건을 한 메서드에 뒤섞기보다 전환 우선순위와 상태 수명을 분명히 나누면 패턴을 추가해도 예측 가능한 보스 제어기를 유지할 수 있다.



