낑깡의 게임 프로그래밍 도전기

C# 상태패턴 본문

카테고리 없음

C# 상태패턴

낑깡겜플밍 2023. 11. 13. 21:42
반응형
SMALL

상태에 대한 것을 나눌때 상태패턴 전략적으로 나누면 전략패턴

상태머신. 유한한 상태머신 유한상태머신

W로 상태를 전이 시켰다.. 하나의 현재상태를 가지고 여러가지 상태로 전환할 수 있는 도구를 상태 머신이라고 한다

예제

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.VisualScripting.Dependencies.Sqlite.SQLite3;
using static UnityEngine.UI.GridLayoutGroup;

public enum STATE_TYPE
{
    IDLE,
    WALK,
    RUN
}

public class State
{
    public GameManager owner = null;

    public State(GameManager owner)
    {
        this.owner = owner;
    }
    public virtual void Update()
    {

    }
}
public class IdleState : State
{
    public IdleState(GameManager owner) : base(owner)
    {
    }

    public override void Update()
    {
        Debug.Log("대기상태");
        if (owner.MoveVec != Vector3.zero)
            owner.curState = new WalkState(owner);
    }
}

public class WalkState : State
{
    public WalkState(GameManager owner) : base(owner)
    {
    }

    public override void Update()
    {
        Debug.Log("걷기상태");
        if (owner.MoveVec == Vector3.zero)
            owner.curState = new IdleState(owner);
        if (Input.GetKeyDown(KeyCode.LeftShift))
            owner.curState = new RunState(owner);
    }
}

public class RunState : State
{
    public RunState(GameManager owner) : base(owner)
    {
    }

    public override void Update()
    {
        Debug.Log("달리기상태");
        if (owner.MoveVec == Vector3.zero)
            owner.curState = new IdleState(owner);
        if (Input.GetKeyUp(KeyCode.LeftShift))
            owner.curState = new WalkState(owner);
    }
}


public class GameManager : MonoBehaviour
{
    //현재 여기까지는 상태 머신.
    // STATE_TYPE curType = STATE_TYPE.IDLE;
    public State curState;
    Vector3 moveVec;
    public Vector3 MoveVec
    {
        get { return moveVec; }
    }

    private void Start()
    {
        curState = new IdleState(this);
    }

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        moveVec = new Vector3(x, 0, z).normalized;

        curState.Update();
        /*
        switch(curType)
        {
            case STATE_TYPE.IDLE:
                Debug.Log("대기상태");
                if(moveVec != Vector3.zero)
                    curType = STATE_TYPE.WALK;
                break;
            case STATE_TYPE.WALK:
                Debug.Log("걷기상태");
                if (moveVec == Vector3.zero)
                    curType = STATE_TYPE.IDLE;
                if (Input.GetKeyDown(KeyCode.LeftShift))
                    curType = STATE_TYPE.RUN;
                break;
            case STATE_TYPE.RUN:
                Debug.Log("달리기상태");
                if (moveVec == Vector3.zero)
                    curType = STATE_TYPE.IDLE;
                if (Input.GetKeyUp(KeyCode.LeftShift))
                    curType = STATE_TYPE.WALK;
                break;
        }
        */
        
    }
}

상태 패턴으로 바꾼것 아직은 아쉬운 상태패턴 얘들을 유연하게 관리해주는 매니저가 있으면 더 좋다

using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.VisualScripting.Dependencies.Sqlite.SQLite3;

