ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 게임 프로그래밍 패턴 Part 2 디자인 패턴 다시보기 - 상태
    보관함 2020. 3. 23. 19:40

    게임 프로그래밍 패턴로버트 나이스트롬 (Robert Nystrom)
    상세보기

    이 시리즈는 [게임 프로그래밍 패턴]에 등장하는 팁을 정리하고 패턴을 직접 구현하거나 구현되어 있는 패턴을 확인하는 것으로 해당 패턴에 대해 이해하는 것을 목표로 한다.

     

    이번 포스팅에서는 Part 2의 여섯 번째 패턴인 상태 패턴을 살펴보고 상태 패턴을 이용해 간단한 플랫포머 게임을 만들어 보는 것을 목표로 한다.


    상태 패턴이란?)

    [GoF디자인 패턴]에서는 상태 패턴을 다음과 같이 소개했다.

    객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다.

    내가 이해한 내용으로 설명하면 상태패턴은 FSM에 OOP를 적용한 것이라고 할 수 있겠다.

    즉, 상태 패턴을 이해하기 위해서는 먼저 FSM을 알아야 한다.

     

    FSM이란?)

    FSM(Finite State Macine)은 오토마타 이론에서 나왔으며 이것의 요점은 다음과 같다.

     

        1. 가질 수 있는 '상태'가 한정된다.

        2. 한 번에 '한 가지' 상태만 될 수 있다.

             : 서 있는 상태이면서 동시에 뛰는 상태가 될 수 없다. 이것은 FSM을 사용하는 이유 중 하나다.

        3. '입력'이나 '이벤트'가 기계에 전달된다.

        4. 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다.

             : 입력에 대해 현재 상태에서 다른 상태로의 전이가 존재하면 다음 상태로 변경한다.

     

    FSM을 이용한 캐릭터 만들기)

    기본적인 테스트씬을 만들어 진행했으며 진행 환경은 다음과 같이 바닥을 위한 큐브, 캐릭터를 위한 캡슐을 사용했다.

     

    플레이어용 캡슐엔 다음 스크립트를 추가했으며 간단하게 Stand, Walk, Jump, Jumping 상태를 가지는 캐릭터를 만들었다.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(Rigidbody))]
    public class PlayerControl : MonoBehaviour
    {
    	public enum State
    	{
    		Stand,
    		Walk,
    		Jump,
    		Jumping
    	}
    
    	[SerializeField]
    	private float m_jumpPower = 10.0f;
    	[SerializeField]
    	private float m_moveSpeed = 20.0f;
    
    	private State m_curState = State.Stand;
    	private Rigidbody myRigidbody;
    	private Transform myTransform;
    
    	private bool m_isOnGround;
    	private bool IsOnGround
    	{
    		get
    		{
    			if (m_isOnGround)
    			{
    				m_isOnGround = false;
    				return true;
    			}
    			else
    			{
    				return false;
    			}
    		}
    	}
    
    	private void Start()
    	{
    		myRigidbody = GetComponent<Rigidbody>();
    		myTransform = transform;
    	}
    
    	void Update()
    	{
    		float h = Input.GetAxis("Horizontal");
    		float v = Input.GetAxis("Vertical");
    		bool isJump = Input.GetKeyDown(KeyCode.Space);
    		HandleInput(h, v, isJump);
    	}
    
    	void HandleInput(float h, float v, bool isJump)
    	{
    		switch (m_curState)
    		{
    			case State.Stand:
    				if (isJump)
    				{
    					m_curState = State.Jump;
    				}
    				else if (h != 0 || v != 0)
    				{
    					m_curState = State.Walk;
    				}
    				break;
    			case State.Walk:
    				if (isJump)
    				{
    					m_curState = State.Jump;
    				}
    				else if (h == 0 && v == 0)
    				{
    					m_curState = State.Stand;
    				}
    				Move(h, v);
    				break;
    			case State.Jump:
    				myRigidbody.AddForce(myTransform.up * m_jumpPower);
    				m_curState = State.Jumping;
    				Move(h, v);
    				break;
    			case State.Jumping:
    				if (IsOnGround)
    				{
    					if (h != 0 || v != 0)
    					{
    						m_curState = State.Walk;
    					}
    					else
    					{
    						m_curState = State.Stand;
    					}
    				}
    				Move(h, v);
    				break;
    		}
    	}
    
    	void Move(float h, float v)
    	{
    		myTransform.position += myTransform.right * h * m_moveSpeed * Time.deltaTime;
    		myTransform.position += myTransform.forward * v * m_moveSpeed * Time.deltaTime;
    	}
    
    	void OnCollisionStay(Collision collision)
    	{
    		m_isOnGround = true;
    	}
    }​

    특정 상태에서 다른 상태로의 전이가 가능하고 해당 상태에서 업데이트 진행 시 특정 기능을 하도록 했다.

     

    상태 패턴 적용)

    상태 패턴은 위의 FSM에 OOP를 도입한 것으로 이해하면 된다. 먼저, PlayerControl을 다음과 같이 변경했다.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    using State = PlayerState.State;
    
    [RequireComponent(typeof(Rigidbody))]
    public class PlayerControl : MonoBehaviour
    {
    	[SerializeField]
    	private float m_jumpPower = 10.0f;
    	[SerializeField]
    	private float m_moveSpeed = 20.0f;
    
    	private State m_preState = State.Stand;
    	private State m_curState = State.Stand;
    	private Rigidbody myRigidbody;
    	private Transform myTransform;
    
    	private List<PlayerState> m_stateList = new List<PlayerState>();
    	private PlayerState m_state;
    
    	private bool m_isOnGround;
    	private bool IsOnGround
    	{
    		get
    		{
    			if (m_isOnGround)
    			{
    				m_isOnGround = false;
    				return true;
    			}
    			else
    			{
    				return false;
    			}
    		}
    	}
    
    	private void Start()
    	{
    		m_stateList.Add(new StandState());
    		m_stateList.Add(new WalkState());
    		m_stateList.Add(new JumpState());
    		m_stateList.Add(new JumpingState());
    
    		myRigidbody = GetComponent<Rigidbody>();
    		myTransform = transform;
    
    		foreach(var state in m_stateList)
    		{
    			state.ApplyOptions(myTransform, myRigidbody, m_moveSpeed, m_jumpPower);
    		}
    		m_state = m_stateList[(int)m_curState];
    	}
    
    	void Update()
    	{
    		float h = Input.GetAxis("Horizontal");
    		float v = Input.GetAxis("Vertical");
    		bool isJump = Input.GetKeyDown(KeyCode.Space);
    		m_curState = m_state.HandleInput(h, v, isJump, IsOnGround);
    		if(m_preState != m_curState)
    		{
    			m_preState = m_curState;
    			m_state = m_stateList[(int)m_curState];
    		}
    	}
    
    	void OnCollisionStay(Collision collision)
    	{
    		m_isOnGround = true;
    	}
    }

     

    그리고 플레이어의 상태를 저장하기 위한 상위 클래스를 생성했다.

    using UnityEngine;
    
    public abstract class PlayerState
    {
    	public enum State
    	{
    		Stand,
    		Walk,
    		Jump,
    		Jumping
    	}
    
    	protected Transform myTransform;
    	protected Rigidbody myRigidbody;
    	protected float m_moveSpeed;
    	protected float m_jumpPower;
    
    	public void ApplyOptions(Transform tr, Rigidbody rb, float speed, float jump)
    	{
    		myTransform = tr;
    		myRigidbody = rb;
    		m_moveSpeed = speed;
    		m_jumpPower = jump;
    	}
    
    	public abstract State HandleInput(float h, float v, bool isJump, bool onGround);
    
    	protected void Move(float h, float v)
    	{
    		myTransform.position += myTransform.right * h * m_moveSpeed * Time.deltaTime;
    		myTransform.position += myTransform.forward * v * m_moveSpeed * Time.deltaTime;
    	}
    }

     

    그리고 이전에 enum으로 분리했던 상태를 위의 PlayerState를 상속받는 클래스로 만들어 HandleInput을 구현하도록 처리했다.

    using System.Collections;
    using System.Collections.Generic;
    
    public class StandState : PlayerState
    {
        public override State HandleInput(float h, float v, bool isJump, bool onGround)
        {
    		if (isJump)
    		{
    			return State.Jump;
    		}
    		else if (h != 0 || v != 0)
    		{
    			return State.Walk;
    		}
    
    		return State.Stand;
    	}
    }
    using System.Collections;
    using System.Collections.Generic;
    
    public class WalkState : PlayerState
    {
        public override State HandleInput(float h, float v, bool isJump, bool onGround)
        {
    		if (isJump)
    		{
    			return State.Jump;
    		}
    		else if (h == 0 && v == 0)
    		{
    			return State.Stand;
    		}
    		Move(h, v);
    
    		return State.Walk;
    	}
    }
    using System.Collections;
    using System.Collections.Generic;
    
    public class JumpState : PlayerState
    {
        public override State HandleInput(float h, float v, bool isJump, bool onGround)
        {
    		myRigidbody.AddForce(myTransform.up * m_jumpPower);
    		Move(h, v);
    		return State.Jumping;
    	}
    }
    using System.Collections;
    using System.Collections.Generic;
    
    public class JumpingState : PlayerState
    {
        public override State HandleInput(float h, float v, bool isJump, bool onGround)
        {
    		if (onGround)
    		{
    			if (h != 0 || v != 0)
    			{
    				return State.Walk;
    			}
    			else
    			{
    				return State.Stand;
    			}
    		}
    		Move(h, v);
    		return State.Jumping;
    	}
    }

     

    실행해보면 기존과 동일하게 잘 움직이는 것을 확인할 수 있다.

    댓글

Designed by Tistory.