본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 91일차

by Mobics 2025. 4. 22.

 

목차


    게임에 필요한 상식

    25.04.09

    늦잠을 자는 바람에 앞 부분 수업을 조금 놓쳤다.. 따라서 PlayerController 구현 부분은 Github를 참고하여 작성했다.

    PlayerController 구현

    >> Attack 구현

    : 연속으로 공격하지 않던 문제 해결 --> PlayerStateAttack.cs 코드 수정

     

    >> Attack → Idle 구현

    : Behaviour State를 활용하여 구현

     

    >> Move → Idle 조건 변경

     

    ※ Camera와 Player의 거리를 3으로 조정

     

    ※ CustomEditor가 상태를 느리게 반영하는 문제 수정

    : PlayerControllerEditor.cs 수정

     

    Enemy 구현

    └ EnemyController

    : PlayerController와 같이 Old버전과 상태 패턴을 활용한 버전으로 나눠서 제작할 예정

    • Old버전 : 하나의 Script에서 모든 상태를 관리
    • 상태 패턴 : 상태 패턴을 활용하여 Class를 나눠서 관리

    ※ Enemy가 죽는 모션은 rag doll을 통해 만들 예정

     

    >> 상태 변경 조건

    • Idle → Patrol : 일정 주기로 랜덤하게 수를 뽑아서 Patrol을 할지 말지 결정 (수업에서는 1초 주기) --> Patrol하기로 결정되면 Patrol하고자 하는 목적지를 'NavMeshSurface'가 지정되어있는 영역 내에서 랜덤으로 설정하여 SetDestination()으로 이동함
    • Patrol →  Idle : 목적지에 도착
    • Idle → Trace : 적을 발견하면 적 위치로 이동
    • Patrol → Trace : 적을 발견하면 적 위치로 이동
    • Trace → Attack : 적이 공격 범위 안에 들어오면 공격
    • Attack → Trace : 공격 후, 다시 공격 범위를 체크하여 범위 밖이라면 적 위치로 이동
    • Trace → Idle : 적이 근처에 없다면 그 자리에서 다시 Idle 상태로 변경

     

    >> Enemy가 적을 찾아서 추적하는 방법

    : 길찾기 알고리즘이 필요한데, 대표적으로 A*(A Star) Algorithm, Dijkstra(다익스트라) Algorithm이 있다.

    --> 이번 코드에서는 Unity에서 A* Algorithm을 기반으로 미리 구현해놓은 'NavMesh'를 사용할 예정이다.

     

    ▶ NavMesh에 추가되어 있는 유용한 기능들

    • 중간에 장애물 추가되어도 처리
    • 있던 장애물이 삭제되어도 처리
    • 점프를 해서 건너가야할 공간도 처리
    • 오르막이라던지 계단과 같이 경사가 있어도 처리

     

    >> EnemyControllerOld.cs 생성 및 코드 작성

    ※ 캐릭터의 체력이나 공격력 같은 데이터들은 'ScriptableObject'로 관리하기도 한다.

     

    >> Chomper에 EnemyControllerOld 추가

    --> EnemyControllerOld.cs에서 OnAnimatorMove()로 관리하기 때문에 Apply Root Motion이 Handled by Script로 바뀐 모습

     

    └ Enemy Animation

    >> Chomper의 Animator Controller 생성

    : 이름은 'Chomper' --> 생성한 후, Chomper에 Animator Controller 바인딩

     

    >> Animation 추가

    • ChomperIdle : 이름을 'Idle'로 수정
    • ChomperWalkForward : 이름을 'Patrol'로 수정
    • ChomperAttack : 이름을 'Attack'으로 수정
    • ChomperHit1 : 이름을 'Hit'으로 수정
    • Blend Tree를 만들어서 이름을 'Trace'로 수정

     

    --> 다음과 같이 Make Transition

     

    >> Parameter를 추가하고, Transition 조건 설정

    : Bool 타입으로 'Idle', 'Patrol', 'Trace' 추가 --> Idle, Patrol, Trace 간의 Transition만 우선 설정

     

    >> 코드로 Animation 구현

    : EnemyControllerOld.cs

     

    └ NavMesh 사용하기

    - NavMesh를 사용하기 위해 필요한 정보

    1. 길찾기를 할 지형에 대한 정보
    2. 길찾기 정보를 기반으로 움직이게 될 Object

    ※ Animation이 Transform 정보를 갖고 있느냐에 따라 구현이 조금 달라진다

    : Animation을 실행하면 Object가 이동하는지, 제자리에 있는지에 따라 다르다 (Apply Root Motion의 체크 유무와도 관련)

     

    1. Package Manager에서 'AI Navigation' 설치

     

    2. 길찾기를 할 지형에 Component 추가

    : 'Plane'에 'NavMeshSurface' 추가

    • Agent : 움직이는 Object를 지칭하는 용어
    • Radius : 움직일 Object가 이동할 수 있는 길을 결정해주는 범위 --> 만약 문이 설정된 Radius보다 좁다면 갈 수 없게 된다.
    • Height : 움직일 Object가 이동할 수 있는 길을 결정해주는 높이 --> 만약 천장이 설정된 Height보다 낮다면 갈 수 없게 된다.
    • Step Height : 움직일 Object가 이동할 수 있는 높이 --> 계단이나 턱이 있을 때 넘어갈 수 있는 높이
    • Max Slope : 움직일 Obejct가 이동할 수 있는 경사각

    --> 설정 완료 후 Bake (수업에서는 따로 수치를 건들지 않음)

    --> Agent가 갈 수 있는 공간은 Scene에 파란색으로 표시된다.

     

    3. 움직일 Agent에 Component 추가

    : 'Chomper'에 'Nav Mesh Agent' 추가

    • Speed : 이동 속도 --> Nav Mesh가 Agent를 설정한 목적지까지 직접 이동시키는데, 그때의 속도
    • Angular Speed : 회전 속도
    • Acceleration : 가속도
    • Stopping Distance : Agent가 설정한 목적지에서 얼만큼 떨어진 위치에서 멈출지 --> 보통 Enemy는 Player를 찾으면 Player의 위치까지 가는 것이 아니라 Player의 위치 근처까지 가서 공격 범위를 확보한 상태에서 공격해야하기 때문 (0으로 설정하면 정확히 설정한 목적지까지 간다.)
    • Auto Braking : Agent가 멈춰하는 상황에서 자동으로 처리해주는 기능 (수업에선 코드로 구현할 예정이라 체크 해제)
    • Radius : Agent의 범위
    • Height : Agent의 높이

     

    4. 코드 작성

    : EnemyControllerOld.cs

     

    ※ NavMesh 테스트

    : 빈 게임 오브젝트로 'Target Position'을 생성하고 잘 보이도록 Icon을 설정한 후, Position을 옮겨준다.

    --> 이후 Chomper에 Target Position 바인딩

    --> 테스트 이후 작성한 테스트 코드와 만든 'Target Position'은 삭제

    --> 코드의 OnAnimatorMove() 를 주석처리하고 Apply Root Motion을 체크 해제

     

    - 테스트 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    
    public enum EnemyState { Idle, Patrol, Trace, Attack, Hit, Dead }
    
    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(NavMeshAgent))]
    public class EnemyControllerOld : MonoBehaviour
    {
        [Header("Enemy")]
        [SerializeField] private int attackPower = 1;
        [SerializeField] private int maxHealth = 100;
    
        [SerializeField] private Transform targetPosition;	// Test
        
        public Animator EnemyAnimator { get; private set; }
        
        private int _currentHealth;
        private EnemyState _currentState;
        
        private NavMeshAgent _navMeshAgent;
    
        private void Awake()
        {
            EnemyAnimator = GetComponent<Animator>();
            _navMeshAgent = GetComponent<NavMeshAgent>();
        }
    
        private void Start()
        {
            _currentHealth = maxHealth;
    
            _navMeshAgent.SetDestination(targetPosition.position);	// Test
        }
    
        private void Update()
        {
            switch (_currentState)
            {
                case EnemyState.Idle:
                    break;
                case EnemyState.Patrol:
                    break;
                case EnemyState.Trace:
                    break;
                case EnemyState.Attack:
                    break;
                case EnemyState.Hit:
                    break;
                case EnemyState.Dead:
                    break;
            }
        }
    
        public void SetState(EnemyState newState)
        {
            switch (newState)       // 새로운 State에 대한 처리
            {
                case EnemyState.Idle:
                    break;
                case EnemyState.Patrol:
                    break;
                case EnemyState.Trace:
                    break;
                case EnemyState.Attack:
                    break;
                case EnemyState.Hit:
                    break;
                case EnemyState.Dead:
                    break;
            }
    
            switch (_currentState)      // 기존의 State에 대한 처리
            {
                case EnemyState.Idle:
                    break;
                case EnemyState.Patrol:
                    break;
                case EnemyState.Trace:
                    break;
                case EnemyState.Attack:
                    break;
                case EnemyState.Hit:
                    break;
                case EnemyState.Dead:
                    break;
            }
            
            _currentState = newState;
        }
    
        #region 동작 처리
    
        // private void OnAnimatorMove()
        // {
        //     
        // }
    
        #endregion
    
        #region 디버깅
    
        private void OnDrawGizmos()
        {
            
        }
    
        #endregion
    }

     

    5. Layer 추가

    : 이후, Ellen의 Layer를 Player로 지정하고 EnemyControllerOld의 Target Layer를 Player로 설정

     

    └ 상태 패턴을 활용한 EnemyController 초기 세팅

    >> EnemyController.cs 생성

    : 기존에 구현한 EnemyControllerOld.cs는 전부 주석처리 --> EnemyState 등 겹치기 때문

     

    ※ EnemyControllerEditor.cs 에서 오류가 발생하는데, 이 상태로 수업이 종료됨

     

    >> Interface 및 각 상태별 Script 생성

    • EnemyStateIdle.cs
    • EnemyStatePatrol.cs
    • EnemyStateTrace.cs
    • EnemyStateAttack.cs
    • EnemyStateHit.cs
    • EnemyStateDead.cs

     

    최종 코드

    >> EnemyControllerOld.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    using Random = UnityEngine.Random;
    
    public enum EnemyState { Idle, Patrol, Trace, Attack, Hit, Dead }
    
    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(NavMeshAgent))]
    public class EnemyControllerOld : MonoBehaviour
    {
        [Header("Enemy")]
        [SerializeField] private int attackPower = 1;
        [SerializeField] private int maxHealth = 100;
    
        [Header("AI")]
        [SerializeField] private LayerMask targetLayer;
        [SerializeField] private float detectCircleRadius = 10f;    // 감지 범위
        [SerializeField] private float maxPatrolWaitTime = 3f;      // Idle -> Patrol의 촤대 대기 시간
        [SerializeField] private float detectSightAngle = 30f;      // 감지 시야각
    
        private Animator _enemyAnimator;
        private NavMeshAgent _navMeshAgent;
        
        private int _currentHealth;     // 적의 체력
        
        // 적의 현재 상태
        private EnemyState _currentState;
        public EnemyState CurrentState => _currentState;
    
        // Player의 추적 위치를 업데이트 하는 Coroutine
        private Coroutine _updateDestinationCoroutine;
        
        private float _detectCircleRadiusSqr;   // Player와의 거리를 비교하기 위한 변수
        private float _patrolWaitTime;          // 누적 Patrol 대기 시간
        
        // ㅡㅡㅡ AI ㅡㅡㅡ
        private Transform _playerTransform; // 감지된 Player의 Transform
    
        private void Awake()
        {
            _enemyAnimator = GetComponent<Animator>();
            _navMeshAgent = GetComponent<NavMeshAgent>();
            _navMeshAgent.updatePosition = false;   // navMesh가 직접 Agent의 Position을 옮기기 때문에 이를 비활성화
            _navMeshAgent.updateRotation = true;
        }
    
        private void Start()
        {
            _currentHealth = maxHealth;
            // 제곱 정도는 Mathf.Pow() 함수보다 직접 곱하는 것이 성능에 이점이 있다.
            _detectCircleRadiusSqr = detectCircleRadius * detectCircleRadius;
            _patrolWaitTime = 0f;
            
            SetState(EnemyState.Idle);
        }
    
        private void Update()
        {
            switch (_currentState)
            {
                case EnemyState.Idle:
                {
                    // 플레이어 감지
                    var detectPlayer = DetectPlayerInCircle();
                    if (detectPlayer)
                    {
                        _playerTransform = detectPlayer;
                        SetState(EnemyState.Trace);
                        break;
                    }
                    
                    // 정찰 여부 판단
                    if (_patrolWaitTime > maxPatrolWaitTime && Random.Range(0, 100) < 30)
                    {
                        // 정찰하기로 결정
                        SetState(EnemyState.Patrol);
                        break;
                    }
                    _patrolWaitTime += Time.deltaTime;
                    break;
                }
                case EnemyState.Patrol:
                {
                    // 플레이어 감지
                    var detectPlayer = DetectPlayerInCircle();
                    if (detectPlayer)
                    {
                        _playerTransform = detectPlayer;
                        SetState(EnemyState.Trace);
                        break;
                    }
                    
                    // Patrol 위치에 도착하면 Idle 상태로 전환
                    // pathPending : 길찾기 연산 중 / ramainingDistance : 목적지와의 남은 거리
                    if (!_navMeshAgent.pathPending && _navMeshAgent.remainingDistance < 0.1f)
                    {
                        SetState(EnemyState.Idle);
                        break;
                    }
                    break;
                }
                case EnemyState.Trace:
                {
                    // Player와 Enemy의 거리 계산
                    // 거리를 계산할 때, Vector3.Distance(transform.position, _playerTransform.position); 를 사용해도 된다.
                    // Magnitude는 루트 연산이 들어가기 때문에 Update()에서 계속 처리하기에는 무겁다.
                    // 따라서 sqrMagnitude를 사용했고 대신 거리를 비교할 때, 마찬가지로 제곱된 값으로 비교해야한다. 
                    var playerDistanceSqr = (_playerTransform.position - transform.position).sqrMagnitude;
                    
                    // Trace 중 시야에 플레이어가 들어오면 속도 증가
                    if (DetectPlayerInSight(_playerTransform))
                    {
                        _enemyAnimator.SetFloat("Speed", 1f);
                    }
                    else
                    {
                        _enemyAnimator.SetFloat("Speed", 0f);
                    }
                    
                    // 일정 거리 이상으로 Player가 멀어지면 Idle로 전환
                    if (playerDistanceSqr > _detectCircleRadiusSqr)
                    {
                        SetState(EnemyState.Idle);
                    }
                    break;
                }
                case EnemyState.Attack:
                {
                    break;
                }
                case EnemyState.Hit:
                {
                    break;
                }
                case EnemyState.Dead:
                {
                    break;
                }
            }
        }
    
        public void SetState(EnemyState newState)
        {
            switch (newState)       // 새로운 State에 대한 처리 (Enter)
            {
                case EnemyState.Idle:
                {
                    // 찾아야 할 Player 정보를 초기화
                    _playerTransform = null;
                    // Patrol 대기 시간 초기화
                    _patrolWaitTime = 0f;
                    // Idle 상태에서는 Agent의 이동을 중지
                    _navMeshAgent.isStopped = true;
                    // Idle Animation 재생
                    _enemyAnimator.SetBool("Idle", true);
                    break;
                }
                case EnemyState.Patrol:
                {
                    // 랜덤으로 정찰 위치를 구하고, 있으면 해당 위치로 이동, 없으면 다시 Idle 상태로 전환
                    var patrolPoint = FindRandomPatrolPoint();
                    if (patrolPoint == transform.position)
                    {
                        SetState(EnemyState.Idle);
                        break;
                    }
    
                    _navMeshAgent.isStopped = false;
                    _navMeshAgent.SetDestination(patrolPoint);
                    
                    // Patrol Animation 재생
                    _enemyAnimator.SetBool("Patrol", true);
                    break;
                }
                case EnemyState.Trace:
                {
                    // 감지된 Player를 향해 이동
                    _navMeshAgent.isStopped = false;
                    _updateDestinationCoroutine = StartCoroutine(UpdateDestination());
                    
                    // Trace Animation 재생
                    _enemyAnimator.SetBool("Trace", true);
                    break;
                }
                case EnemyState.Attack:
                {
                    break;
                }
                case EnemyState.Hit:
                {
                    break;
                }
                case EnemyState.Dead:
                {
                    break;
                }
            }
    
            switch (_currentState)      // 기존의 State에 대한 처리 (Exit)
            {
                case EnemyState.Idle:
                {
                    // Idle Animation 종료
                    _enemyAnimator.SetBool("Idle", false);
                    break;
                }
                case EnemyState.Patrol:
                {
                    // Patrol Animation 종료
                    _enemyAnimator.SetBool("Patrol", false);
                    break;
                }
                case EnemyState.Trace:
                {
                    // Player의 위치를 갱신하는 Coroutine 중지
                    if (_updateDestinationCoroutine != null)
                    {
                        StopCoroutine(UpdateDestination());
                        _updateDestinationCoroutine = null;
                    }
                    // Trace Animation 종료
                    _enemyAnimator.SetBool("Trace", false);
                    break;
                }
                case EnemyState.Attack:
                {
                    break;
                }
                case EnemyState.Hit:
                {
                    break;
                }
                case EnemyState.Dead:
                {
                    break;
                }
            }
            
            _currentState = newState;
        }
    
        #region 적 감지
    
        private Vector3 FindRandomPatrolPoint()
        {
            Vector3 randomDirection = Random.insideUnitSphere * detectCircleRadius;
            randomDirection += transform.position;
    
            NavMeshHit hit;
            if (NavMesh.SamplePosition(randomDirection, out hit, detectCircleRadius, NavMesh.AllAreas))
            {
                return hit.position;
            }
            else
            {
                return transform.position;
            }
        }
        
        IEnumerator UpdateDestination()
        {
            while (_playerTransform)
            {
                _navMeshAgent.SetDestination(_playerTransform.position);
                yield return new WaitForSeconds(0.5f);
            }
        }
    
        // 일정 반경에 플레이어가 진입하면 플레이어 소리를 감지했다고 판단
        private Transform DetectPlayerInCircle()
        {
            // 현재 위치에서 일정 반경 안에 Collider가 있으면 해당 Object의 Collider를 hitColliders에 담게 된다.
            // LayerMask로 원하는 Collider만 구분하도록 처리
            var hitColliders = Physics.OverlapSphere(transform.position, detectCircleRadius, targetLayer);
            if (hitColliders.Length > 0)
            {
                return hitColliders[0].transform;
            }
            else
            {
                return null;
            }
        }
        
        // 일정 반경에 플레이어가 진입하면 시야에 들어왔다고 판단
        private bool DetectPlayerInSight(Transform playerTransform)
        {
            if (playerTransform == null)
            {
                return false;
            }
    
            // Player와 Enemy 사이의 각 구하는 방법 (1)
            // Vector3 direction = playerTransform.position - transform.position;
            // float angle = Vector3.Angle(direction, transform.forward);
    
            // Player와 Enemy 사이의 각 구하는 방법 (2)
            var cosTheta = Vector3.Dot(transform.forward,
                (playerTransform.position - transform.position).normalized);
            var angle = Mathf.Acos(cosTheta) * Mathf.Rad2Deg;
    
            if (angle < detectSightAngle)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    
        #endregion
    
        #region 동작 처리
    
        private void OnAnimatorMove()
        {
            // Animator에서 Enemy가 움직이고 난 이후의 Position이 'position'에 할당 
            var position = _enemyAnimator.rootPosition;  // rootPosition : Animator가 가지고 있는 해당 Position (좌표)
            
            position.y = _navMeshAgent.nextPosition.y;  // nextPosition : Agent를 이동시킬 때 다음에 이동할 위치
            _navMeshAgent.nextPosition = position;
            transform.position = position;
        }
    
        #endregion
    
        #region 디버깅
    
        private void OnDrawGizmos()
        {
            // Circle 감지 범위
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(transform.position, detectCircleRadius);
            
            // 시야각
            Gizmos.color = Color.red;
            Vector3 rightDirection = Quaternion.Euler(0, detectSightAngle, 0) * transform.forward;
            Vector3 leftDirection = Quaternion.Euler(0, -detectSightAngle, 0) * transform.forward;
            Gizmos.DrawRay(transform.position, rightDirection * detectCircleRadius);
            Gizmos.DrawRay(transform.position, leftDirection * detectCircleRadius);
            Gizmos.DrawRay(transform.position, transform.forward * detectCircleRadius);
        }
    
        #endregion
    }

     

    >> EnemyControllerEditor.cs

    using UnityEditor;
    using UnityEngine;
    
    [CustomEditor(typeof(EnemyControllerOld))]
    public class EnemyControllerEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            // 기본 인스펙터를 그리기
            base.OnInspectorGUI();
            
            // 타겟 컴포넌트 참조 가져오기
            EnemyControllerOld enemyControllerOld = (EnemyControllerOld)target;
            
            // 여백 추가
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("상태 디버그 정보", EditorStyles.boldLabel);
            
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            
            // 상태별 색상 지정
            switch (enemyControllerOld.CurrentState)
            {
                case EnemyState.Idle:
                    GUI.backgroundColor = new Color(0, 0, 1, 1f);
                    break;
                case EnemyState.Patrol:
                    GUI.backgroundColor = new Color(0, 1, 0, 1f);
                    break;
                case EnemyState.Trace:
                    GUI.backgroundColor = new Color(1, 0, 1, 1f);
                    break;
                case EnemyState.Attack:
                    GUI.backgroundColor = new Color(1, 1, 0, 1f);
                    break;
                case EnemyState.Hit:
                    GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1f);
                    break;
                case EnemyState.Dead:
                    GUI.backgroundColor = new Color(1, 0, 0, 1f);
                    break;
            }
    
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            EditorGUILayout.LabelField("현재 상태", enemyControllerOld.CurrentState.ToString(),
                EditorStyles.boldLabel);
            EditorGUILayout.EndVertical();
            
            EditorGUILayout.EndVertical();
            
            // Color 초기화
            GUI.backgroundColor = Color.white;
        }
        
        // 인스펙터가 활성화될 때 에디터 업데이트 함수 등록
        private void OnEnable()
        {
            EditorApplication.update += OnEditorUpdate;
        }
    
        // 인스펙터가 비활성화될 때 에디터 업데이트 함수 해제
        private void OnDisable()
        {
            EditorApplication.update -= OnEditorUpdate;
        }
    
        // 에디터 업데이트 시 인스펙터를 계속 새로고침하여 실시간으로 상태 반영
        private void OnEditorUpdate()
        {
            if (target != null)
            {
                Repaint();
            }
        }
    }

     

     

    >> EnemyController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.AI;
    
    public enum EnemyState { None, Idle, Patrol, Trace, Attack, Hit, Dead }
    
    [RequireComponent(typeof(NavMeshAgent))]
    [RequireComponent(typeof(Animator))]
    public class EnemyController : MonoBehaviour
    {
        
    }

     

    >> IEnemyState.cs

    public interface IEnemyState
    {
        void Enter(EnemyController enemyController);
        void Update();
        void Exit();
    }

     

    >> EnemyStateIdle.cs

    using UnityEngine;
    
    public class EnemyStateIdle : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }

     

    >> EnemyStatePatrol.cs

    using UnityEngine;
    
    public class EnemyStatePatrol : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }

     

    >> EnemyStateTrace.cs

    using UnityEngine;
    
    public class EnemyStateTrace : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }

     

    >> EnemyStateAttack.cs

    using UnityEngine;
    
    public class EnemyStateAttack : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }

     

    >> EnemyStateHit.cs

    using UnityEngine;
    
    public class EnemyStateHit : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }

     

    >> EnemyStateDead.cs

    using UnityEngine;
    
    public class EnemyStateDead : IEnemyState
    {
        public void Enter(EnemyController enemyController)
        {
            
        }
    
        public void Update()
        {
            
        }
    
        public void Exit()
        {
            
        }
    }