odyssey

#4 odyssey 개발일지 : 상태 패턴으로 리팩토링 하기

san10 2023. 3. 13. 01:29

지난 4달동안 개발일지를 쓰지 않았다

개발일지를 쓰고 바로 여행+기말과제+기말고사를 치르고 나서 종강을 했고

종강을 하고나서 5일 뒤에 오리키우기를 출시했다

 

오리키우기를 출시하고 많은 피드백을 받았고..

방학동안은 오리키우기 개발에 집중했다.

 

오리키우기 다음은 뭘만들지 고민이 많았는데

그냥 odyssey 개발하던거 이어서 개발하기로 마음먹었다

어차피 마땅한 아이디어도 없었고

오리키우기와 장르가 다른만큼 배우는게 있을 것 같아서이다.

그래서 이번 1학기에는 odyssey 개발 완료하는것이 목표이다.

 

그래서 이번에는 기존의 PlayerController 스크립트에 상태 패턴을 적용해보았다.

 

 

상태 패턴이란?

디자인 패턴 중 하나로, 상태를 체크해서 특정 행위를 하는 것이 아니라,

상태 자체를 객체화 하는 것이다.

 

 

기존의 PlayerController 스크립트는 아래와 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;

public class PlayerController : MonoBehaviour
{
    Vector3 direction;
    public float rayDistance = 5f;
    Rigidbody2D rigid;
    public float jumpForce;
    public float doubleJumpForce;
    public float walkForce = 6;
    public float gravityForce = 5f;

    float playerAngle;
    private Vector3 velocity;
    private Vector3 gravity;
    Vector3 tmp;

    
    public AnimationCurve jumpAnimation;
    public float GoalTime;
    float t;
    float upVector;
    public bool isJump; // 점프중인지 체크
    public bool isDoubleJump;
    private RaycastHit2D slopeHit;

    private Transform childAngle;
    public int anglePos;
    
    void Start()
    {
        rigid = GetComponent<Rigidbody2D>();
        direction = transform.right;
        tmp = new Vector3(1, 0, 0);
        childAngle = transform.GetChild(0);
    }

    private Vector3 AdjustDirectionToSlope(Vector3 direction)
    {
        Vector3 adjustVelocityDirection = Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
        return adjustVelocityDirection;
    }

    private void rayDown()
    {
        slopeHit = Physics2D.Raycast(transform.position, Vector3.down, rayDistance, 1 << 6);
        //Debug.DrawRay(transform.position, Vector3.down * rayDistance, Color.red);
    }

    private void changeAngle()
    {
        if (slopeHit)
        {
            if (slopeHit.normal.x >= 0)
            {
                playerAngle = -Vector3.Angle(transform.up, slopeHit.normal);
            }
            else
            {
                playerAngle = Vector3.Angle(transform.up, slopeHit.normal);
            }
            transform.GetChild(0).localEulerAngles = new Vector3(0, 0, playerAngle);
        }

    }

    private void jumpCall()
    {
        if (Input.GetKeyDown(KeyCode.Space) && slopeHit && isDoubleJump == false)
        {
            isJump = true;
        }

        else if (Input.GetKeyDown(KeyCode.Space) && isJump == true && isDoubleJump == false)
        {
            isDoubleJump = true;
            isJump = false;
            t = 0;
        }

    }

    private void Move()
    {
        if (isJump == false&&isDoubleJump==false)
        {
            velocity = AdjustDirectionToSlope(direction);
            gravity = Vector3.down*gravityForce;
            rigid.velocity = velocity * walkForce + gravity;
        }
        
    }
    

    private void onChangeAngle()
    {
        if (!slopeHit)
        {
            if (Input.GetKey(KeyCode.Space))
            {
                childAngle.localEulerAngles += new Vector3(0, 0, 0.01f * anglePos);
            }
        }
    }

    
    private void jump()
    {
        if (isJump == true)
        {
            t += Time.deltaTime;
            if (t < GoalTime)
            {
                rigid.velocity = (Vector2)(velocity * walkForce + gravity) + new Vector2(0, 0.1f*jumpAnimation.Evaluate(t) *jumpForce);
                
            }
            else
            {
                t = 0;
                isJump = false;
            }
        }
    }

