관리 메뉴

개발블로그

[유니티 C#] 유한 상태머신(Finite State Machine, FSM) 본문

Unity/스크립팅

[유니티 C#] 유한 상태머신(Finite State Machine, FSM)

dvmoo 2021. 10. 13. 12:20

💪 GOAL

- 유한 상태머신의 뜻을 이해한다.

- 유한 상태머신을 이용한 캐릭터 애니메이션을 구현한다.

 


게임을 개발할 때 '유한 상태머신(FSM)'이라는 말을 한번쯤은 들어봤을 것이다.

유한 상태머신은 유한한 수의 상태가 존재하며, 한 번에 한 상태만 '현재 상태'가 되도록 프로그램을 설계하는 모델이다.

유한 상태 머신에서는 어떤 상태에서 다른 상태로 전이하여 현재 상태를 전환할 수 있다.

 

 

가장 기본적인 예로 게임의 Monster AI를 유한 상태머신으로 구현한다고 가정해보자.

그림 1 Monster AI의 상태도

 

가장 기본적으로 몬스터는 Idle(대기), Trace(추적), Attack(공격) 로 세가지 상태를 가질 수 있다.

 

화면에서 대기하고 있다가, 플레이어(공격대상)가 몬스터의 시야에 들어오면 추적하고, 사정거리에 있다면 공격한다.

만약 시야에서 벗어나면 다시 대기상태로 변화하고, 사정거리에 들어오면 공격하는 것으로 상태머신을 구현할 수 있다.

 

 

🔶 애니메이터(Animator)

그림 1 애니메이터

인공지능 몬스터 AI를 만든다고 가정했을 때,  애니메이터에서 애니메이션을 추가해서 각각 연결해준다.

몬스터는 처음엔 대기상태였다가 시야에 공격 대상이 들어오면 공격 대상을 향해 걸어간다. 그리고 공격을 당하면 죽는다.

이 때, 애니메이션 상태는 Make Transition으로 이어져있어야 상황에 따라 애니메이션이 전환 된다.

 

그림 2 파라미터

 

그림 3

 

Die 라는 이름의 Trigger 파라미터를 생성 후, AnyState에서 Die로 가는 TransitionDie 파라미터를 Conditions으로 추가했다. 

이렇게만 해서는 몬스터는 죽음 상태로 가지않는다. 

 

스크립트 작업이 필요하다.

using Photon.Pun;
using System;
using UnityEngine;

public class Entity : MonoBehaviourPun, IDamageable
{
    public bool dead { get; protected set; }
    public event Action onDeath;

    [PunRPC]
    public virtual void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal)
    {
        if(PhotonNetwork.IsMasterClient)
        {
            health -= damage;
            
            photonView.RPC("ApplyUpdateHealth", RpcTarget.Others, health, dead);        // 호스트에서 클라이언트로 동기화
            photonView.RPC("OnDamage", RpcTarget.Others, damage, hitPoint, hitNormal);
        }
       
        if(health <= 0 && !dead)
        {
            Die();
        }
    }

  public virtual void Die()
  {
        if(onDeath != null)
        {
            onDeath();
        }

        dead = true;
  }
}
using System.Collections;
using UnityEngine;
using UnityEngine.AI;

public class Monster : Entity
{
	NavMeshAgent pathFinder; 

   public override void Die()
    {
        base.Die();

        Collider[] MonsterColliders = GetComponents<Collider>();
        
        for(int i = 0; i < MonsterColliders.Length; i++)
        {
            MonsterColliders[i].enabled = false;
        }

        pathFinder.isStopped = true;
        pathFinder.enabled = false;

        MonsterAnim.SetTrigger("Die");
        MonsterAudioPlayer.PlayOneShot(deathSound);
    }
}

-현재 설명에서 필요없는 부분은 생략하고 코드 일부만 가져옴-

 

Entity 클래스에서 OnDamage함수에서 Damage를 입었을 때,

만약 health 가 0보다 작거나 같고, dead가 true이면 Die 메소드를 실행한다.

Monster클래스에서 Die메소드에서 몬스터의 활성화된 모든 콜라이더를 해제시키고

몬스터의 NavMeshAgent를 멈추고, 해제시킨 뒤

 

MonsterAnim.SetTrigger("Die"); 로 상태를 전이 시킨다.

이렇게 되면 몬스터의 애니메이션은 공격을 받은 후 체력이 0이 되어 죽은 상태가 true가 되면 Die 상태로 변경된다.

 

 

 

다른 방법으로는 enum문을 사용하는 방법이 있다.

enum SKILLSTATE
{
    READY, 
    CREATE, 
    LAUNCH, 
    ARRIVED, 
    DESTROY, 
    DEACTIVATE
}
public class SkillController : MonoBehaviour
{
    [SerializeField] List<Skill> skills;
    
     SKILLSTATE skillState = SKILLSTATE.READY;
    
    void ChangeState(SKILLSTATE state)
    {
        switch(state)
        {
            case SKILLSTATE.READY: 
                break;
            case SKILLSTATE.CREATE: 
                break;
            case SKILLSTATE.LAUNCH: 
                break;
            case SKILLSTATE.ARRIVED: 
                break;
            case SKILLSTATE.DESTROY:
                break;
            case SKILLSTATE.DEACTIVATE:
                break;
            default:
                break;
        }
    }
}

이런 식으로 switch-case 문을 이용해서 상태별 동작을 정해줄 수 도 있다.

 

 

 


* 공부하는 단계입니다. 잘못된 부분이 있다면 피드백 부탁드립니다☺

* e-mail : heehee970@naver.com