모름

//*_2020-05-17

상태패턴 조금 개선해서 업로드하였습니다. 링크 첨부합니다

 

올 인 원 유니티 상태패턴, 스테이트 패턴 (State Pattern) 코드 공유

설명 기존의 상태패턴에 다소 복잡함을 느껴 한 클래스에 상태패턴을 우겨넣어 구현해보았습니다. 유니티 상태패턴을 구글링해보면, 위와 같이 다소 복잡한 구조를 가집니다. 제가 머리가 안좋

morm.tistory.com

*//

 

이번 글에서는 복잡한 상태패턴 말고, 가볍고 심플하지만 유용한 상태패턴을 소개합니다. 간단한 FSM이 필요하시거나, 한 클래스에 상태를 다 넣어도 좋을만큼 가벼운 상태로직을 작성하실분께서 쓰시면 좋을듯합니다.

 

상태패턴을 구현하기 위해서 검색을 해보면 온통 초보자 입장에선 어렵고 복잡하게만 보입니다. 저도 상태패턴을 구현하기위해 찾다가 아래의 상태패턴을 사용해봤습니다. 매우 좋고 신박한 패턴이라고 생각했지만 상황에 맞춰서 사용해야하고 난이도가 조금 있었습니다.

 

저같은 경우 헤비한 상태 로직이 필요한게 아니었기 때문에 오히려 코드만 복잡해지고 관리하기 어려워지는걸 느꼈습니다. 물론 실력이 부족한게 90% 겠지만요 쿨럭 결국 다 갈아엎었습니다

 

(참고)아래는 제가 참고한 상태패턴 튜토리얼입니다. 상태패턴의 정석적인 구현에 관심있으시면 보시면 좋습니다.

 

State Pattern using Unity

Learn all about the Finite State Machine design pattern in Unity. Then implement it to control the movement of your own character!

www.raywenderlich.com

 

어쨌든 이번 글에선 위의 링크와 같은 복잡한 상태 패턴이 아닌 하나의 클래스에 상태를 몰아넣어 관리할수 있는 코드를 소개해드리겠습니다.

 

(좌)상태패턴 기본구조 (우)추상 상태 클래스 기본구조, 왼쪽 Behavior의 내부에 속함

 

기존의 상태패턴의 경우에 한 객체에 여러 개의 상태머신을 할당함으로서, 한 객체에게 여러 가지 상태패턴을 적용시킬수있습니다. 예를 들어 움직임을 담당하는 상태머신을 만들고, 공격수비같은 태세를 담당하는 상태머신을 만들면, 움직임과는 별개로 공격 수비를 담당하는 상태패턴을 별도로 관리할 수 있게 되는 것입니다.

 

(이것을 HFSM (계층적 유한 상태 머신) 이라고 부르는 듯 합니다. 정확한지 애매해서 일단 줄긋습니다)

 

하지만 위와 같은 계층적 상태가 필요한 경우가 아니라면, 상태 패턴을 사용하는 것은 너무 힘듭니다. 클래스만 해도 상태머신 클래스, 상태 추상 클래스, 객체 클래스, 각 상태별 클래스 등 너무 많은 클래스를 작성해야만 하기 때문입니다.

 

그래서 계층적 상태머신 표현이 필요없을 경우에 쓸 간단한 업데이트문에서 한 클래스 안에서 돌아가는 FSM을 소개해봅니다.

 

기존 상태패턴에서 상태의 구조
기존 상태패턴에서 상태머신의 구조

 

우선 위, 기존의 상태패턴을 보시면 Enter(), Update(), Exit() 크게 이 3가지의 구조로 상태를 들어왔다가 빠져나갑니다. 이것을 구현할 것이구요.

 

상태머신을 보면 ChangeState()라는 함수를 통해 상태를 바꿔주고 있습니다. 이 개념을 가져와서 또 구현해보겠습니다. 급하신 분은 아래 보시면 됩니다.

 