public enum STATE_TYPE
{
    IDLE,
    WALK,
    RUN
}
public class State//유징스페이스 잘못쓰면 스테이트라고하는 다른놈이 들어오므로 주의
{
    public StateMachine sm = null; //이제는 머신이 관리할 것이라 바꿔즘
    public virtual void Enter() 
    {
        Debug.Log(GetType().Name + "상태진입");//내타입을  가지고 오는 것
    }
    public virtual void Update()
    {
    }
    public virtual void Exit()
    {
        Debug.Log(GetType().Name + "상태빠져나감");
    }
}
public class IdleState : State
{
    public override void Update()
    {
        Debug.Log("대기상태");
        if (sm.owner.MoveVec != Vector3.zero)
            sm.SetState("Walk");
        if(Input.GetKeyDown(KeyCode.V))
            sm.SetState("Attack");
    }
}
public class WalkState : State
{
    public override void Update()
    {
        Debug.Log("걷기상태");
        if (sm.owner.MoveVec == Vector3.zero)
            sm.SetState("Idle");
        if (Input.GetKeyDown(KeyCode.LeftShift))
            sm.SetState("Run");
    }
}
public class RunState : State
{
    public override void Update()
    {
        Debug.Log("달리기상태");
        if (sm.owner.MoveVec == Vector3.zero)
            sm.SetState("Idle");
        if (Input.GetKeyUp(KeyCode.LeftShift))
            sm.SetState("Walk");
    }
}
public class AttackState : State
{
    public override void Enter()
    {
        base.Enter();
        Debug.Log("공격!");
        sm.SetState("Idle");
    }
}

public class StateMachine//보통 상태패턴을 제어하는 애 이름을 관용적으로 이렇게 지음
{
    //이제 소유주를 머신에서 체크한다
    public Player owner = null;
    public State curState;
    public Dictionary<string, State> stateDic;

    public StateMachine(Player owner)
    {
        this.owner = owner;
        stateDic = new Dictionary<string, State>();
    }
    public void AddState(string stateName, State state)
    {
        //여러개를 담으려면 콜렉션을 써야함. 딕셔너리를 써보겠음
        if (stateDic.ContainsKey(stateName))//ContainsKey 있는지 없는지 체크해보는거
            return;//이프이프 문 방지  리턴되는거 먼저 만들어 놓고 실행
        stateDic.Add(stateName, state);
        state.sm = this;
    }

    public void SetState(string stateName)
    {
        if (stateDic.ContainsKey(stateName))//예외처리
        {
            if(curState != null)
            {
                curState.Exit();
            }
            curState = stateDic[stateName];
            curState.Enter();
        }
    }
    public void Update()
    {
        curState.Update();
    }
}

public class Player : MonoBehaviour
{
    //public State curState;//위로뺌
    StateMachine sm;
    Vector3 moveVec;
    public Vector3 MoveVec
    {
        get { return moveVec; }
    }

    void Start()
    {
        sm = new StateMachine(this);
        sm.AddState("Idle", new IdleState());
        sm.AddState("Walk", new WalkState());
        sm.AddState("Run", new RunState());
        sm.AddState("Attack", new AttackState());
        sm.SetState("Idle");
    }

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        moveVec = new Vector3(x, 0, z).normalized;
        sm.Update();
    }
}

매니저를 이용해 상태패턴을 다시 구현한 것

이렇게 하면 나중에 예를 들어 플레이어를 추격하는 몬스터에게 다양한 상태를 부여할 수 있는데(추후 플레이어를 추격을 하지 않았을때 몬스터에게 다양한 상태를 주고 플때 등)  처음 만들때는 그 모든 상태를 고려하지 않아도 되는 등 확장성이 매우 높아진다.

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;


public class GMState
{
    public virtual void Update()
    {
    }
}
public class DefaultState : GMState
{
    public override void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("게임 시작합니다");
            GameManager.instance.curState = new LoadingState();
        }
    }
}
public class PauseState : GMState
{

    public override void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Debug.Log("일시정지 해제");
            GameManager.instance.curState = new PlayState();
        }
    }
}

public class PlayState : GMState
{
    public override void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Debug.Log("일시정지");
            GameManager.instance.curState = new PauseState();
        }
    }
}

public class LoadingState : GMState
{
    float targetTime = 3f;
    float curTime = 0;
    public override void Update()
    {
        curTime += Time.deltaTime;
        if(targetTime < curTime) 
        {
            GameManager.instance.curState = new PlayState();
            curTime = 0;
        }
    }
}

