본문 바로가기

Programming/Unity

몬스터 FSM 구현

실용적인 예제로 본 게임 인공지능 책을 보고 C++ FSM을 구현할 때는 별 문제가 없었지만 이중상속이 지원되지 않는 유니티 C# 환경에서 FSM을 구현 하려면 딜리게이트나 코루틴을 이용해야 하는데 처음에는 코루틴을 이용해 구현하려했다가 실패를 했고 딜리게이트 역시 내 스스로 딜리게이트 개념을 잘 이해하고 있지 않아서인지 꼬이게 되어서 다른 방법을 인터페이스 개념을 생각해서 따로 만들게 되었다.


기본이 되는 FSM_State 클래스


 - MonoBehaviour를 상속받지 않기 때문에 유니티에서 자동으로 업데이트 하지 않는다. 다른 상태들의 인터페이스를 제공할

   추상클래스


abstract public class FSM_State<t>
{
    abstract public void EnterState(T _Slime);

    abstract public void UpdateState(T _Slime);

    abstract public void ExitState(T _Slime);
}


Attack 클래스


 - 공격을 담당할 클래스 들어올 때 타겟의 유무를 확인하고 매 업데이트마다 죽음 유무또한 확인 타겟이 해당사정거리에 있고

   타겟의 방향이 전방 45도 내에 있다면 공격을 시작한다. 내적과 길이를 구하는 것으로 판단한다. 요구조건에 만족하지 

   않는다면 Move로 상태를 변경하고 타겟을 찾거나 방황하게 한다.



using UnityEngine;
using System.Collections;

public class State_Attack : FSM_State<slime>
{
    static readonly State_Attack instance          = new State_Attack();
    public static State_Attack  Instance
    {
        get 
        { 
            return instance; 
        }
    }
    private float                AttackTimer       = 0f;

    static State_Attack() { }
    private State_Attack() { }

    public override void EnterState(Slime _Slime)
    {
        // 타겟 유무 확인
        if( _Slime.myTarget == null)
        {
            return;
        }
        AttackTimer = _Slime.AttackSpeed;
    }

    public override void UpdateState(Slime _Slime)
    {
        // 죽음 유무 확인
        if (_Slime.CurrentHP <= 0)
        {
            _Slime.ChangeState(State_Die.Instance);
        }

        AttackTimer += Time.deltaTime;
        if (!_Slime.myTarget.GetComponent<charactorcontrol>().IsDead && _Slime.CheckRange() && _Slime.CheckAngle())
        {
            if (AttackTimer >= _Slime.AttackSpeed)
            {
                int Damage = Random.Range(_Slime.SlimeDamage, _Slime.SlimeDamage + _Slime.DamageRandValue);
                _Slime.MGR.SlimeAttack(Damage);
                _Slime.GetComponent<animation>().CrossFade("Attack", 0.2f);
                AttackTimer = 0.0f;
                _Slime.ChaseTime = 0.0f;
            }
        }
        else
        {
            _Slime.ChangeState(State_Move.Instance);
        }
    }

    public override void ExitState(Slime _Slime)
    {
        Debug.Log("State_Attack 빠져나옴");
    }

}


Die 클래스


 - 몬스터가 죽음에 이르렀을 때 활성화 하는 상태로 본래는 초가 증가함에 따라 서서히 투명화되게 만드려고 코드를 짰는데 받은

   몬스터 리소스가 알파조작이 안먹혀 어쩔 수 없이 그냥 사라지게 하였다...



using UnityEngine;
using System.Collections;

public class State_Die : FSM_State<slime>
{
    static readonly State_Die instance = new State_Die();
    public static State_Die Instance
    {
        get 
        { 
            return instance; 
        }
    }
    float Count = 2f;
    float time = 0f;

    static State_Die() { }
    private State_Die() { }

    public override void EnterState(Slime _Slime)
    {
        _Slime.GetComponent<animation>().CrossFade("Dead");
        _Slime.IsDead = true;
        _Slime.CreateItem(Random.Range(0, 3), Random.Range(0, 4));
    }

    public override void UpdateState(Slime _Slime)
    {
        time += Time.deltaTime;
        //_Slime.AlphaChange(time / Count, Count);
        //color = _Slime.renderer.material.color;
        //color.a *= time / Count;
        //if( color.a > Count)
        //    color.a = Count;
        //_Slime.renderer.material.color = color;
        if( _Slime.isActiveAndEnabled && time >= Count)
        {
            _Slime.gameObject.SetActive(false);
            time = 0f;
        }
    }

    public override void ExitState(Slime _Slime)
    {

    }
}


Move 클래스


 - 몬스터 클래스에서 coliider를 이용해 OnTriggerEnter 에 플레이어가 들어오게 되면 타겟을 잡고 해당 타겟으로 공격에 대한 

   값이 만족할 때 까지 이동하게 해놨다. 혹은 타겟이 없을경우 랜덤한 방향으로 이동속도가 감소된 상태로 방황하게 설정

   하였다.



