4 분 소요

Intro

이번 포스팅에서는 몬스터 AI를 상태패턴을 활용하여 구현해보려합니다.

몬스터는 모든 몬스터가 공통적으로 가지는 데이터를 관리할 Monster 클래스를 개별 몬스터가 상속받고,

개별 몬스터 클래스는 StateMachine을 통해 상태에 따른 동작을 수행하도록 설계했습니다.

image

상태구현

먼저 몬스터가 가질 여러가지 상태들의 기초가되는 BaseState 클래스를 만들고,

만들고자하는 상태는 BaseState를 상속받아 각 상태에서의 동작을 세부 구현합니다.

이때, BaseState는 Monster 클래스를 상속받은 다양한 몬스터의 데이터를받아 초기화하기위해 제네릭으로 만들었습니다.

image

BaseState.cs

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

/*
                BaseState : 몬스터가 가지는 상태 구현을 위한 추상클래스

        * 개별 몬스터는 Monster 클래스를 상속받아 구현
            => 제네릭으로 구현하여 여러 몬스터들을 받을 수 있게 구현

        - OnStateEnter() : 상태에 처음 진입했을 때 한 번만 호출(초기설정)
        - OnStateUpdate() : 매 프레임마다 호출
        - OnStateExit() : 상태 변경시 호출(마무리작업)
 */

public abstract class BaseState<T> where T : Monster
{
    protected T monster;
   
    protected BaseState(T monster)
    {
        this.monster = monster;
    }
    
    public abstract void OnStateEnter();
    public abstract void OnStateUpdate();
    public abstract void OnStateExit();
}

IdleState.cs

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

/*
                Monster State - Idle (기본상태)
        
           - OnStateEnter : 기본상태 애니메이션 설정
 */
public class IdleState<T> : BaseState<T> where T : Monster
{
    public IdleState(T monster) : base(monster) { }

    public override void OnStateEnter()
    {

    }

    public override void OnStateUpdate()
    {

    }

    public override void OnStateExit()
    {
        
    }
}

ChaseState.cs

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

/*
                Monster State - Chase (추격상태)

        - OnStateEnter : 추격 애니메이션 설정
 */
public class ChaseState<T> : BaseState<T> where T : Monster
{
    public ChaseState(T monster) : base(monster) { }

    public override void OnStateEnter()
    {

    }

    public override void OnStateUpdate()
    {

    }

    public override void OnStateExit()
    {

    }
}

AttackState.cs

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

/*
                Monster State - Attack (공격상태)

        - OnStateEnter : 공격 애니메이션 설정
 */

public class AttackState<T> : BaseState<T> where T : Monster
{
    public AttackState(T monster) : base(monster) { }

    public override void OnStateEnter()
    {

    }

    public override void OnStateUpdate()
    {

    }

    public override void OnStateExit()
    {

    }
}

DieState.cs

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

/*
                Monster State - Die (죽음상태)

        - OnStateEnter : 죽음 애니메이션 설정
 */

public class DieState<T> : BaseState<T> where T : Monster
{
    public DieState(T monster) : base(monster) { }

    public override void OnStateEnter()
    {
        
    }

    public override void OnStateUpdate()
    {
        
    }

    public override void OnStateExit()
    {
        
    }
}

총 4가지 상태(Idle, Chase, Attack, Die)를 만들었습니다.

각 상태 클래스에서는 세가지 함수를 구현해주어야합니다.

  1. 상태에 진입했을 때 작업 : OnStateEnter()
  2. 해당 상태에서 매 프레임마다 수행할 작업 : OnStateUpdate()
  3. 상태를 빠져나갈 때 작업 : OnStateExit()

이때, 개별 상태클래스는 자신이 수행해야할 작업만 구현해주면되며, 상태의 변경에 관해서는 구현할필요도 알필요도없습니다.

상태 변경의 처리는 몬스터 상태를 관리할 StateMachine에서 수행합니다.

Monster.cs

Monster 클래스는 몬스터가 공통적으로 가지는 데이터를 관리할 클래스입니다.

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

/*
            Monster 클래스는 몬스터의 공통데이터를 관리

 */
public class Monster : MonoBehaviour
{
    #region **Monster Stats**
    [Header("#Monster Stats")]
    public int maxHp;
    public int curHp;
    public int maxMp;
    public int curMp;
    public float speed;
    #endregion


