• 티스토리 홈
  • 프로필사진
    유니얼
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
유니얼
  • 프로필사진
    유니얼
    • 분류 전체보기 (295)
      • Unity (17)
        • 게임 개발 (5)
      • Unreal (24)
        • 게임 개발 (20)
      • DirectX (36)
      • 코딩테스트 (91)
        • 프로그래머스 (25)
        • 백준 (66)
      • Google Workspace (1)
      • Programing (102)
        • C# (68)
        • C++ (24)
        • JavaScript (10)
      • 게임 서버 프로그래밍 (17)
      • Web (6)
        • 슈퍼코딩 (6)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
    등록된 댓글이 없습니다.
  • 최근 공지
    등록된 공지가 없습니다.
# Home
# 공지사항
#
# 태그
# 검색결과
# 방명록
  • Unity AI 아키텍처: 유한 상태 머신(FSM) 패턴을 이용한 몬스터 AI
    2024년 04월 23일
    • 유니얼
    • 작성자
    • 2024.04.23.: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를 구현할 수 있습니다.

    반응형
    다음글
    다음 글이 없습니다.
    이전글
    이전 글이 없습니다.
    댓글
조회된 결과가 없습니다.
스킨 업데이트 안내
현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
목차
표시할 목차가 없습니다.
    • 안녕하세요
    • 감사해요
    • 잘있어요

    티스토리툴바