using UnityEngine;
using System;
using System.Collections;

public class State_Move : FSM_State<slime>
{
    static      readonly State_Move         instance        = new State_Move();
    public      static State_Move           Instance
    {
        get 
        { 
            return instance; 
        }
    }

    private     float                       ResetTime       = 3f;
    private     float                       CurrenntTime;

    static State_Move() { }
    private State_Move() { }

    public override void EnterState(Slime _Slime)
    {
        CurrenntTime = ResetTime;
    }

    public override void UpdateState(Slime _Slime)
    {
        // 죽음 유무 확인
        if (_Slime.CurrentHP <= 0)
        {
            _Slime.ChangeState(State_Die.Instance);
        }

        // 타겟 유무 확인
        if(_Slime.myTarget != null)
        {
            if(!_Slime.CheckRange())
            {
                // 추적시간을 초과하면 타겟을 잃음
                _Slime.ChaseTime += Time.deltaTime;
                if (_Slime.ChaseTime >= _Slime.ChaseCancleTime)
                {
                    _Slime.myTarget = null;
                    _Slime.ChaseTime = 0.0f;
                    return;
                }

                // 회전각 구하기
                Vector3 Dir = _Slime.myTarget.position - _Slime.transform.position;
                Vector3 NorDir = Dir.normalized;
                
                Quaternion angle = Quaternion.LookRotation(NorDir);

                // 회전
                _Slime.transform.rotation = angle;

                // 이동
                Vector3 Pos = _Slime.transform.position;
                Pos += _Slime.transform.forward * Time.smoothDeltaTime * _Slime.MoveSpeed;
                _Slime.transform.position = Pos;

            }
            else
            {
                _Slime.ChangeState(State_Attack.Instance);
            }
              
        }
        else
        {
            // 타겟이 없을 때는 임의의 방향으로 방황함
            // 방향 재설정
            SetRandDir(_Slime);
            //#if DEBUG
            // 몬스터의 진행방향 표시
            // 시각적으로 알기 쉽게 y좌표만 높여줌
            Vector3 endPoint = _Slime.transform.position + (_Slime.transform.forward * 2f);
            endPoint.y += 1f;
            Debug.DrawLine(_Slime.transform.position, endPoint, Color.red);
            //Debug.Log("x : " + _Slime.transform.forward.x + ", y : " + _Slime.transform.forward.y + ", z : " + _Slime.transform.forward.z);
            //#endif
            endPoint.y = 0;
            Vector3 pos = _Slime.transform.position;
            pos += _Slime.transform.forward * Time.smoothDeltaTime * (_Slime.MoveSpeed / 3f);
            _Slime.transform.position = pos;
        }
        _Slime.Ani.CrossFade("Walk");
    }

    public override void ExitState(Slime _Slime)
    {
#if DEBUG
        Debug.Log("State_Move를 종료합니다.");
#endif
    }

    // ResetTime 때 마다 임의의 방향으로 설정
    void SetRandDir(Slime _Slime)
    {
        CurrenntTime += Time.smoothDeltaTime;
        if (CurrenntTime >= ResetTime)
        {
            _Slime.transform.forward = Quaternion.AngleAxis(UnityEngine.Random.Range(0, 360f), Vector3.up) * Vector3.forward;
            // 시간 재설정
            ResetTime = UnityEngine.Random.Range(1f, 4f);
            CurrenntTime = 0f;
        }
    }
}


상태 변경을 담당할 StateMachine 클래스 


 - 몬스터클래스에서 각 몬스터상태 클래스를 변경할 수 있도록 연결해주는 역할을 한다.



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

public class StateMachine <t>
{
    private T Owner;
    private FSM_State<t> CurrentState;
    private FSM_State<t> PreviousState;

    // 변수 초기화
    public void Awake()
    {
        CurrentState = null;
        PreviousState = null;
    }

    // 상태 변경
    public void ChangeState(FSM_State<t> _NewState)
    {
        // 같은 상태를 변환하려 한다면 나감
        if(_NewState == CurrentState)
        {
            return;
        }

        PreviousState = CurrentState;
        
        // 현재 상태가 있다면 종료
        if( CurrentState != null)
        {
            CurrentState.ExitState(Owner);
        }

        CurrentState = _NewState;

        // 새로 적용된 상태가 null이 아니면 실행
        if( CurrentState != null)
        {
            CurrentState.EnterState(Owner);
        }
    }

    // 초기상태설정
    public void Initial_Setting(T _Owner, FSM_State<t> _InitialState)
    {
        Owner = _Owner;
        ChangeState(_InitialState);
    }

    // 상태 업데이트
	public void Update ()
    {
        if(CurrentState != null)
        {
            CurrentState.UpdateState(Owner);
        }
	}