더보기
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Monster : MonoBehaviour
{
	public enum State
    {
        WALK,
        FLY,
        FALL,
        WIN,
        LOSE,
        DIE
    }
    public State state;

    private void Start(){
    	//체인지 스테이트에서 원하는 상태로 시작합니다
        ChangeState(State.state);
    }

    private void Update() {
        switch (state) {
            case State.WALK:
                Walk();
                break;
            case State.FLY:
                Fly();
                break;
            case State.FALL:
                Fall();
                break;
            case State.WIN:
                Win();
                break;
            case State.LOSE:
                Lose();
                break;
            case State.DIE:
                Die();
                break;
        }
    }

    private void OnTriggerStay(Collider other) {
        if (state == State.WALK) {
            WalkTriggerStay(other);
        }
    }

    private void OnTriggerEnter(Collider other) {
        switch (state) {
            case State.WALK:
                WalkTrigger(other);
                break;
            case State.FLY:
                FlyTrigger(other);
                break;
            case State.FALL:
                FallTrigger(other);
                break;
            case State.WIN:
                WinTrigger(other);
                break;
            case State.LOSE:
                LoseTrigger(other);
                break;
            case State.DIE:
                DieTrigger(other);
                break;
        }
    }

    private void ChangeState(State state) {
        //스테이트에서 나가기전에 마지막으로 실행되는 Exit()함수;
        switch (this.state) {
            case State.WALK:
                WalkExit();
                break;
            case State.FLY:
                FlyExit();
                break;
            case State.FALL:
                FallExit();
                break;
            case State.WIN:
                WinExit();
                break;
            case State.LOSE:
                LoseExit();
                break;
            case State.DIE:
                DieExit();
                break;
        }

        this.state = state;

        //스테이트에서 들어가고나서 처음으로 실행되는 Enter()함수;
        switch (state) {
            case State.WALK:
                WalkEnter();
                break;
            case State.FLY:
                FlyEnter();
                break;
            case State.FALL:
                FallEnter();
                break;
            case State.WIN:
                WinEnter();
                break;
            case State.LOSE:
                LoseEnter();
                break;
            case State.DIE:
                DieEnter();
                break;
        }
    }






    private void WalkEnter() {

    }
    private void Walk() {

    }
    private void WalkTrigger(Collider other) {

    }
    private void WalkTriggerStay(Collider other) {

    }
    private void WalkExit() {

    }




    private void FlyEnter() {
        throw new NotImplementedException();
    }
    private void Fly() {
        throw new NotImplementedException();
    }
    private void FlyTrigger(Collider other) {
        throw new NotImplementedException();
    }
    private void FlyExit() {
        throw new NotImplementedException();
    }




    private void FallEnter() {
        throw new NotImplementedException();
    }
    private void Fall() {
        throw new NotImplementedException();
    }
    private void FallTrigger(Collider other) {
        throw new NotImplementedException();
    }
    private void FallExit() {
        throw new NotImplementedException();
    }




    private void WinEnter() {
        throw new NotImplementedException();
    }
    private void Win() {
        throw new NotImplementedException();
    }
    private void WinTrigger(Collider other) {
        throw new NotImplementedException();
    }
    private void WinExit() {
        throw new NotImplementedException();
    }




    private void LoseEnter() {
        throw new NotImplementedException();
    }
    private void Lose() {
        throw new NotImplementedException();
    }
    private void LoseTrigger(Collider other) {
        throw new NotImplementedException();
    }
    private void LoseExit() {
        throw new NotImplementedException();
    }




    private void DieEnter() {
        throw new NotImplementedException();
    }
    private void Die() {
        throw new NotImplementedException();
    }
    private void DieTrigger(Collider other) {
        throw new NotImplementedException();
    }
    private void DieExit() {
        throw new NotImplementedException();
    }
}

 

 

	public enum State
    {
        WALK,
        FLY,
        FALL,
        WIN,
        LOSE,
        DIE
    }
    public State state = State.WALK;