    protected PlayerController targetPlayer;        // 타깃 플레이어
    private BoxCollider hitBoxCol;                  // 몬스터 히트박스
    private Animator anim;                          // 몬스터 애니메이터

    public Animator Anim => anim;

    protected void Awake()
    {
        Init();
    }

    private void Init()
    {
        targetPlayer = GameManager.Instance.player;
        anim = GetComponent<Animator>();
        hitBoxCol = GetComponent<BoxCollider>();
    }
}
  • 몬스터의 체력, 마나, 이동속도등의 데이터를 가지며 Collider와 같은 컴포넌트를 초기화합니다.

TurtleShell.cs

TurtleShell는 제가만든 개별 몬스터로, 개별 몬스터가 가질 상태, Stats, 상태변경 조건등을 구현합니다.

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

/*
                    Monster - TurtleShell
        - 가지는 상태 : Idle, Chase, Attack, Die
 */
public class TurtleShell : Monster
{
    // TurtleShell이 가지는 상태
    private enum States
    {
        Idle,
        Chase,
        Attack,
        Die
    }

    private States curState;                             // 현재 상태
    private StateMachine<TurtleShell> stateMachine;

    private void Awake()
    {
        // 부모(Monster)의 초기화
        base.Awake();
        InitValues();
    }

    private void InitValues()
    {
        // TurtleShell 능력치 초기화
        maxHp = 100;
        maxMp = 100;
        curHp = maxHp;
        curMp = maxMp;
        speed = 5f;

        // 초기상태는 Idle
        curState = States.Idle;
        // StateMachine 객체 생성(Idle상태)
        stateMachine = new StateMachine<TurtleShell>(new IdleState<TurtleShell>(this));
    }
    
    // 상태 변경
    private void ChangeState(States nextState)
    {
        curState = nextState;
        switch(curState)
        {
            case States.Idle:
                stateMachine.ChangeState(new IdleState<TurtleShell>(this));
                break;
            case States.Chase:
                stateMachine.ChangeState(new ChaseState<TurtleShell>(this));
                break;
            case States.Attack:
                stateMachine.ChangeState(new AttackState<TurtleShell>(this));
                break;
            case States.Die:
                stateMachine.ChangeState(new DieState<TurtleShell>(this));
                break;
        }
    }
    // 상태 구현에 필요한 함수들..
}
  • TurtleShell 이 가질 수 있는 상태는 열거형으로 선언하였고, InitValues 함수에서 몬스터의 능력치와 초기상태, StateMachine 객체를 생성 및 Idle상태로 초기화합니다.

  • 부모인 Monster 클래스에서 컴포넌트를 초기화하므로 base.Awake()를 통해 StateMachine 객체를 생성하기전 필요한 작업을 먼저 수행합니다.

  • 나중에 Update() 함수에서 상태변경을 위한 여러 조건들을 걸고, ChangeState() 호출을 통해 다른상태로의 변경을 수행할 수 있습니다.

StateMachine.cs

StateMachine 클래스는 몬스터의 상태를 관리하는 역할을 수행합니다.

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

/*
            StateMachine은 Monster의 AI 상태를 관리
 */

public class StateMachine<T> where T : Monster
{
    private BaseState<T> curState;

    public StateMachine(BaseState<T> init)
    {
        // 현재상태 초기화
        curState = init;
        curState.OnStateEnter();
    }

    // 상태 변경
    public void ChangeState(BaseState<T> nextState)
    {
        // 다른상태로만 변경
        if (nextState == curState)
            return;

        if (curState != null)
            curState.OnStateExit();

        // 현재상태를 nextState로 설정
        curState = nextState;
        curState.OnStateEnter();
    }

    public void UpdateState()
    {
        if (curState != null)
            curState.OnStateUpdate();
    }
}
  • StateMachine 객체를 생성할 때 초기 상태를 가지고 초기화하며

  • ChangeState() 함수는 현재상태를 종료하고, 다음 상태로의 변경을 수행합니다.

TurtleShell 몬스터 만들어보기

에셋 스토어에서 무료로 배포중인 에셋을 사용하여 몬스터를 만들어보았습니다.

image


TurtleShell 오브젝트에 필요한 애니메이터 컨트롤러 및 BoxCollider를 설정했습니다.

image


TurtleShell 오브젝트에 TurtleShell 스크립트를 붙여줍니다.

확인을 위해 IdleState의 OnStateEnter 함수에서 Walk 애니메이션을 동작하도록 해보았습니다.

image

댓글남기기