Unity
Unity AI 아키텍처: 유한 상태 머신(FSM) 패턴을 이용한 몬스터 AI
유니얼
2024. 4. 23. 22:42
728x90
FSM 패턴이란?
유한 상태 머신(FSM)은 시스템이 가질 수 있는 일련의 상태들과 그 상태들 간의 전환을 정의하는 아키텍처 패턴입니다. AI 개발에서 FSM을 사용하면 NPC(Non-Player Character)나 적 AI의 다양한 행동 상태(예: 순찰, 추적, 공격)를 체계적으로 관리할 수 있습니다. 각 상태는 특정 행동을 실행하고, 주어진 입력에 따라 다른 상태로 전환될 수 있습니다.

1. 상태(State) 인터페이스 정의하기
AI의 각 행동 상태를 나타내는 인터페이스를 정의합니다. 이 인터페이스는 상태가 시작될 때, 실행될 때, 종료될 때 호출되는 메서드들을 포함합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// IState 인터페이스를 정의합니다.
public interface IState
{
// EnterState 메서드는 상태가 시작될 때 호출됩니다.
// 이 메서드는 상태로 전환될 때 초기 설정을 수행하는 데 사용됩니다.
public void EnterState();
// UpdateState 메서드는 매 프레임마다 호출됩니다.
// 이 메서드는 상태가 활성 상태일 때 실행되는 로직을 담당합니다.
public void UpdateState();
// ExitState 메서드는 상태가 종료될 때 호출됩니다.
// 이 메서드는 상태를 빠져나올 때 필요한 정리 작업을 수행하는 데 사용됩니다.
public void ExitState();
}
2. 구체적인 상태 클래스 구현하기
위 인터페이스를 구현하는 구체적인 상태 클래스를 만듭니다. 예를 들어, AI 캐릭터가 순찰, 추적, 공격하는 상태를 다음과 같이 구현할 수 있습니다.
1, PatrolState
using UnityEngine;
using UnityEngine.AI;
// PatrolState 클래스는 MonoBehaviour와 IState 인터페이스를 상속받아 순찰 상태를 구현합니다.
public class PatrolState : MonoBehaviour, IState
{
[Header("Enemy Guarding Var")]
public GameObject[] walkPoints; // 순찰할 위치들을 담은 배열
int currentEnemyPosition = 0; // 현재 순찰 위치의 인덱스
float walkingPointRadius = 2; // 순찰 지점 도달 판정에 사용되는 반경
private Animator animator; // 애니메이션 컴포넌트
private NavMeshAgent EnemyAgent; // 적 AI의 네비게이션을 위한 컴포넌트
private Enemy _enemy; // 적 AI 스크립트
// 상태 진입 시 호출되는 메서드입니다.
public void EnterState()
{
// 필요한 컴포넌트를 캐싱합니다.
if (!animator) animator = GetComponent<Animator>();
if (!EnemyAgent) EnemyAgent = GetComponent<NavMeshAgent>();
if (!_enemy) _enemy = GetComponent<Enemy>();
// 순찰 상태의 애니메이션 설정
animator.SetBool("Walk", true);
animator.SetBool("AimRun", false);
animator.SetBool("Shoot", false);
animator.SetBool("Die", false);
}
// 매 프레임마다 호출되는 메서드입니다.
public void UpdateState()
{
// 현재 위치와 다음 순찰 지점 간의 거리를 확인
if (Vector3.Distance(walkPoints[currentEnemyPosition].transform.position, transform.position) < walkingPointRadius)
{
// 다음 순찰 지점을 무작위로 선택
currentEnemyPosition = UnityEngine.Random.Range(0, walkPoints.Length);
// 배열의 범위를 넘어선 경우 인덱스를 초기화
if (currentEnemyPosition >= walkPoints.Length)
{
currentEnemyPosition = 0;
}
}
// 선택된 지점으로 이동 명령을 내림
EnemyAgent.SetDestination(walkPoints[currentEnemyPosition].transform.position);
}
// 상태에서 벗어날 때 호출되는 메서드입니다.
public void ExitState()
{
// 순찰 상태 종료 시 애니메이션 상태를 초기화합니다.
animator.SetBool("Walk", false);
animator.SetBool("AimRun", false);
animator.SetBool("Shoot", false);
animator.SetBool("Die", false);
}
}
2, ChaseState
using UnityEngine;
using UnityEngine.AI;
// ChaseState 클래스는 MonoBehaviour와 IState 인터페이스를 상속받습니다.
public class ChaseState : MonoBehaviour, IState
{
private Animator animator; // 캐릭터의 애니메이션을 관리합니다.
private NavMeshAgent EnemyAgent; // NavMeshAgent를 사용하여 경로 찾기와 이동을 관리합니다.
private Transform target; // 추적 대상의 위치 정보입니다.
private Enemy _enemy; // Enemy 스크립트를 참조합니다.
// 상태로 진입할 때 호출되는 메서드입니다.
public void EnterState()
{
// 필요한 컴포넌트가 없을 경우, 해당 컴포넌트를 GameObject에서 가져옵니다.
if (!animator) animator = GetComponent<Animator>();
if (!EnemyAgent) EnemyAgent = GetComponent<NavMeshAgent>();
if (!_enemy) _enemy = GetComponent<Enemy>();
if (!target) target = _enemy.playerBody;
// 추적 상태의 애니메이션 설정
animator.SetBool("Walk", false);
animator.SetBool("AimRun", true);
animator.SetBool("Shoot", false);
animator.SetBool("Die", false);
}
// 매 프레임마다 호출되는 메서드입니다.
public void UpdateState()
{
// NavMeshAgent를 사용하여 타겟 위치로 이동을 시도합니다.
EnemyAgent.SetDestination(target.position);
}
// 상태에서 벗어날 때 호출되는 메서드입니다.
public void ExitState()
{
// 추적 상태 종료 시 애니메이션 상태를 초기화합니다.
animator.SetBool("Walk", false);
animator.SetBool("AimRun", false);
animator.SetBool("Shoot", false);
animator.SetBool("Die", false);
}
}
3, ShootState
using UnityEngine;
using UnityEngine.AI;
// ShootState 클래스는 MonoBehaviour와 IState 인터페이스를 상속받아 공격 상태를 구현합니다.
public class ShootState : MonoBehaviour, IState
{
public Transform LookPoint; // 적 AI가 목표로 하는 지점
public ParticleSystem muzzleSpark; // 총구에서 발생하는 스파크 이펙트
[Header("Sound And UI")]
public AudioClip shootingSound; // 총소리 클립
public AudioSource audioSource; // 오디오 소스 컴포넌트
[Header("Enemy Shooting var")]
public float timebtwShoot; // 연속 사격 사이의 간격
public Camera ShootingRaycastArea; // 사격을 수행할 카메라 위치
public float Damage = 5; // 총알 한 발당 입힐 수 있는 피해량
public float shootingRaidus; // 사격이 가능한 범위
bool previouslyShoot; // 이전에 총을 쏜 적이 있는지 여부
private Animator animator; // 애니메이션 컴포넌트
private NavMeshAgent EnemyAgent; // 적 AI의 네비게이션을 위한 컴포넌트
private Enemy _enemy; // 적 AI 스크립트
// 상태 진입 시 호출되는 메서드입니다.
public void EnterState()
{
if (!animator) animator = GetComponent<Animator>();
if (!EnemyAgent) EnemyAgent = GetComponent<NavMeshAgent>();
if (!audioSource) audioSource = GetComponent<AudioSource>();
if (!_enemy) _enemy = GetComponent<Enemy>();
// 사격 상태의 애니메이션 설정
animator.SetBool("Walk", false);
animator.SetBool("AimRun", false);
animator.SetBool("Shoot", true);
animator.SetBool("Die", false);
}
// 매 프레임마다 호출되는 메서드입니다.
public void UpdateState()
{
// 적이 현재 위치에 고정됩니다.
EnemyAgent.SetDestination(transform.position);
// 적이 LookPoint를 바라보도록 합니다.
transform.LookAt(LookPoint);
// 사격을 처음 할 때에만 수행합니다.
if (!previouslyShoot)
{
// 총구 이펙트를 재생합니다.
muzzleSpark.Play();
// 총소리를 재생합니다.
audioSource.PlayOneShot(shootingSound);
RaycastHit hit;
// 사격이 성공했는지를 판별합니다.
if (Physics.Raycast(ShootingRaycastArea.transform.position, ShootingRaycastArea.transform.forward, out hit, shootingRaidus))
{
Debug.Log("Shooting " + hit.transform.name);
// 적중한 대상이 PlayerController 컴포넌트를 가지고 있으면 피해를 입힙니다.
PlayerController player = hit.transform.GetComponent<PlayerController>();
if (player)
{
player.HitDamage(Damage);
}
}
// 연속 사격을 막기 위해 플래그를 설정합니다.
previouslyShoot = true;
Invoke(nameof(ActiveShooting), timebtwShoot);
}
}
// 사격 가능 상태로 전환하는 메서드입니다.
private void ActiveShooting()
{
previouslyShoot = false;
}
// 상태에서 벗어날 때 호출되는 메서드입니다.
public void ExitState()
{
// 사격 상태 종료 시 애니메이션 상태를 초기화합니다.
animator.SetBool("Walk", false);
animator.SetBool("AimRun", false);
animator.SetBool("Shoot", false);
animator.SetBool("Die", false);
}
}
3. 상태 머신(State Machine) 클래스 구현하기
상태 전환 로직을 중앙에서 관리하여, AI가 다양한 상태(예: 순찰, 추적, 사격) 간에 전환할 수 있도록 지원합니다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// EState 열거형은 적 AI가 가질 수 있는 가능한 상태들을 정의합니다.
enum EState
{
Patrol, // 순찰 상태
Chase, // 추적 상태
Shoot, // 사격 상태
}
// EnemyStateContext 클래스는 적 AI의 상태 관리를 담당합니다.
public class EnemyStateContext
{
// CurrentState 프로퍼티는 현재 적 AI가 가지고 있는 상태를 저장합니다.
public IState CurrentState { get; set; }
// _controller 필드는 적 AI의 주요 제어 로직을 담은 Enemy 클래스의 인스턴스를 참조합니다.
private readonly Enemy _controller;
// 생성자에서는 Enemy 클래스의 인스턴스를 받아 _controller 필드에 할당합니다.
public EnemyStateContext(Enemy controller)
{
_controller = controller;
}
// Transition 메서드는 적 AI가 다른 상태로 전환할 때 호출됩니다.
public void Transition(IState state)
{
// 현재 상태가 null이 아니면, 현재 상태의 ExitState 메서드를 호출하여 상태 종료 로직을 수행합니다.
if(CurrentState != null)
CurrentState.ExitState();
// 새로운 상태를 CurrentState 프로퍼티에 할당합니다.
CurrentState = state;
// 새로운 상태의 EnterState 메서드를 호출하여 상태 시작 로직을 수행합니다.
CurrentState.EnterState();
}
}
4. 상태 전환 로직 추가하기
AI의 행동에 따라 적절한 상태로 전환하기 위한 조건을 체크하는 로직을 추가합니다. 매 프레임마다 플레이어와의 거리에 따라 순찰, 추적, 사격 중 하나의 상태로 전환합니다.
public class Enemy : MonoBehaviour
{
[Header("Enemy States")]
// 각 상태에 해당하는 스크립트 컴포넌트를 SerializeField 어트리뷰트를 사용하여 인스펙터에서 할당받을 수 있도록 설정합니다.
[SerializeField] private PatrolState patrolState;
[SerializeField] private ChaseState chaseState;
[SerializeField] private ShootState shootState;
// 상태 머신 관리를 위한 컨텍스트 객체
private EnemyStateContext enemyStateContext;
// Unity의 Awake 생명주기 이벤트 메서드에서 초기 설정을 수행합니다.
private void Awake()
{
// EnemyStateContext 인스턴스를 생성하고, 이 객체에 이 클래스의 인스턴스를 전달합니다.
enemyStateContext = new EnemyStateContext(this);
// 적 AI의 초기 상태를 순찰 상태로 설정합니다.
enemyStateContext.Transition(patrolState);
}
// Unity의 Update 생명주기 이벤트 메서드에서 매 프레임마다 호출됩니다.
private void Update()
{
// 적이 플레이어를 감지하는지 여부를 검사합니다. Physics.CheckSphere 함수를 사용하여 특정 반경 내에 플레이어가 있는지 확인합니다.
playerInvisionRadius = Physics.CheckSphere(transform.position, visionRadius, PlayerLayer);
playerInShootingRadius = Physics.CheckSphere(transform.position, shootingRaidus, PlayerLayer);
// 상태 업데이트 로직: 플레이어와의 거리에 따라 적절한 상태로 전환합니다.
if (!playerInvisionRadius && !playerInShootingRadius) UpdateState(EState.Patrol);
if (playerInvisionRadius && !playerInShootingRadius) UpdateState(EState.Chase);
if (playerInvisionRadius && playerInShootingRadius) UpdateState(EState.Shoot);
// 현재 상태의 UpdateState 메서드를 호출하여, 상태에 맞는 행동을 수행합니다.
enemyStateContext.CurrentState.UpdateState();
}
// 상태를 업데이트하는 메서드입니다. EState 열거형을 기반으로 적절한 상태로 전환을 요청합니다.
private void UpdateState(EState state)
{
switch (state)
{
case EState.Patrol:
enemyStateContext.Transition(patrolState);
break;
case EState.Chase:
enemyStateContext.Transition(chaseState);
break;
case EState.Shoot:
enemyStateContext.Transition(shootState);
break;
}
}
}
마무리
Unity에서 FSM을 사용하여 AI를 설계하는 방법은 AI 개발을 명확하고 관리하기 쉽게 만들어 줍니다. 각 상태가 명확하게 정의되어 있고, 상태 간의 전환도 체계적으로 관리되므로, 복잡한 AI 로직도 이해하고 디버깅하기가 더 쉬워집니다. 이러한 접근 방식을 통해 더욱 동적이고 반응적인 게임 AI를 구현할 수 있습니다.
반응형