몬스터 베이스 아키텍처 설계
목차
1. 시스템 요구 사항
몬스터는 전투 단위 개체로서 자신의 스탯과 상태를 독립적으로 소유해야 하며, 외부 입력(플레이어 공격 등)에 따라 체력이 감소하고 그 결과로 행동 상태가 전이되어야 한다.
이때 몬스터의 종류가 늘어나더라도 체력 감소 규칙과 사망 판정 기준은 일관되게 유지되어야 하며, 개별 몬스터는 AI 행동만 다르게 확장할 수 있어야 한다.
또한 피격 처리 로직은 전투 데이터 갱신과 상태 전이를 명확히 구분해야 하며, 사망 여부 판단은 단일 기준(curHp ≤ 0)으로 통일되어야 한다.
외부 시스템(퀘스트, 스폰 관리 등)과의 연동은 필요하지만, 몬스터가 보상 지급이나 퀘스트 조건 판단까지 직접 수행하지 않도록 책임을 분리해야 한다.
따라서 몬스터 베이스 클래스는 다음을 만족해야 한다.
체력 및 스탯을 소유하고, 데미지 입력을 단일 경로로 처리하며, 상태 전이 기준을 정의하고, 외부 시스템과 연결될 수 있는 확장 지점을 제공하되 실행 책임은 자식 클래스에 위임하는 구조여야 한다.
2. 설계 목표
- 모든 몬스터가 공유하는 상태(State) 체계 정의
- 체력 및 기본 스탯을 베이스 클래스가 소유
- 데미지 처리 진입점(GetDamage)을 단일화하여 규칙 일관성 확보
- '데이터 갱신 → 상태 전이' 순서를 강제하여 피드백 일관성 확보
- 실행 로직(AI, 애니메이션, 드랍)은 자식 클래스가 소비
3. 흐름도
[외부 입력: 플레이어 공격/스킬]
↓
Monster.GetDamage(dmg)
↓
curHp -= dmg (데이터 갱신)
↓
curHp > 0 ? state = DAMAGED
curHp <=0 ? state = DIE
↓
[자식 클래스 Update에서 state를 소비]
DAMAGED: 피격 연출/추적 재개
DIE: 사망 연출/드랍/파괴
이 구조의 핵심은 베이스 클래스가 행동을 직접 수행하지 않는다는 점이다.
Monster는 체력 데이터와 상태만 갱신한다.
실제 이동 중단, 애니메이션 재생, 드랍 생성, Destroy 호출은 자식 클래스에서 state 값을 소비하여 실행한다.
즉, Monster는 상태를 정의하고 만들며, 자식 클래스는 그 상태를 행동으로 해석한다.
이로써 데이터 계층과 실행 계층이 분리된다.
4. 구현
4.1. 공통 상태 정의(State)
public enum State
{
IDLE = 0,
MOVE,
CHASE,
ATTACK,
DAMAGED,
RETURN,
DIE
};
public State state;이 enum은 몬스터가 현재 어떤 행동 단계에 있는지에 대한 논리적 상태를 표현하는 타입다.
문자열 기반 상태 분기처럼 오타로 런타임 오류가 나지 않고, 값의 후보가 컴파일 타임에 고정되기 때문에 유지보수성이 올라간다.
또한 자식 클래스에서 switch(state) 기반 상태 머신을 구현할 때 케이스가 명확해져 디버깅이 쉬워지고, 상태가 추가·변경되면 컴파일 에러로 누락 지점을 빠르게 찾을 수 있다.
Unity에서 Animator 파라미터로도 상태를 표현할 수 있지만, 애니메이션은 표현 계층이고, AI 상태는 로직 계층이므로 둘을 동일시하면 결합도가 커진다.
여기서는 로직 상태를 enum으로 유지하고, 애니메이션은 그 상태를 소비해 트리거/Bool로 동기화하는 구조를 선택했다.
이 상태 정의가 베이스 클래스에 존재하여, 모든 몬스터가 동일한 상태 체계를 공유한다.
이로써 몬스터 종류가 늘어나도 상태 구조가 흔들리지 않고, 행동 로직만 자식 클래스에서 확장하면 된다.
4.2. 공통 스탯 및 변수
public string monsterName;
ublic bool isDie;
public bool isRaidMonster = false;
[Header("[ 몬스터 스테이터스 ]")]
public float maxHp;
public float curHp;
public float speed;
public float atk;
[Header("[ 몬스터 범위관련 ]")]
public float chaseDist; //플레이어 인지해서 추적 시작 범위
public float attackDist; //몬스터가 플레이어 공격 시작 범위
public float maxChaseDist; //몬스터 최대 이동 거리(이 거리 밖으로 나가면 원위치)Monster 클래스가 maxHp, curHp, speed, atk 같은 스탯을 직접 소유한다.
이 값들은 전투 중 변경되거나 참조되는 핵심 데이터이며, 외부 시스템이 아니라 몬스터 자신이 관리한다.
특히 curHp는 전투 중 지속적으로 변하는 값이다.
이 값이 어디에 저장되어 있는지가 불명확해지면 피격 처리, UI 갱신, 사망 전이에서 결합이 폭증한다.
isDie는 사망 확정 이후 불필요한 Update 로직 실행을 차단하기 위한 안전 장치다.
state가 DIE로 전이되었더라도, 별도의 불린 플래그를 두어 조기 return을 할 수 있게 설계한 것은 실행 안정성을 높이기 위한 선택이다.
isRaidMonster는 동일한 베이스 구조 위에서 레이드 전용 행동/규칙(타겟 변경, 추적거리 확장, 복귀 규칙 변경 등)을 분기할 수 있게 해주는 플래그로, 공통 규칙은 유지하되 특정 모드에 대해 조건부 확장을 가능하게 하였다.
4.3. 데미지 처리 진입점과 상태 전이
public void GetDamage(float f)
{
curHp -= f;
if (curHp > 0)
{
UpdateHpBar();
state = State.DAMAGED;
}
else if (curHp <= 0)
{
state = State.DIE;
if (hpBar) hpBar.GetComponent<MonsterHpBar>().UpdateHpBarUI(0);
}
}GetDamage 함수는 몬스터가 피해를 받는 유일한 진입점이다.
이 함수를 통해서만 curHp가 감소하게 만들어, 몬스터의 체력 변경 규칙이 한 곳에 고정되어 일관성을 확보할 수 있다.
여기서 핵심은 curHp를 먼저 갱신하고, 그 결과로 상태를 전이시키는 순서를 강제한 점이다.
전투 UX 관점에서 피격은 숫자 감소와 연출이 즉시 맞물려야 하므로, 데이터 갱신이 늦거나 상태 전이가 먼저 일어나면 맞았는데 체력이 안 줄어든 것처럼 보이는 프레임이 생길 수 있다.
또한 이 함수는 사망(curHp <= 0) 이후의 실행(드랍/Destroy)을 직접 수행하지 않고 state를 DIE로 바꾸는 것으로 끝나는데, 이는 베이스가 실행 계층까지 침범하지 않게 하기 위한 책임 분리다.
결과적으로 베이스는 전투 데이터와 상태만 만들고, 실제로 DIE 상태에서 무엇을 할지는 자식 클래스가 Update에서 소비하면서 결정하게 된다.
UpdateHpBar 호출이 포함된 것도 같은 맥락으로, 체력 값이 변했을 때 UI 갱신 트리거를 보장하되, UI 표현 방식 자체(fillAmount 등)은 MonsterHpBar가 소유하도록 경계를 유지한다.
마지막으로 hpBar의 체력바를 0으로 설정하는 것은, 체력바가 아예 빈 것처럼 보이게 하기 위함이다.
체력이 0이 되면 UpdateHpBar 함수가 호출되지 않기 때문에, 체력바를 직접 0으로 설정하여 체력이 빈것을 확인할 수 있게 하였다.
이렇게 하면 체력 감소 시 UI 갱신 누락이나 상태 전이 누락이 발생하지 않는다.
여기서 베이스 클래스는 Destroy나 드랍 처리를 직접 수행하지 않는다.
단지 state를 변경한다.
실행은 자식 클래스의 Update에서 state를 소비해 이루어진다.
이 분리 덕분에 전투 데이터와 실행 로직이 강하게 결합되지 않는다.
또한 체력 감소 직후 UpdateHpBar를 호출해 UI를 갱신하는데, 이 역시 데이터 변경 이후 표현 갱신이라는 순서를 유지하기 위함이다.
전투 피드백의 일관성을 보장하기 위한 설계다.
4.4. 외부 시스템(퀘스트) 연동 포인트
public void KillCount(int n)
{
for (int i = 0; i < RegisterQuest.instance.registerQuest.Count; i++)
{
if (RegisterQuest.instance.registerQuest[i].questID == n)
{
RegisterQuest.instance.MonsterKill(monsterName);
}
}
}
public void MonsterKill()
{
if (!isRaidMonster)
{
spawnMonster.instance.KillCount(monsterName);
}
}이 함수들은 몬스터가 사망했을 때 외부 시스템과 연결되는 지점이다.
KillCount 함수는 퀘스트 시스템에 몬스터의 사망 사실을 전달하고, MonsterKill 함수는 스폰 관리 시스템에 사망 사실을 전달한다.
여기서 중요한 점은, 몬스터가 퀘스트 조건 판단이나 스폰 규칙을 직접 처리하지 않는다는 것이다.
몬스터는 단지 죽음이 발생했다는 사실을 전달할 뿐이며, 실제 퀘스트 카운트 증가나 스폰 로직은 각각의 시스템이 담당한다.
이렇게 설계하면 전투 시스템과 퀘스트/스폰 시스템이 강하게 결합되지 않는다.
몬스터는 자신의 도메인(전투) 책임만 유지하며, 외부 시스템은 각자의 도메인 로직을 독립적으로 관리할 수 있다.
5. 개발 의도
이 베이스 아키텍처의 핵심은 몬스터를 행동 중심이 아니라 데이터와 상태 중심으로 먼저 안정화하는 데 있다.
Monster 클래스는 체력과 스탯을 소유하고, 데미지를 받으면 데이터를 갱신하고 상태를 전이시킨다.
그러나 실제 이동, 공격, 사망 연출과 같은 실행은 수행하지 않는다.
이 구조는 데이터 계층과 실행 계층을 분리하여 확장성을 확보하기 위한 설계다.
몬스터 종류가 늘어나도 체력 감소 규칙과 상태 체계는 유지되며, 자식 클래스는 상태를 해석하여 자신만의 AI 행동을 구현하면 된다.
또한 외부 시스템과의 연결 역시 사건 전달 수준으로 제한해 결합도를 낮추었다.
결과적으로 이 베이스 클래스는 이후 AI 상태 머신, 공격 판정 구조, 사망 처리 로직을 안정적으로 확장할 수 있는 기반 계층으로 기능한다.
