와와

캐릭터 움직임 구현1: 유한상태머신(FSM) 본문

개발/Unity 3D

캐릭터 움직임 구현1: 유한상태머신(FSM)

정으주 2023. 4. 16. 02:19

 
캐릭터 조작을 구현하려면 FSM 공부가 필수인가봅니다.
공부해보겠습니다
 


1. 왜 필요할까?

 
 내가 현재 작업하고 있는 부분은 플랫포머 2D 캐릭터 조작이다.
좌/우 이동, 점프, 사다리 오르내리기만 구현하면 될거라 생각하고 아주 얕잡아봤었다!
 
이 코드는 초반에 내가 작성한 코드......

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

public class MoveController : MonoBehaviour
{
    private Movement movement;
    private float x;

    private void Awake()
    {
        movement = GetComponent<Movement>();
    }

    private void Update()
    {
        x = Input.GetAxisRaw("Horizontal");

        // jump
        if (Input.GetKeyDown(KeyCode.Space))
        {
            movement.Jump();
        }
        // long jump
        if (Input.GetKey(KeyCode.Space))
        {
            movement.LongJump(true);

        }
        else if (Input.GetKeyUp(KeyCode.Space))
        {
            movement.LongJump(false);
        }

    }

    private void FixedUpdate()
    {
        //move
        movement.Move(x);

    }
}

 
첨엔 이런식으로 입력에 따라 움직임 함수를 호출하고 movement 클래스에 각 움직임을 구현했었다.
 
문제는 사다리타기 였는데
사다리 코드 자체를 아주 복잡하게 구현해놓은 상태에서 사다리타다가 점프, 점프하다가 사다리까지 구현하려다 보니 내 머리가 터져나갔고, 코드 양도 홍수처럼 불어났었다...
이 모습을 보다못한 팀원들이 이 상태에서 애니메이션까지 들어가면 많이 힘들어질 것 같다며 FSM를 추천해줬다. 링크까지 달아줌,, 얼마나 답답했을까~!
 


 

2. 유한 상태 기계(finite-state machine, FSM)

 
유한 상태 기계의 요점
 
- 가질 수 있는 '상태'가 한정된다. ( 서있기, 사다리, 걷기, 점프 상태 )
- 한 번에 '한 가지' 상태만 될 수 있다. ( 서있기&사다리 동시에 있을 수 없음 )
- '입력'이나 '이벤트'가 기계에 전달된다. ( A,D,W,S,Space 키 )
- 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다.
 

상태 기계인가 ,, 플로 차트인가,,

 
내가 구현하고자 하는 모습을 먼저 그려보았다
 


 

3. 열거형(enum)을 이용한 FSM

 
상태를 표시하는 플러그 변수( isJumping, isClimbing,,,)들이 많고, 하나만 참일 때가 많다면 열겨형을 사용하자!
 

그렇다면 enum이 뭘까?

그렇다면 enum이 뭘까?

 

 : 열거형 상수(constant)를 표현하기 위한 것

 : 상수 숫자들을 단어들로 표현할 수 있음

enum문은 클래스 안이나 네임스페이스 내에서만 선언될 수 있다. 
https://www.csharpstudy.com/CSharp/CSharp-enum.aspx

 
내 캐릭터의 4가지 상태를 PlayerState에 정리했다.

public enum PlayerState{
    Idle, //0
    Run, //1
    Jump, //2
    Climb //3
}

 
그리고 뒤에 나올 PlayerController 클래스에 각 상태에 대한 동작을 처리할 메서드(Handle~ )들을 구현하였다.
또한, ChangeState 메서드를 사용하여 상태를 변경할 수 있도록 하였음
 
 

public enum PlayerState
{
    Idle,
    Run,
    Jump,
    Climb
}

public class PlayerController : MonoBehaviour
{
    private PlayerState currentState;
    private Vector2 climbDirection;
    private float climbSpeed = 5f;
    private bool isGrounded;

    private void Start()
    {
        currentState = PlayerState.Idle;
    }