    private void doubleJump()
    {
        if (isDoubleJump == true)
        {
            t += Time.deltaTime;
            if (t < GoalTime)
            {
                rigid.velocity = (Vector2)(velocity * walkForce + gravity) + new Vector2(0, 0.1f * jumpAnimation.Evaluate(t) * doubleJumpForce);

            }
            else
            {
                t = 0;
                isDoubleJump = false;
            }
        }
    }


    void FixedUpdate()
    {
        rayDown();
        jump();
        doubleJump();
        Move();
        changeAngle();
        onChangeAngle();
    }
    void Update()
    {
        
        jumpCall();
    }
}

odyssey에서 플레이어가 할 수 있는 행위에는 이동, 점프, 이단점프, 회전 총 네가지의 상태가 있다.

지금까지는 모든 처리를 PlayerController에서 처리했다.

 

 

상태패턴을 적용

우선 상태들을 묶어줄 인터페이스를 작성했다.

public interface IPlayerState
{
    void Handle(PlayerController controller);
}

 

그리고 IPlayerState를 상속받는 상태(이동, 점프, 이단점프, 회전) 클래스를 작성했다.

 

이동

using UnityEngine;

public class PlayerMoveState : MonoBehaviour,IPlayerState
{
    private PlayerController playerController;

    public void Handle(PlayerController controller)
    {
        
        controller.rigid.velocity = controller.velocity * controller.walkForce + controller.gravity;
    }
}

 

점프

using UnityEngine;

public class PlayerJumpState : MonoBehaviour, IPlayerState
{
    private PlayerController playerController;

    public void Handle(PlayerController controller)
    {
        controller.time += Time.deltaTime;
        if (controller.time < controller.GoalTime)
        {
            controller.rigid.velocity = (Vector2)(transform.right * controller.walkForce + controller.gravity) + new Vector2(0, 0.1f * controller.jumpAnimation.Evaluate(controller.time) * controller.jumpForce);

        }
        else
        {
            controller.time = 0;
            controller.isJump = false;
        }
    }
}

 

이단점프

using UnityEngine;

public class PlayerDoubleJumpState : MonoBehaviour, IPlayerState
{
    private PlayerController playerController;

    public void Handle(PlayerController controller)
    {
        controller.time += Time.deltaTime;
        if (controller.time < controller.GoalTime)
        {
            controller.rigid.velocity = (Vector2)(transform.right * controller.walkForce + controller.gravity) + new Vector2(0, 0.1f * controller.jumpAnimation.Evaluate(controller.time) * controller.jumpForce);

        }
        else
        {
            controller.time = 0;
            controller.isDoubleJump = false;
        }
    }

}

 

회전

using UnityEngine;

public class PlayerRotateState : MonoBehaviour, IPlayerState
{
    private PlayerController playerController;

    public void Handle(PlayerController controller)
    {
        if (Input.GetKey(KeyCode.Space))
        {
            controller.childAngle.localEulerAngles += new Vector3(0, 0, 0.01f * controller.anglePos);
        }
    }
}

 

 

그리고 상태변화를 인식하고 상태를 전환해주는 Context 스크립트를 작성했다.

using UnityEngine;

public class PlayerStateContext : MonoBehaviour
{
    public PlayerStateContext(PlayerController controller)
    {
        playerController = controller;
    }
    public IPlayerState CurrentState { get;set;}
    private PlayerController playerController;


    public void Transition(IPlayerState state)
    {
        CurrentState = state;
        CurrentState.Handle(playerController);
    }
}

 

마지막으로 PlayerController이다.

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

public class PlayerController : MonoBehaviour
{
    public float rayDistance = 5f;
    public Rigidbody2D rigid;
    public float jumpForce;
    public float doubleJumpForce;
    public float walkForce = 6;
    public float gravityForce = 5f;
    public float time;
    public Vector3 velocity;
    public Vector3 gravity;

