[UNITY],[C#]/TIL : UNITY

[디자인패턴] 상태패턴

네,가능합니다 2024. 10. 29. 18:12

어제도 디자인 패턴에 대한것을 알아봤는데, 어제 전략패턴을 공부하고 상태패턴까지 공부를 할 생각이었기에 상태패턴에 관한 내용을 작성하겠다.

 

상태패턴은 말 그대로 객체의 상태에 따라 동작을 정의하는 패턴이다.

 

코드들을 보면서 코드의 흐름을 따라가보자.

 

public interface IPlayerState
{
    void EnterState(PlayerController player);
    void UpdateState(PlayerController player);
    void ExitState(PlayerController player);
}

 

 

상태 인터페이스를 구현했다.

이 상태 인터페이스를 상속받는 idle, move, jump를 구현해보자

 

public class IdleState : IPlayerState
{
    public void EnterState(PlayerController player)
    {
        //Idle상태에서 필요한 작업 ex) 휴식을시작하여 체력이 회복됩니다.
    }

    public void UpdateState(PlayerController player)
    {
    	//이동키 입력시 상태전환
        if (Input.GetKeyDown(KeyCode.W))
        {
            player.SwitchState(new MovingState());
        }
        //점프키 입력시 상태전환
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            player.SwitchState(new JumpingState());
        }
    }

    public void ExitState(PlayerController player)
    {
    	//체력회복 끄기
    }
}

public class MovingState : IPlayerState
{
    public void EnterState(PlayerController player)
    {
    
    }

    public void UpdateState(PlayerController player)
    {
        if (Input.GetKeyUp(KeyCode.W)) // W를 때면 idle로 전환요청
        {
            player.SwitchState(new IdleState());
        }
        else if (Input.GetKeyDown(KeyCode.Space)) // 점프키 입력시 이동중 jumping으로 전환요청
        {
            player.SwitchState(new JumpingState());
        }
    }

    public void ExitState(PlayerController player)
    {
    
    }
}

public class JumpingState : IPlayerState
{
    public void EnterState(PlayerController player)
    {
    
    }

    public void UpdateState(PlayerController player)
    {
        if (player.IsGrounded()) // 땅에 닿으면 Idle로 전환요청
        {
            player.SwitchState(new IdleState());
        }
    }

    public void ExitState(PlayerController player)
    {
    
    }
}

 

 

 

이런식으로 구성한다음 이 상태들을 관리해주는 컨트롤러 스크립트를 만들어준다면

아주 깔끔하고 유지보수가 매~~~~~~~우 용이한 스크립트가 완성된다.

 

public class PlayerController : MonoBehaviour
{
    private IPlayerState currentState;

    private void Start()
    {
        currentState = new IdleState(); // 초기상태설정
        currentState.EnterState(this);
    }

    private void Update()
    {
        currentState.UpdateState(this); // 현재 상태의 Update 메서드 호출
    }
	//상태전환 메서드 각 상태들에서 호출해서 사용하면 된다.
    public void SwitchState(IPlayerState newState)
    {
        currentState.ExitState(this);
        currentState = newState;
        currentState.EnterState(this);
    }

    public bool IsGrounded()
    {
    	// 아까 jumping상태의 IsGrounded()로 불값을 받아오는 로직
        return Physics.Raycast(transform.position, Vector3.down, 0.1f);
    }
}

 

 

이걸 왜 이렇게 복잡하게 만들어 ? 라는 생각이 든다면 이런저런 코드를 더 많이 작성하고 다시 보는것이 도움이 될것이다.

 

다음부터는 이런 상태패턴을 적용하여 플레이어의 상태를 구분하고 쉽게 수정할 수 있는 코딩을 해보도록 노력해봐야겠다.

 

--------------------------------------

 

업데이트

 

이렇게 상태패턴을 정리해보고 너무 마음에 들고 깔끔한 디자인 패턴이라 직접 적용해보았다.

 

내배캠에서 진행중인 개인 과제에서 적용했었는데, 디자인패턴들을 정리하던 중 나도 다시 한번 짚고 넘어가면 좋을 것 같아서 정리해보기로 했다

 

적용은 AI Navigation을 적용한 NPC에게 했으며, 해당 NPC는 Idle, Move, Talk 3가지 상태를 가지고 있다.

 

// 인터페이스 구현
public interface INPCState
{
    void EnterState(NPCController npc);
    void UpdateState(NPCController npc);
    void ExitState(NPCController npc);
}
// Idle
public class IdleState : INPCState
{	// 정해진 위치로 이동하는 함수 호출
    public void EnterState(NPCController npc)
    {
        npc.animator.SetFloat("Speed", 0f);
        npc.StartMoveToTarget();
    }

    public void UpdateState(NPCController npc)
    {
        npc.SwitchState(new MoveState());
    }

    public void ExitState(NPCController npc) { }
}

public class MoveState : INPCState
{
    public void EnterState(NPCController npc)
    {
        npc.animator.SetFloat("Speed", 1f);
    }

    public void UpdateState(NPCController npc)
    {	// 만약 도착하면
        if (npc.AtTarget())
        {	// 다음 이동위치를 정한 뒤
            npc.SetNextWaypoint();
            // 대화상태로 전환요청
            npc.SwitchState(new TalkState());
        }
    }

    public void ExitState(NPCController npc) { }
}

public class TalkState : INPCState
{
    private Coroutine talkCoroutine;

    public void EnterState(NPCController npc)
    {
        npc.animator.SetFloat("Speed", 0f);
        npc.animator.SetBool("IsTalk", true);
        // talk 코루틴 호출 (TalkRoutine참고) 3초후 Idle상태로 전환요청
        talkCoroutine = npc.StartCoroutine(TalkRoutine(npc));
    }

    public void UpdateState(NPCController npc)
    {

    }

    public void ExitState(NPCController npc)
    {
        npc.animator.SetBool("IsTalk", false);
        if (talkCoroutine != null)
        {
            npc.StopCoroutine(talkCoroutine);
            talkCoroutine = null;
        }
    }

    private IEnumerator TalkRoutine(NPCController npc)
    {
        yield return new WaitForSeconds(3f);
        npc.SwitchState(new IdleState());
    }
}

 

완벽하게 잘 사용한것인지는 사실 모르겠으나 여기서 NPC에게 원하는 행동이 몇가지 추가되거나 빼고싶어도 복잡하지 않고 간단하게 수정할 수 있을 것 같다.

 

NPCController는 아래

public class NPCController : MonoBehaviour
{
    public Animator animator;
    public Transform[] waypoints;
    private int currentWaypointIndex;
    private NavMeshAgent agent;
    private INPCState currentState;

    private void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator.speed = 1.0f;
        currentState = new IdleState();
        currentState.EnterState(this);
    }

    private void Update()
    {
        currentState.UpdateState(this);
    }

    public void SwitchState(INPCState newState)
    {
        currentState.ExitState(this);
        currentState = newState;
        currentState.EnterState(this);
    }

    public void StartMoveToTarget()
    {
        if (waypoints.Length > 0)
        {
            agent.SetDestination(waypoints[currentWaypointIndex].position);
        }
    }

    public bool AtTarget()
    {
        return !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance;
    }

    public void SetNextWaypoint()
    {
        currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length;
    }
}