public class GameOverState : GMState
{
    public override void Update()
    {
        if (Input.GetKeyDown(KeyCode.F))
        {
            Debug.Log("다시시작");
            GameManager.instance.curState = new PlayState();
        }
        else if (Input.GetKeyDown(KeyCode.G))
        {
            Debug.Log("기본상태로");
            GameManager.instance.curState = new DefaultState();
        }
    }

}
public class GameManager : MonoBehaviour
{
    public static GameManager instance = null;
    //게임매니저는 싱글톤이기 때문에 생성자가 필요없을 수 있다.
    //일시정지, 플레이, 로딩중, 게임오버
    public GMState curState;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
            Destroy(gameObject);
    }
    void Start()
    {
        curState = new DefaultState();
    }

    // Update is called once per frame
    void Update()
    {
        curState.Update();
    }
}

좀 더 이해하기 쉬운 게임 상태변화 예시

 

where T는 범위를 좁혀준다 클래스로 좁혀줌

구조체는 null을 가르킬수없고 클래스는  null을 가르킬 수 있는 등의 차이 탓도 있다.

대충 상태머신 하나만들고 여기저기 다쓰고 싶을때 유용할 것 같다.

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

public interface IStateMachine
{
    object GetOwner();
    void SetState(string stateName);
}

public class StateMachine<T> : IStateMachine where T : class
{
    public T owner = null;
    public State curState;
    public Dictionary<string, State> stateDic;


    public StateMachine(T owner)
    {
        this.owner = owner;
        stateDic = new Dictionary<string, State>();
    }

    public void AddState(string stateName, State state)
    {
        if (stateDic.ContainsKey(stateName))
            return;
        stateDic.Add(stateName, state);
        state.Init(this);

    }

    public object GetOwner()
    {
        return owner;
    }

    public void SetState(string stateName)
    {
        if (stateDic.ContainsKey(stateName))
        {
            if (curState != null)
            {
                curState.Exit();
            }
            curState = stateDic[stateName];
            curState.Enter();

        }
    }
    public void Update()
    {
        curState?.Update();
    }

}

public class State
{
    public IStateMachine sm = null;

    public virtual void Init(IStateMachine sm)
    {
        this.sm = sm;
    }

    public virtual void Enter()
    {
        Debug.Log(GetType().Name + "상태 진입");
    }
    public virtual void Update()
    {

    }
    public virtual void Exit()
    {
        Debug.Log(GetType().Name + "상태 빠져나옴");
    }
}
public class GMState : State
{
    public GameManager gm;
    public event Action onEnter;

    public override void Enter()
    {
        base.Enter();
        if(onEnter != null) onEnter();
    }
    public override void Init(IStateMachine sm)
    {
        this.sm = sm;
        gm = (GameManager) sm.GetOwner();
    }
}

public class DefaultState : GMState
{
    public override void Update()
    {
        base.Update();
        if(Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("게임 시작합니다.");
            sm.SetState("Loading");//= new LoadingState();
        }
    }
}
public class PauseState : GMState
{
    public override void Update()
    {
        base.Update();
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Debug.Log("일시정지를 해제합니다.");
            sm.SetState("Play");//= new PlayState();
        }
    }
}
public class PlayState : GMState
{
    public override void Update()
    {
        base.Update();
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Debug.Log("일시정지합니다.");
            sm.SetState("Pause");
        }
    }
}

public class LoadingState : GMState
{
    float targetTime = 3f;
    float curTime = 0;
    public override void Update()
    {
        base.Update();
        curTime += Time.deltaTime;
        if(targetTime < curTime) 
        {
            sm.SetState("Play");
            curTime = 0;
        }
        Debug.Log("로딩중..." + curTime);
    }
}

public class GameOverState : GMState
{
    public override void Update()
    {

    }
}

public class GameManager : MonoBehaviour
{
    public static GameManager instance = null;
    public StateMachine<GameManager> sm;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
            Destroy(gameObject);
    }
    public GameObject obj;

    void Start()
    {
        sm = new StateMachine<GameManager>(this);
        PlayState ps = new PlayState();
        ps.onEnter += () => { obj.SetActive(true); };

        sm.AddState("Default", new DefaultState());
        sm.AddState("Play", ps);
        sm.AddState("Loading", new LoadingState());
        sm.AddState("Pause", new PauseState());
        sm.SetState("Default");
    }

    // Update is called once per frame
    void Update()
    {
        sm.Update();
    }
}

상태를 생각 할수 있는 기본형태

반응형
LIST