-
Unity AI 아키텍처: 유한 상태 머신(FSM) 패턴을 이용한 몬스터 AI2024년 04월 23일
- 유니얼
-
작성자
-
2024.04.23.:42
728x90FSM 패턴이란?
유한 상태 머신(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를 구현할 수 있습니다.
반응형다음글이전글이전 글이 없습니다.댓글
스킨 업데이트 안내
현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드
받을 수 있는 페이지로 이동하시겠습니까?
("아니오" 를 선택할 시 30일 동안 최신 버전이
감지되어도 모달 창이 표시되지 않습니다.)