본문 바로가기

Unity

[Unity3D]FSM을 활용한 AI개발(마무리)

1. 서론

코드는 직접 타이핑하고 직접 만들어 보는 게 최고입니다.

그럼에도 제가 코드를 올려놓는 이유는 그래도 어떻게 해야 하는지 감이 안 잡히는 분들은 복사해서 분석하시라고 올려놓는 겁니다. 많이 부족합니다. 양해 부탁드리며, 좋은 취지로 올리는 것이니 너무 뭐라 하지 말아 주시면 감사하겠습니다.

 

2. 본론

FSM.cs

...더보기

FSM.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MyFSM
{
    abstract public class FSM<T>
    {
        private readonly T m_Owner;
        abstract public void Begin();
        abstract public void Run();
        abstract public void Exit();
    }

    //public enum SOLDIER_STATE
    //{
    //    Idle = 0,
    //    Walk,
    //    Attack,
    //    Covering,
    //    Die,
    //    END
    //}

    public enum ZOMBIE_STATE
    {
        Idle = 0,
        Walk,
        Attack,
        Die,
        END
    }

    //public enum CIVILIAN_STATE
    //{
    //   Idle = 0,
    //    Walk, 
    //    Die,
    //    END
    }
}

 

Head_Machine.cs

...더보기

Head_Machine.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MyFSM
{

    public class Head_Machine<T>
    {
        //오브젝트
        private T Owner;

        //상태 값
        private FSM<T> m_CurState = null;//현재
        private FSM<T> m_PrevState = null;//이전

        //첫 상태값
        public void Begin()
        {
            if (m_CurState != null)
            {
                m_CurState.Begin();
            }
        }

        //상태 업데이트
        public void Run()
        {
            CheckState();
        }


        public void CheckState()
        {
            if (m_CurState != null)
            {
                m_CurState.Run();
            }
        }

        //fsm종료
        public void Exit()
        {
            m_CurState.Exit();
            m_CurState = null;
            m_PrevState = null;
            Debug.Log(Owner.ToString() + " FSM 종료");
        }

        public void Change(FSM<T> _state)
        {
            //같은 상태를 변화...는 리턴
            if (_state == m_CurState)
                return;

            m_PrevState = m_CurState;

            //현재 상태가 있다면 종료
            if (m_CurState != null)
                m_CurState.Exit();

            m_CurState = _state;
            //새로 적용된 상태가 널이 아니면 실행
            if (m_CurState != null)
                m_CurState.Begin();
        }

        //변화할땐 아무런 인자값이 없으면 전에 상태값을 출력한다
        public void Revert()
        {
            if (m_PrevState != null)
                Change(m_PrevState);
        }

        //상태값세팅
        public void SetState(FSM<T> _state, T _Owner)
        {
            Owner = _Owner;
            m_CurState = _state;

            //들어온 상태값이 지금과 다를때, 현재상태값이 채워져 있을때
            if (m_CurState != _state && m_CurState != null)
                m_PrevState = m_CurState;
        }
    }
}

 

ZombieFSM.cs

...더보기

ZombieFSM.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Global;

namespace MyFSM
{
    public class ZombieFSM : MonoBehaviour
    {
        public Head_Machine<ZombieFSM> m_state;
        public FSM<ZombieFSM>[] m_arrState = new FSM<ZombieFSM>[(int)ZOMBIE_STATE.END];

        public float m_fFindRange;
        public Transform m_TransTarget;

        public ZOMBIE_STATE m_eCurState;
        public ZOMBIE_STATE m_ePrevState;

        public int m_iHealth;
        public float m_fAttackRange;

        public Animator m_Animator;

        public ZombieFSM()
        {
            Init();
        }

        public void Init()
        {
            m_state = new Head_Machine<ZombieFSM>();

            m_arrState[(int)ZOMBIE_STATE.Idle] = new Zombie_Idle(this);
            m_arrState[(int)ZOMBIE_STATE.Walk] = new Zombie_Walk(this);
            m_arrState[(int)ZOMBIE_STATE.Die] = new Zombie_Die(this);
            m_arrState[(int)ZOMBIE_STATE.Attack] = new Zombie_Attack(this);


            m_state.SetState(m_arrState[(int)ZOMBIE_STATE.Idle], this);
        }

        private void Awake()
        {
            m_Animator = this.GetComponent<Animator>();
        }

        private void Start()
        {
            Begin();
        }

        private void Update()
        {
            Run();

            m_fFindRange += Time.deltaTime;
            m_fFindRange = Mathf.Clamp(m_fFindRange , 10f, 500f);

            if (m_fFindRange >= 25f)
                m_fFindRange = 10f;
        }

        public void ChangeFSM(ZOMBIE_STATE ps)
        {
            for (int i = 0; i < (int)ZOMBIE_STATE.END; ++i)
            {
                if (i == (int)ps)
                    m_state.Change(m_arrState[(int)ps]);
            }
        }

        public void Begin()
        {
            m_state.Begin();
        }

        public void Run()
        {
            m_state.Run();
        }

        public void Exit()
        {
            m_state.Exit();
        }