    // 이전 상태로 회귀
    public void StateRevert()
    {
        if(PreviousState != null)
        {
            ChangeState(PreviousState);
        }
    }
}


마지막으로 이걸 적용한 몬스터 클래스 Slime

using UnityEngine;
using System.Collections;

public class Slime : MonoBehaviour 
{
    public      float                   SlimeHP                 = 50;
    public      float                   CurrentHP               = 50;
    public      int                     HPRandValue             = 10;
    public      int                     SlimeDamage             = 5;
    public      int                     DamageRandValue         = 3;
    public      float                   AttackRange             = 2f;
    public      float                   AttackSpeed             = 1.5f;
    public      float                   ChaseCancleTime         = 5.0f;
    public      float                   ChaseTime               = 0;
    public      float                   MoveSpeed               = 2.5f;
    public      float                   RotSpeed                = 100;
    public      Transform               myTarget;
    private     StateMachine<slime>     state                   = null;
    public      Animation               Ani                     = null;
    public      float                   DeadTimmer              = 0;
    public      bool                    IsDead                  = false;
    public      GameMGR                 MGR;
    public      GameObject              HPBar;
    public      HUDText                 DMGText                 = null;



    void Awake()
    {
        // 게임 매니져 설정
        MGR = GameObject.Find("GameMGR").GetComponent<gamemgr&t();
        // 초기 설정
        ResetState();
        // 애니메이션 컴포넌트 가져오기
        Ani = GetComponent<animation>();
        //체력바 생성
        HPBar = (GameObject)Instantiate(Resources.Load("Prefab/MOBHPbar", typeof(GameObject)), new Vector3(0, 0, 0), Quaternion.identity);
    }

	// Use this for initialization
	void Start () 
    {
        
	}
	
	// Update is called once per frame
	void Update () 
    {
        state.Update();
        HPBarUpdate();        
	}

    // 체력바 업데이트
    void HPBarUpdate()
    {
        // 위치 재조정
        Vector3 Pos = Camera.main.WorldToViewportPoint(transform.position);
        Vector3 hpbarpos = GameObject.Find("UICam").GetComponent<camera>().ViewportToWorldPoint(Pos);
        hpbarpos.y += 0.2f;
        HPBar.transform.position = new Vector3(hpbarpos.x, hpbarpos.y, 0);

        HPBar.transform.GetComponent<uislider>().value = CurrentHP / SlimeHP;
    }
    
    // 상태변경
    public void ChangeState(FSM_State<slime> _State)
    {
        state.ChangeState(_State);
    }

    void OnTriggerEnter (Collider _Other)
    {
        // 플레이어 접근시 move_state로 변경
        if( _Other.transform.tag == "Player")
        {
#if DEBUG
            Debug.Log("플레이어가 범위 내에 접근하였습니다.");
#endif
            myTarget = _Other.transform.FindChild("Cha_Knight");

            if( CheckRange())
            {
                ChangeState(State_Attack.Instance);
            }
            else
            {
                ChangeState(State_Move.Instance);
            }
        }
        else
        {
            return;
        }

    }

    public bool CheckRange()
    {
        if( Vector3.Distance(myTarget.transform.position, transform.position) <= AttackRange)
        {
            return true;
        }
        return false;
    }

    public bool CheckAngle()
    {
        if (Vector3.Dot(myTarget.transform.position, transform.position) >= 0.5f)
        {
            return true;
        }
        return false;
    }

    public void AlphaChange(float _Value, float _Max)
    {
        Color color = transform.GetComponent<renderer>().material.color;
        color.a *= _Value;
        if (color.a > _Max)
            color.a = _Max;
        GetComponent<renderer>().material.color = color;
    }

    public void ResetState()
    {
        // 능력치 설정
        CurrentHP = Random.Range(SlimeHP, SlimeHP + HPRandValue);
        state = new StateMachine<slime>();
        // 초기 상태 설정
        state.Initial_Setting(this, State_Move.Instance);
        // 타겟 null 설정
        myTarget = null;
    }

    public void CreateItem( int _MaxPotion, int _MaxGold)
    {
        for(int i = 0 ; i < _MaxPotion ; i++)
        {
            Instantiate(Resources.Load("Prefab/HPPotion"), transform.position, Quaternion.identity);
        }

        for (int i = 0; i < _MaxGold; i++)
        {
            Instantiate(Resources.Load("Prefab/Gold"), transform.position, Quaternion.identity);
        }
    }


}


'Programming > Unity' 카테고리의 다른 글

Unity C# Zoom in out  (0) 2015.08.03
NGUI HUDText 사용  (2) 2015.07.30
NGUI HP Bar 만들기  (0) 2015.07.30
가상 조이스틱 구현  (1) 2015.07.30
유니티(unity) 디버깅 하기  (0) 2014.01.28