    float playerAngle;
    
    public AnimationCurve jumpAnimation;
    public float GoalTime;
    public bool isJump; // 점프중인지 체크
    public bool isDoubleJump;
    private RaycastHit2D slopeHit;

    public Transform childAngle;
    public int anglePos;


    private IPlayerState moveState, jumpState, doubleJumpState, rotateState;
    private PlayerStateContext playerStateContext;

    void Start()
    {
        gravity = Vector3.down * gravityForce;
        rigid = GetComponent<Rigidbody2D>();
        childAngle = transform.GetChild(0);
        velocity=transform.right;

        playerStateContext = new PlayerStateContext(this);
        moveState = gameObject.AddComponent<PlayerMoveState>();
        jumpState = gameObject.AddComponent<PlayerJumpState>();
        doubleJumpState = gameObject.AddComponent<PlayerDoubleJumpState>();
        rotateState = gameObject.AddComponent<PlayerRotateState>();
    }

    public void MovePlayer()
    {
        playerStateContext.Transition(moveState);
    }

    public void JumpPlayer()
    {
        playerStateContext.Transition(jumpState);
    }

    public void DoubleJumpPlayer()
    {
        playerStateContext.Transition(doubleJumpState);
    }

    public void RotatePlayer()
    {
        playerStateContext.Transition(rotateState);
    }

    public Vector3 AdjustDirectionToSlope(Vector3 direction)
    {
        Vector3 adjustVelocityDirection = Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
        return adjustVelocityDirection;
    }

    private void SetVelocity()
    {
        velocity = AdjustDirectionToSlope(velocity);
    }

    private void SetSlopeHit()
    {
        slopeHit = Physics2D.Raycast(transform.position, Vector3.down, rayDistance, 1 << 6);
        //Debug.DrawRay(transform.position, Vector3.down * rayDistance, Color.red);
    }

    private void SetJumpFlag()
    {
        if (Input.GetKeyDown(KeyCode.Space) && slopeHit && isDoubleJump == false)
        {
            isJump = true;
        }

        else if (Input.GetKeyDown(KeyCode.Space) && isJump == true && isDoubleJump == false)
        {
            isDoubleJump = true;
            isJump = false;
            time = 0;
        }
    }

    private void ChangePlayerAngle()
    {
        if (slopeHit)
        {
            if (slopeHit.normal.x >= 0)
            {
                playerAngle = -Vector3.Angle(transform.up, slopeHit.normal);
            }
            else
            {
                playerAngle = Vector3.Angle(transform.up, slopeHit.normal);
            }
            transform.GetChild(0).localEulerAngles = new Vector3(0, 0, playerAngle);
        }

    }

    private void PlayerState()
    {
        if (!isJump && !isDoubleJump)
        {
            MovePlayer();
        }
        if (!slopeHit)
        {
            RotatePlayer();
        }

        if (isJump)
        {
            JumpPlayer();
        }

        if (isDoubleJump)
        {
            DoubleJumpPlayer();
        }
    }

    void FixedUpdate()
    {
        SetSlopeHit();
        SetVelocity();
        ChangePlayerAngle();
        PlayerState();
    }

    void Update()
    {
        SetJumpFlag();
    }
}

원래는 PlayerController에서 4가지의 상태의 동작을 move(), jump()등의 함수로 구현했었는데,

지금은 각 상태를 캡슐화했다.

그 외에도 변수명이나 함수명을 좀더 명확하게 알아볼 수 있게 수정했다.

 

 

상태 패턴으로 리팩토링 하기위해서 

유니티로 배우는 게임 디자인 패턴 이라는 책과

인터넷의 자료들을 참고해서 하긴 했는데

사실 내가 개념을 제대로 이해했는지 약간 의문이 든다..

 

상태패턴이 정말 필요하고 유용해서 적용했다기 보다는

어거지로 끼워넣은 느낌..

나중에 필요하면 더 수정해야겠다.