    private void Update()
    {
        switch (currentState)
        {
            case PlayerState.Idle:
                HandleIdleState();
                break;
            case PlayerState.Run:
                HandleRunState();
                break;
            case PlayerState.Jump:
                HandleJumpState();
                break;
            case PlayerState.Climb:
                HandleClimbState();
                break;
        }
    }

    private void HandleIdleState()
    {
        // Idle 상태에서의 동작 구현
    }

    private void HandleRunState()
    {
        // Run 상태에서의 동작 구현
    }

    private void HandleJumpState()
    {
        // Jump 상태에서의 동작 구현
    }

    private void HandleClimbState()
    {
        // Climb 상태에서의 동작 구현
        transform.Translate(climbDirection * climbSpeed * Time.deltaTime);
    }

    private void ChangeState(PlayerState newState)
    {
        currentState = newState;
    }

	//사다리 감지
    private void OnTriggerEnter2D(Collider2D other)  
    {
        if (other.CompareTag("Ladder"))
        {
            climbDirection = Vector2.up;
            ChangeState(PlayerState.Climb);
        }
    }

	//사다리 탈출 감지
    private void OnTriggerExit2D(Collider2D other)  
    {
        if (other.CompareTag("Ladder"))
        {
            climbDirection = Vector2.zero;
            ChangeState(PlayerState.Idle);
        }
    }
    
    //isGrounded 검사
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }
    }
}

 
자세한 구현은 더보기. ( 이 글에선 안쓸예정... )
틀만 잡아놨는데 한달 밀린 방청소 끝낸 기분...!
 
 
여기서 더 나아가 Jump->Climb, Climb->Jump 의 상태전이도 생각해보았다.
 
HandleClimbState 메서드에서 Space 키를 누르면 Jump 상태로 변경하고
OnTriggerStay2D 메서드를 통해 Jump 상태에서 사다리에 닿아있을 때, Climb 키를 누르면 캐릭터가 사다리를 타도록 구현함
 
< PlayerController.cs >

public enum PlayerState
{
    Idle,
    Run,
    Jump,
    Climb
}

public class PlayerController : MonoBehaviour
{
    private PlayerState currentState;
    private Vector2 climbDirection;
    private float climbSpeed = 5f;
    private float jumpForce = 5f;
    private bool isGrounded;

    private void Start()
    {
        currentState = PlayerState.Idle;
    }

    private void Update()
    {
        switch (currentState)
        {
            case PlayerState.Idle:
                HandleIdleState();
                break;
            case PlayerState.Run:
                HandleRunState();
                break;
            case PlayerState.Jump:
                HandleJumpState();
                break;
            case PlayerState.Climb:
                HandleClimbState();
                break;
        }
    }

    private void HandleIdleState()
    {
        // Idle 상태에서의 동작 구현
    }

    private void HandleRunState()
    {
        // Run 상태에서의 동작 구현
    }

    private void HandleJumpState()
    {
        // Jump 상태에서의 동작 구현
        if (isGrounded)
        {
            ChangeState(PlayerState.Idle);
        }
    }

    private void HandleClimbState()
    {
        // Climb 상태에서의 동작 구현
        transform.Translate(climbDirection * climbSpeed * Time.deltaTime);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            ChangeState(PlayerState.Jump);
            GetComponent<Rigidbody2D>().velocity = Vector2.up * jumpForce;
            isGrounded = false;
        }
    }

    private void ChangeState(PlayerState newState)
    {
        currentState = newState;
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Ladder"))
        {
            climbDirection = Vector2.up;
            ChangeState(PlayerState.Climb);
        }
    }

    private void OnTriggerStay2D(Collider2D other)
    {
        if (other.CompareTag("Ladder") && Input.GetKeyDown(KeyCode.UpArrow))
        {
            climbDirection = Vector2.up;
            ChangeState(PlayerState.Climb);
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.CompareTag("Ladder"))
        {
            climbDirection = Vector2.zero;
            ChangeState(PlayerState.Idle);
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }
    }
}

 
 
 
상태 패턴도 공부해야겠다.