먼저 상태를 선언합니다.

 

 

    private void Update() {
        switch (state) {
            case State.WALK:
                Walk();
                break;
            case State.FLY:
                Fly();
                break;
            case State.FALL:
                Fall();
                break;
            case State.WIN:
                Win();
                break;
            case State.LOSE:
                Lose();
                break;
            case State.DIE:
                Die();
                break;
        }
    }

그리고 업데이트 문에서 스위치로 상태별 함수를 만듭니다.

 

 

    private void ChangeState(State state) {
        //스테이트에서 나가기전에 마지막으로 실행되는 Exit()함수;
        switch (state) {
            case State.WALK:
                WalkExit();
                break;
            case State.FLY:
                FlyExit();
                break;
            case State.FALL:
                FallExit();
                break;
            case State.WIN:
                WinExit();
                break;
            case State.LOSE:
                LoseExit();
                break;
            case State.DIE:
                DieExit();
                break;
        }

        this.state = state;

        //스테이트에서 들어가고나서 처음으로 실행되는 Enter()함수;
        switch (state) {
            case State.WALK:
                WalkEnter();
                break;
            case State.FLY:
                FlyEnter();
                break;
            case State.FALL:
                FallEnter();
                break;
            case State.WIN:
                WinEnter();
                break;
            case State.LOSE:
                LoseEnter();
                break;
            case State.DIE:
                DieEnter();
                break;
        }
    }

그리고 ChangeState()라는 함수를 만들어 각각 상태별로 진입함수와 탈출함수를 작성해줍니다. 탈출하고 중간에 상태가 바뀌고 진입함수가 실행되기 때문에 항상 ChangeState() 를 통해 상태를 변경해주셔야합니다.

 

 

    private void WalkEnter() {
		//진입시 속성을 설정합니다
    }
    private void Walk() {
    	//업데이트문에서 이동을 시킵니다
        transform.position += currentDir * dirNo * speed * Time.deltaTime;
    }
    private void WalkTrigger(Collider other) {
        //OnTrigger도 스위치로 쪼개서 상태별로 작동방식을 다르게 달아줄수있습니다
    }
    private void WalkTriggerStay(Collider other) {
        //추가적으로 OnCollision, TrigerStay등도 스위치와 함수를 추가해주시면 깔끔하게 관리가능
    }
    private void WalkExit() {
		//상태 종료시 속성을 설정합니다
    }

그리고 위에서 작성한 함수를 아래쪽에 상태별 함수별로 모아주면 각 상태별 로직을 한 곳에서 깔끔하게 관리할수있습니다. 추가적으로 함수 작성이 필요하다면 상태별 로직박스 지역에 함수를 배치해줌으로써 관리하기도 쉽습니다.

 

이로써 기존의 상태패턴이 가지는 Enter(), Update(), Exit()ChangeState()라는 핵심 함수를 하나의 클래스에서 관리할수있게 됐습니다. 거기다가 유니티 콜백함수인 OnTrigger, OnCollision등도 쉽게 상태별로 작성이 가능합니다.

 

또한 계층적 상태머신이 필요하다면 위와같은 클래스를 하나 더 작성하여 같은 객체에게 붙여주면 되는 것이겠죠. 기존의 상태패턴을 이용했다면 최소 5개 이상의 클래스가 필요한 것을 하나의 클래스에서 깔끔하게 관리할수있게됐습니다. 이상입니다.

 

 

...

 

 

사실 별다른 코드도 아니고 고급적인 문법도 없지만, 나름 초보자가 삽질해가면서 정리한 상태패턴입니다... 많은 도움이 되셨으면 좋겠습니다. 감사합니다.

 

코드의 비용문제에 대해서는 제가 지식이 얕아 잘 알지 못합니다. 딱히 뭐 문제는 없어보이지만요.