Unity 몬스터의 대기·추적·공격·복귀 행동을 어떻게 나누고 전환할까요? 상태 다이어그램과 C# 예제로 상태별 행동, 전환 조건, 상태에 들어갈 때의 처리를 살펴봅니다. Character와 Controller를 분리하고 UI 상속 구조를 구성해 게임 프레임워크로 확장하는 과정도 다룹니다.
2022년 1학기 한국애니메이션고등학교 2학년 「3D 게임 프로그래밍」 3강, 4강 「게임 프레임워크 개발」, 5강 「게임 프레임워크 개발」에서 가르친 내용입니다.
실습: 선공 몬스터의 상태 머신

유한 상태 기계(Finite State Machine)는 사물이 가질 수 있는 상태를 정해 두고, 한 번에 상태 하나에 머물며, 조건이 맞으면 다른 상태로 옮겨 가게 하는 설계입니다.
- IDLE — 제자리에 멈춰 적을 찾습니다. 적을 찾으면 추적합니다. 15초 동안 못 찾으면 다른 위치로 이동합니다.
- Moving — 다음 위치로 이동합니다. 이동 중에 적을 찾으면 추적합니다. 도착하면 IDLE 로 돌아갑니다.
- Following — 적을 쫓습니다. 적이나 스폰 위치와 10m 넘게 멀어지면 스폰 위치로 돌아가기 시작합니다. 사정거리 안까지 쫓아가면 공격합니다.
- Attacking — 계속 공격합니다. 적이 사정거리 밖으로 나가면 다시 쫓습니다. 적이 죽으면 스폰 위치로 돌아가고, 몬스터가 죽으면 상태 기계를 끝냅니다.
- Returning — 스폰 위치로 돌아갑니다. 도착하면 IDLE 로 돌아갑니다. 돌아가는 동안에는 피해를 받지 않습니다.
| 모양 | 뜻 |
|---|---|
| 둥근 사각형 | 상태 |
| 화살표와 글자 | 전환과 그 조건 (적 발견, 15초 지남 …) |
| entry / … | 그 상태에 들어갈 때 한 번 하는 일 — Returning 에서는 스폰 위치를 바라보게 회전 |
| do / … | 그 상태에 있는 동안 계속 하는 일 — 스폰 위치로 이동 |
| 검은 점 · 겹친 원 | 시작과 끝 |
Returning 의 "돌아가는 동안 피해를 받지 않는다" 는 규칙은 게임 디자인에서 중요한 장치입니다. 이 규칙이 없으면 플레이어가 몬스터를 스폰 위치에서 멀리 끌고 가 돌아가는 등 뒤를 공짜로 때릴 수 있습니다. 상태 다이어그램에 규칙을 적어 두면 이런 구멍을 설계 단계에서 찾습니다.
enum MonsterState { Idle, Moving, Following, Attacking, Returning }
MonsterState state = MonsterState.Idle;
float idleTime;
void Update()
{
switch (state)
{
case MonsterState.Idle:
idleTime += Time.deltaTime;
if (FindEnemy()) Change(MonsterState.Following);
else if (idleTime > 15f) Change(MonsterState.Moving);
break;
case MonsterState.Moving:
if (FindEnemy()) Change(MonsterState.Following);
else if (MoveToNextPoint()) Change(MonsterState.Idle); // 도착
break;
case MonsterState.Following:
if (Distance(target) > 10f || Distance(spawnPoint) > 10f) Change(MonsterState.Returning);
else if (InAttackRange(target)) Change(MonsterState.Attacking);
else Chase(target);
break;
case MonsterState.Attacking:
if (target.IsDead) Change(MonsterState.Returning);
else if (!InAttackRange(target)) Change(MonsterState.Following);
else Attack(target);
break;
case MonsterState.Returning:
if (MoveTo(spawnPoint)) Change(MonsterState.Idle); // 도착
break;
}
}
void Change(MonsterState next)
{
state = next;
idleTime = 0f;
invincible = next == MonsterState.Returning; // 돌아가는 동안 무적
if (next == MonsterState.Returning) LookAt(spawnPoint); // entry: 스폰 위치를 바라봄
}상태가 바뀌는 곳을 Change() 에 모았습니다. entry 에 해당하는 일(바라보기, 무적 켜기)을 여기에 두면, 상태가 늘어나도 들어갈 때의 일을 빠뜨리지 않습니다.
게임 프레임워크의 세 갈래
User Interaction
UI 는 Windows 같은 GUI 운영체제와 닮았습니다. 창 시스템을 설계한다고 생각하고 효율적이고 빠짐없게 설계합니다.
Content Objects
게임은 사람들이 만지고 싸우고 경험치를 얻는 콘텐츠 사물로 이루어집니다. MMORPG 라면 어떤 사물이 필요할지 먼저 적습니다.
Artificial Intelligence
어떤 사물은 스스로 판단합니다. 상태 머신으로 생애를 관리하는 클래스가 많으므로, 맨 위부터 아래까지 효율적인 상속 나무가 필요합니다.
콘텐츠 사물 설계
Character 는 능력치와 모양을 가진 몸이고, Controller 는 그 몸을 움직이는 머리입니다. 같은 Character 에 MonsterController 를 붙이면 AI 가 움직이고, PlayerController 를 붙이면 사람이 움직입니다. 1편의 로봇에서 "부위를 나눈다" 고 했던 생각이 "몸과 머리를 나눈다" 로 자랐습니다.
캐릭터는 프리팹으로 만들고, 동작은 Animator Controller 로 상태를 나눠 둡니다. 애니메이션에도 상태 머신이 쓰입니다.
- 마우스로 클릭한 지점으로 캐릭터를 이동시킵니다.
- 메인 카메라는 언제나 캐릭터에서 (0, 8.5, -3.5) 만큼 떨어져 따라다닙니다. 캐릭터가 생길 때와 캐릭터의 Update 마다 맞춥니다.
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
int layer_mask = LayerMask.GetMask("Terrain");
if (Physics.Raycast(ray, out hit, 1000, layer_mask))
{
hit.point; // 피킹된 3D 위치
}화면의 클릭 위치에서 카메라 방향으로 광선(Ray)을 쏘아, 지형(Terrain) 층에 처음 닿는 점을 찾습니다. 그 점이 캐릭터가 갈 곳입니다. 다음 실습은 스페이스바를 누르면 공격 동작이 나오게 하는 것이었습니다.
- 몬스터가 IDLE 상태에서 캐릭터를 찾습니다.
- 찾으면 Following 상태로 바꿉니다.
- Following 중에는 캐릭터를 계속 쫓고, 거리가 7 넘게 벌어지면 마지막으로 쫓던 위치로 돌아옵니다.
UI 클래스 설계
모든 UI 는 UIBase 를 물려받습니다. 화면 전체를 덮는 UIWindow 와 위에 뜨는 UIDialogue 로 나뉘고, 대화창은 다시 메시지 상자 → 확인 상자 → 아이템 확인 상자로 구체화됩니다. 목록 패널(UIListPanel)은 항목(UIElement)을 여러 개 품습니다(꽉 찬 마름모, 0..*).
아이템을 버릴지 묻는 창을 새로 만들 때, 부모가 이미 "메시지를 띄우고 확인 · 취소를 받는" 일을 하므로 아이템 그림을 띄우는 부분만 더하면 됩니다. 상속 나무를 잘 세우면 새 창을 만드는 비용이 점점 줄어듭니다.
프레임워크 위에 기능 얹기
5강에서는 만든 프레임워크 위에 게임 기능을 하나씩 얹었습니다.
| 실습 | 하는 일 | 핵심 |
|---|---|---|
| 토스트 메시지 | 애니메이션과 함께 뜨는 짧은 알림 프리팹을 만들어 Resources 에 두고, Create 로 화면에 띄운 뒤 3초 뒤에 지웁니다 | Instantiate(Resources.Load(…)), 부모는 캔버스, Destroy(gameObject, LifeTime) |
| 몬스터 스포너 | 시작할 때 최대 수만큼 몬스터를 만들고, 마지막 사망 시각에서 정한 시간이 지나면 다시 채웁니다 | Character.Create(id), Random.Range() 로 위치 고르기 |
| 적 탐색 | 플레이어를 찾던 SearchEnemies() 를 몬스터도 찾을 수 있게 고칩니다 | 제네릭 where T : Controller |
| 그 밖에 | 플레이어 조작 UI, 피해 숫자 효과, 인벤토리와 아이템, 몬스터의 공격과 사망 |
// 플레이어 전용이던 탐색을 컨트롤러 종류로 일반화한다
public List<T> SearchEnemies<T>(float radius) where T : Controller
{
List<T> found = new List<T>();
foreach (T c in FindObjectsOfType<T>())
{
if (Vector3.Distance(c.transform.position, transform.position) <= radius)
found.Add(c);
}
return found;
}
// 쓰는 쪽
var players = SearchEnemies<PlayerController>(10f);
var monsters = SearchEnemies<MonsterController>(10f);where T : Controller 는 "T 는 Controller 의 자식이어야 한다" 는 약속입니다. 상속 나무를 잘 세워 두었기 때문에 탐색 함수 하나로 플레이어와 몬스터를 모두 찾습니다.
지금 100턴에서는
「졸업까지 100턴」의 캐릭터도 상태 머신을 씁니다. 다만 상태마다 무엇을 할지를 Strategy 객체로 빼서, 보드의 말과 산책 NPC 와 미니게임 캐릭터가 같은 캐릭터 클래스를 씁니다. 수업의 Character + Controller 가 실제 서비스에서 어떻게 자랐는지는 구조 읽기 4편에 정리했습니다.
정리
- 상태는 하나씩, 전환은 조건으로. 상태와 조건을 표로 먼저 적고 다이어그램과 코드로 옮깁니다.
- 몸(Character)과 머리(Controller)를 나눕니다. 같은 몸을 AI 도, 사람도 움직입니다.
- 상속 나무를 잘 세우면 새 기능이 싸집니다. UI 창도, 적 탐색도 부모가 일을 덜어 줍니다.
다음 편에서는 캐릭터를 움직이고 시야를 계산하는 게임 수학, 벡터와 행렬을 다룹니다.
함께 읽으면 좋은 글
AI가 만든 Unity 코드를 직접 검증해 보고 싶다면
이 글에서 살펴본 구조를 바탕으로 AI에게 작업을 요청하고 결과를 읽는 연습을 이어가세요. 「AI 코드를 읽는 게임 개발자」 무료 세션 1에서 시작하거나, 전체 커리큘럼과 수강 안내를 확인할 수 있습니다.