        public void TakeDamage(SoldierInfo _SoldierInfo)
        {
            if (m_iHealth <= 0)
            {
                ChangeFSM(ZOMBIE_STATE.Die);
                return;
            }

            SoldierInfo info = new SoldierInfo
            {
                _iDamage = _SoldierInfo._iDamage,
                _Owner = _SoldierInfo._Owner
            };

            if (m_TransTarget == null)
            {
                m_TransTarget = info._Owner;
                ChangeFSM(ZOMBIE_STATE.Walk);
            }

            if (info._iDamage >= 0 && info._iDamage <= 59)
            {
                m_iHealth -= 3;
            }
        }

        public void DestroyObject()
        {
            Destroy(this.gameObject, 2f);
        }
    }
}

 

Zombie_Idle.cs

...더보기

Zombie_Idle.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MyFSM
{
    public class Zombie_Idle : FSM<ZombieFSM>
    {
        private ZombieFSM m_Owner;
        
        public Zombie_Idle(ZombieFSM _owner)
        {
            m_Owner = _owner;   
        }

        public override void Begin()
        {
            Debug.Log("Idle Begin");
            m_Owner.m_eCurState = ZOMBIE_STATE.Idle;
        }

        public override void Run()
        {
            if (FindRange())
            {
                m_Owner.ChangeFSM(ZOMBIE_STATE.Walk);
            }
            else
            {
                m_Owner.m_TransTarget = null;
            }
        }

        public override void Exit()
        {
            Debug.Log("Idle Exit");
            m_Owner.m_ePrevState = ZOMBIE_STATE.Idle;
        }

        private bool FindRange()
        {
            Collider[] hitColliders = Physics.OverlapSphere(m_Owner.transform.position, m_Owner.m_fFindRange, (1 << LayerMask.NameToLayer("Civilian")) | (1 << LayerMask.NameToLayer("Soldier")));
            
            if (hitColliders.Length != 0)
            {
                for (int i = 0; i < hitColliders.Length; ++i)
                {
                    if (hitColliders[i].gameObject.layer == LayerMask.NameToLayer("Soldier"))
                    {
                        m_Owner.m_TransTarget = hitColliders[i].transform;
                    }
                }

                if (m_Owner.m_TransTarget == null)
                    m_Owner.m_TransTarget = hitColliders[0].transform;
               
                return true;
            }
            return false;
        }
    }
}

 

Zombie_Walk.cs

...더보기

Zombie_Walk.cs

using Global;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

namespace MyFSM
{
    public class Zombie_Walk : FSM<ZombieFSM>
    {
        private ZombieFSM m_Owner;

        private NavMeshAgent agent;

        private Animator m_Animator;

        public Zombie_Walk(ZombieFSM _owner)
        {
            m_Owner = _owner;
        }

        public override void Begin()
        {
            Debug.Log("Walk Begin");
            agent = m_Owner.GetComponent<NavMeshAgent>();
            agent.isStopped = false;
            m_Animator = m_Owner.m_Animator;

            m_Owner.m_eCurState = ZOMBIE_STATE.Walk;
            m_Animator.SetBool("Walk", true);
        }

        public override void Run()
        {
            if (m_Owner.m_TransTarget == null)
            {
               
                m_Owner.ChangeFSM(ZOMBIE_STATE.Idle);
            }
            

            if (m_Owner.m_TransTarget != null)
                GotoTarget();

            if (m_Owner.m_TransTarget != null && m_Owner.m_fAttackRange >= Vector3.Distance(m_Owner.transform.position, m_Owner.m_TransTarget.position))
            {
                m_Owner.ChangeFSM(ZOMBIE_STATE.Attack);
            }

        }

        public override void Exit()
        {
            Debug.Log("Walk Exit");
            agent.isStopped = true;
            m_Owner.m_ePrevState = ZOMBIE_STATE.Walk;
            m_Animator.SetBool("Walk", false);
        }

        private bool FindRange()
        {
            Collider[] hitColliders = Physics.OverlapSphere(m_Owner.transform.position, m_Owner.m_fFindRange, (1 << LayerMask.NameToLayer("Civilian")) | (1 << LayerMask.NameToLayer("Soldier")));

            if (hitColliders.Length != 0)
            {
                for (int i = 0; i < hitColliders.Length; ++i)
                {
                    if (hitColliders[i].gameObject.layer == LayerMask.NameToLayer("Soldier"))
                    {
                        m_Owner.m_TransTarget = hitColliders[i].transform;
                    }
                }

                if (m_Owner.m_TransTarget == null)
                    m_Owner.m_TransTarget = hitColliders[0].transform;

                return true;
            }
            return false;
        }

        private void GotoTarget()
        {
            ZombieInfo info = new ZombieInfo
            {
                _Owner = m_Owner.transform
            };

            agent.SetDestination(m_Owner.m_TransTarget.position);

            if(m_Owner.m_TransTarget.gameObject.layer == LayerMask.NameToLayer("Civilian"))
                m_Owner.m_TransTarget.gameObject.SendMessage("ZombieFollow", info, SendMessageOptions.DontRequireReceiver);
        }
    }

}

3. 결론

이것으로 마무리 짓겠습니다.

감사합니다.