본문 바로가기
Development/멋쟁이사자처럼 게임개발 부트캠프

[멋쟁이사자처럼 Unity 게임 부트캠프 4기] 28일차 - State 패턴 횡스크롤 2D (2)

by jjeondeuk1008 2025. 4. 18.
반응형

[ 목차 ]


     

    오늘의 포스팅은 지난 실습에 이어서 하는 내용이다.

    추가적으로는 State 패턴을 활용한다는 점인데,

     

    게임디자인패턴 중 State를 실습을 통해 어떻게 쓰이는지 하나씩 알아보도록 한다!

     

     

     

    State 패턴 횡스크롤 (1)

     

    [멋쟁이사자처럼 Unity 게임 부트캠프 4기] 27일차 (2) - State 패턴 횡스크롤 2D

    [ 목차 ] 오늘의 포스팅은 상속을 활용한 횡스크롤 2D를 간단하게 실습한 이후에 게임디자인 패턴에서 배운 state 패턴 응용을 하는 것이다. 1. 스테이트머신 패턴 응용 이제 스테이트머신 패턴을

    gang-design.com

     

     


     

    1. Player & Animator 생성

     

     

     

    게임디자인패턴을 활용한 게임을 본격적으로 제작해 볼 것이다.

    GameObject를 만들어 Player 이름의 객체로 만든 뒤에

     

    자식으로 idle인 상태의 플레이어 사진 1장을 넣어준다.

    이름은 Animator로 수정한다.

     

     

     

     

    현재 이 이미지의 특성상 기준점을 맞춰주기 위해

    자식인 Animator 객체에서 Pivot을 눌러준 뒤에 x로 0.4 만큼 이동한다.

     

     

     

     

    그렇게 되면 부모인 Player 객체의 기준점은 가운데로 오게 된다.

     

     

     

     

    이제 플레이어의 애니메이션을 만들 것이다.

    Animation 폴더를 만든 뒤에

    Create - Animation - Animator Controller 생성한다.

     

     

     

     

    Window - Animation (ctrl + 6)을 열어준 뒤에

    Animator 객체를 선택한 상태에서 Create를 눌러준다.

     

     

     

     

    player_idle로 생성 후 idle 상태인 여러 이미지를 선택하고,

    Animation 창으로 드래그 앤 드롭을 한다.

    Sample 프레임을 10으로 낮춰준다.

     

     

     

     

    Animator에서 새로운 Parameters를 생성한다.

    bool로 된 IdleMove를 만들어준다.

     

     

     

     

    Entry에서 idle로 이어지고, Exit로 이어지는 트랜지션을 만든다.

    Move도 마찬가지이다.

     

     

     

     

    idle → Exit로 가는 트랜지션은 idle false로 설정한다.

     

     

     

     

    Entry → move로 가는 트랜지션은 Move true로 설정한다.

     

     

     

     

    move → Exit로 가는 트랜지션은 Move false로 설정한다.

     

     

     

     

    이제 Player 스크립트로 가서 애니메이션에 대한 추가 코드 작성을 한다.

    Animator 프로퍼티 변수를 작성한다.

    public Animator anim { get; private set; }
    

     

     

     

     

    게임을 시작할 때 자식에 있는 Animator를 찾아주는 코드를 작성한다.

    private void Start()
    {
        //자식에 있는 Animator 찾기
        anim = GetComponentInChildren<Animator>();
    
        //게임 시작 시 초기 상태를 대기 상태(idle)로 설정
        stateMachine.Initialize(idleState);
    }
    

     

     

     

     

    여기에서 Tip!

     

    코드가 길어지면 보기에 불편할 수 있다.

    그럴 때

    #region (태그 이름)

     

    이렇게 태그 이름을 설정해 목록을 나누어 접어줄 수가 있다.

    끝내는 지점에

    #endregion

     

    넣으면

    이 사이의 범위를 설정할 수 있다.

    //태그 설정해 코드가 길 때 유용함
    #region Components
    public Animator anim { get; private set; }
    
    #endregion
    

     

     

     

     

    이렇게 접을 때 설정해 둔 태그 이름으로 구분할 수 있다.

     

     

     

     

    상태 변수도 태그 이름으로 구분해 놓았다.

     

     

     


    2. 바닥 생성

     

     

     

    Square 객체를 생성해 바닥을 설정한다.

     

     

     

     

    Box Collider 2D를 생성한다.

     

     

     

     

    Player도 Capsule Collider 2DRigidbody 2D를 생성한다.

    그리고 Freeze Rotation z값 체크를 한다.

     

     

     

     

    Rigidbody2D에서

    Gravity Scale 3.5

    Collision Detection - Continuous

    Interpolate - Interpolate로 설정한다.

    조금 더 세밀하게 체크해준다.

     

     

     

     


     

    3. 플레이어 이동

     

     

     

    Playerstate 스크립트

     

    키 입력을 받을 변수를 설정한 후에 이동 처리가 되는 코드를 작성한다.

    // 키 입력을 받을 변수
    protected float xInput;
    
    // 상태 업데이트 시 수행할 동작 (예: 이동 처리 등)
    public virtual void Update()
    {
        //키 입력을 받기
        xInput = Input.GetAxisRaw("Horizontal");
    }
    

     

     

     

     

    PlayerIdleState 스크립트

     

    Update() 메서드 안에 테스트용 N키 입력 시에 업데이트되는 코드 삭제 후 코드 작성

    xInput이 0이 아닐 때 Move 상태로 변경하는 코드이다.

    public override void Update()
    {
        base.Update();
    
        //xInput이 0이 아닐 때 movestate으로 변경
        if (xInput != 0)
            stateMachine.ChangeState(player.moveState);
    }
    

     

     

     

     

    PlayerMoveState 스크립트

    여기에는 idleState 스크립트와 반대로 0일 때 idle 상태로 업데이트되는 코드를 작성한다.

    public override void Update()
    {
        base.Update();
    
        if (xInput == 0)
            stateMachine.ChangeState(player.idleState);
    }
    

     

     

     

     

    다시 PlayerState로 가서

    Move와 Idle 애니메이션을 실행하는 코드를 작성한다.

    // 상태 진입 시 수행할 동작 (예: 애니메이션 설정 등)
    public virtual void Enter()
    {
        player.anim.SetBool(animBoolName, true);
    }
    
    // 상태 종료 시 수행할 동작 (예: 애니메이션 해제 등)
    public virtual void Exit()
    {
        player.anim.SetBool(animBoolName, false);
    }
    

     

     

     

     

    Player 스크립트

    Rigidbody2D의 프로퍼티 변수를 선언한다.

    public Rigidbody2D rb { get; private set; }
    

     

     

     

     

    이제 Rigidbody가 있는지 객체에서 찾는 코드를 Start() 메서드에서 작성한다.

    private void Start()
    {
        //자식에 있는 Animator 찾기
        anim = GetComponentInChildren<Animator>();
        //객체에 있는 Rigidbody2D 찾기
        rb = GetComponent<Rigidbody2D>();
    
        //게임 시작 시 초기 상태를 대기 상태(idle)로 설정
        stateMachine.Initialize(idleState);
    }
    

     

     

     

     

    linearVelocity는 Rigidbody2D 속도를 나타내는 벡터이다.

    x축과 y축 속도를 설정해 해당 방향으로 움직이는 코드이다.

    public void SetVelocity(float _xVelocity, float _yVelocity)
    {
        rb.linearVelocity = new Vector2(_xVelocity, _yVelocity);
    }
    

     

     

     

     

    PlayerMoveState 스크립트의 Update() 메서드로 간다.

     

    위의 SetVelocity 메서드를 호출하면서

    x 방향 속도는 xInput 값으로,

    y 방향 속도는 linearVelocityY 속도 값을 그대로 유지하면서 설정하는 코드이다.

    public override void Update()
    {
        base.Update();
    
        player.SetVelocity(xInput, player.rb.linearVelocityY);
    
        if (xInput == 0)
            stateMachine.ChangeState(player.idleState);
    }
    

     

     

     

     

    이제 플레이어가 움직일 때의 속도의 초기값을 선언한다.

     

    Player 스크립트로 이동한다.

    속도를 얼마만큼 줄 것인지 설정을 한다.

    [Header("이동 정보")]
    public float moveSpeed = 10f;
    

     

     

     

     

    다시 PlayerMoveState 스크립트로 돌아간다.

     

    Update() 메서드에서 xInput * player.moveSpeed로 지정한 이동속도를 곱해서 움직이게 한다.

    이제 player.rb.linearVelocityY 코드를 좀 더 간추려보겠다.

    public override void Update()
    {
        base.Update();
    
        player.SetVelocity(xInput *player.moveSpeed, player.rb.linearVelocityY);
    
        if (xInput == 0)
            stateMachine.ChangeState(player.idleState);
    }
    

     

     

     

     

    PlayerState 스크립트로 이동한다.

    Rigidbody2D 컴포넌트를 참조할 변수를 선언한다.

    // 객체의 Rigidbody2D 참조
    protected Rigidbody2D rb;
    

     

     

     

     

    애니메이터에서 특정 애니메이션 조건을 true로 바꾸는 코드를 작성한다.

    // 상태 진입 시 수행할 동작 (예: 애니메이션 설정 등)
    public virtual void Enter()
    {
        player.anim.SetBool(animBoolName, true);
        rb = player.rb;
    }
    

     

     

     

     

    PlayerMoveState 스크립트로 이동한다.

    이제 player.rb.linearVelocityY에서 player를 없애 좀 더 간편한 코드가 된다.

    public override void Update()
    {
        base.Update();
    
        player.SetVelocity(xInput *player.moveSpeed, rb.linearVelocityY);
    
        if (xInput == 0)
            stateMachine.ChangeState(player.idleState);
    }
    

     

     

     

     

    새로운 스크립트 PlayerGroundedState를 생성한다.

    using UnityEngine;
    
    public class PlayerGroundedState : PlayerState
    {
        public PlayerGroundedState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
        }
        public override void Update()
        {
            base.Update();
        }
    
        public override void Exit()
        {
            base.Exit();
        }
    
    }
    
    

     

     

     

     

    PlayerIdleState와 PlayerMoveState의 상속 부모를 PlayerGoundedState로 바꾼다.

    public class PlayerIdleState : PlayerGroundedState
    

     

     

     

     


     

    4. 점프 & 하강 애니메이션

     

     

     

    Jump Animation Clip을 만들어서 Sprite를 배치한다.

     

     

     

     

    fall Animation Clip도 마찬가지로 만든다.

     

     

     

     

    Animator 창에서 Create State - From New Blend Tree를 생성한다.

     

     

     

     

    이름을 JumpFall로 변경하고 Parameters int 형식의 yVelocity를 생성한다.

     

     

     

     

    Blend Tree 안에서 만들어두었던 점프와 하강 클립을 넣어준다.

    jump는 1, fall은 -1로 설정한다.

     

     

     

     

    만들어둔 Blend Tree를 Entry와 Exit를 연결한다.

     

     

     

     

    PlayerAirState를 새로 생성해 PlayerState 상속,

    생성자 생성과 재정의 생성을 한다.

    using UnityEngine;
    
    public class PlayerAirState : PlayerState
    {
        public PlayerAirState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
        }
        public override void Update()
        {
            base.Update();
        }
    
        public override void Exit()
        {
            base.Exit();
        }
    
    }
    
    

     

     

     

     

    PlayerJumpState도 동일하게 해 준다.

    using UnityEngine;
    
    public class PlayerJumpState : PlayerState
    {
        public PlayerJumpState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
        }
        public override void Update()
        {
            base.Update();
        }
    
        public override void Exit()
        {
            base.Exit();
        }
    
    }
    
    

     

     

     

     

    Player 스크립트

     

    Awake() 메서드에서 Jump와 airState 상태 인스턴스 생성 코드를 적어준다.

    private void Awake()
    {
        stateMachine = new PlayerStateMachine();
    
        // 각 상태 인스턴스 생성 (this: 플레이어 객체, stateMachine: 상태 머신, "Idle"/"Move": 상태 이름)
        idleState = new PlayerIdleState(this, stateMachine, "Idle");
        moveState = new PlayerMoveState(this, stateMachine, "Move");
        jumpState = new PlayerJumpState(this, stateMachine, "Jump");
        airState = new PlayerAirState(this, stateMachine, "Jump");
    }
    

     

     

     

     

    PlayerJumpState 스크립트

     

    이제 Rigidbody2D의 VelocityY가 0보다 작을 때에 airState를 실행한다.

     

    여기서 왜 0인 것인가? 하면

    전에 Blend Tree에서 airState 값을 -1로 하였기 때문일 것이다.

    public override void Update()
    {
        base.Update();
    
        // 0보다 클 때 airState로 변경
        if (rb.linearVelocityY < 0)
            stateMachine.ChangeState(player.airState);
    
    }
    

     

     

     

     

    PlayerAirState 스크립트

     

    반대로 0일 때에 JumpState로 변경한다.

    public override void Update()
    {
        base.Update();
    
    	  // 0일때 jumpState로 변경
        if (rb.linearVelocityY == 0)
            stateMachine.ChangeState(player.jumpState);
    }
    

     

     

     

     

    PlayerGroundedState 스크립트

     

    Update() 메서드에서 Space바를 누르면 JumpState가 실행되는 것을 작성한다.

    public override void Update()
    {
        base.Update();
    
        if(Input.GetKeyDown(KeyCode.Space))
            stateMachine.ChangeState(player.jumpState);
    }
    

     

     

     

    이동 정보에 관한 코드를 작성하고 실행하는 코드를 작성한다.

    [Header("이동 정보")]
    public float moveSpeed = 10f;
    public float jumpForce = 12f;
    
    public override void Enter()
    {
        base.Enter();
    
        rb.linearVelocity = new Vector2(rb.linearVelocityX, player.jumpForce);
    }
    

     

     


     

    5. 바닥 & 벽 충돌 체크

     

     

    Player 스크립트충돌에 필요한 변수를 선언한다.

     

    바닥과 벽의 위치 값을 받아오는 Transform 변수,

    바닥을 감지할 거리, 벽 감지할 거리 float 변수를 설정한다.

    [Header("충돌 정보")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckDistance;
    [SerializeField] private Transform wallCheck;
    [SerializeField] private float wallCheckDistance;
    

     

     

     

     

    이제 Gizmos 라인을 만들 것이다.

     

    여기에서 Gizmos란?

    씬 뷰에서만 보이는 디버깅용 시각적 도구이다.

    그래서 game뷰에서는 보이지 않는다.

     

    라인으로 설정하고,

    groundCheck는 해당 위치에서 아래 방향(y)으로 groundcheckDistance의 값만큼 선을 그린다.

    wallCheck는 해당 위치에서 오른쪽(x)으로 wallCheckDistance의 값만큼 선을 그린다.

    public void OnDrawGizmos()
    {
        Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
        Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckDistance, wallCheck.position.y));
    }
    

     

     

     

     

    GameObject를 Player의 하위객체로 생성해서

    GroundCheckWallCheck를 만든다.

     

    충돌 정보에 해당 객체를 넣는다.

    Distance는 개인적으로 값을 조절하면 된다.

     

     

     

     

    이렇게 흰색 위 아래 선이 생겨있을 것이다.

    바닥 선은 플레이어의 발 쪽에 맞게 해 주고,

    벽은 오른쪽 끝에 맞춰서 선을 맞추어준다.

     

     

     

     

    이제 벽을 감지할 Layer 체크 코드를 설정한다.

    Player스크립트로 가서 LayerMask 변수를 설정한다.

    [SerializeField] private LayerMask whatIsGround;
    

     

     

     

     

    플레이어가 바닥에 서있는지 판단하는 함수를 작성한다.

    람다식으로 작성한다. (이것은 선택!)

    public bool IsGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
    

     

     

     

     

    이제 조건문에서 조금 수정한다.

    스페이스 키를 눌렀던 것만 있는 조건과 땅에 있는지 확인하는 함수를 체크하는 조건을 추가한다.

    public override void Update()
    {
        base.Update();
    
        if (Input.GetKeyDown(KeyCode.Space) && player.IsGroundDetected())
            stateMachine.ChangeState(player.jumpState);
    }
    

     

     

     

     

    그리고 플레이어가 땅에 닿아있으면 idleState로 변경하는 로직을 작성한다.

    public override void Update()
    {
        base.Update();
    
        // 0일때 idleState 변경
        if (player.IsGroundDetected())
            stateMachine.ChangeState(player.idleState);
    }
    

     

     

     


    6. 플레이어 방향 전환

     

     

     

    Player 스크립트

     

    방향 전환을 위한 변수를 작성한다.

    // 플레이어가 바라보고 있는 방향을 숫자로 지정
    public int facingDir { get; private set; } = 1;
    private bool facingRight = true;
    

     

     

    Flip() 메서드에서 캐릭터가 Rotate 180로 인한 변경을 해준다.

    public void Flip()
    {
        facingDir = facingDir * -1;     //바라보는 방향을 반대로 바꿈
        facingRight = !facingRight;     //캐릭터가 오른쪽을 바라보는지 여부 확인
        transform.Rotate(0, 180, 0);    //180도 뒤집겠다
    }
    

     

     

     

     

    FipController() 메서드에서

    오른쪽으로 움직이는데, 바라보고 있는 방향이 오른쪽이 아닐 때에 Flip() 메서드를 실행한다.

    반대쪽도 마찬가지로 작성한다.

    public void FlipController()
    {
        if (rb.linearVelocity.x > 0 && !facingRight) //오른쪽으로 움직이는데, 바라보고 있는 방향 오른쪽이 아닐 때
            Flip();
        else if(rb.linearVelocity.x < 0 && facingRight) //왼쪽으로 움직이는데, 바라보고 있는 방향 왼쪽이 아닐 때
            Flip();
    }
    

     

     

     


     

    7. 쿨타임 생성

     

     

    쿨타임에 필요한 변수를 작성한다.

    //쿨타임 생성
    public float timer;
    public float cooldown { get; private set; }
    

     

     

     

     

    Update() 메서드에 가서 키 입력하는 조건문 안에 쿨다운만큼의 시간이 지나야 가능하다.

    private void Update()
    {
        stateMachine.currentState.Update();
        FlipController();
    
        //Cooldown초 뒤를 지나야 키 입력 가능
        timer -= Time.deltaTime;
        if(timer < 0 && Input.GetKeyDown(KeyCode.R))
        {
            timer = cooldown;
        }
    }
    

     

     


    8. 대시

     

     

    Player 스크립트로 이동한다.

    대시 상태 코드를 작성한다.

    public PlayerDashState dashState { get; private set; }
    
    private void Awake()
    {
        stateMachine = new PlayerStateMachine();
    
        // 각 상태 인스턴스 생성 (this: 플레이어 객체, stateMachine: 상태 머신, "Idle"/"Move": 상태 이름)
        idleState = new PlayerIdleState(this, stateMachine, "Idle");
        moveState = new PlayerMoveState(this, stateMachine, "Move");
        jumpState = new PlayerJumpState(this, stateMachine, "Jump");
        airState = new PlayerAirState(this, stateMachine, "Jump");
        dashState = new PlayerDashState(this, stateMachine, "Dash");
    }
    

     

     

     

     

    PlayerDashState

    새로운 스크립트를 생성한다.

     

    대시 타이머가 0보다 작아지면 idle상태를 실행한다.

    using UnityEngine;
    
    public class PlayerDashState : PlayerState
    {
        public PlayerDashState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
    
            //1.5초 동안 대쉬
            stateTimer = 1.5f;
        }
        public override void Update()
        {
            base.Update();
    
            Debug.Log("플레이어 대쉬 중");
    
            if (stateTimer < 0)
                stateMachine.ChangeState(player.idleState); 
        }
    
        public override void Exit()
        {
            base.Exit();
        }
    
    }
    
    

     

     

     

     

    PlayerGroundedState 스크립트

     

    Update() 메서드에서 LeftShift를 누를 때에 dashState를 실행하는 로직을 작성한다.

    public override void Update()
    {
        base.Update();
    
        if (Input.GetKeyDown(KeyCode.Space) && player.IsGroundDetected())
            stateMachine.ChangeState(player.jumpState);
    
        if (Input.GetKeyDown(KeyCode.LeftShift))
            stateMachine.ChangeState(player.dashState);
    }
    

     

     

     

     

    Animator에 Parameters bool 타입의 Dash를 생성한다.

     

     

     

     

    Dash Animation Clip을 만들어서 이미지를 넣는다.

     

     

     

    Player

    [Header("이동 정보")]
    public float moveSpeed = 10f;
    public float jumpForce;
    public float dashSpeed;
    public float dashDuration;
    

     

     

     

     

    PlayerDashState

     

    대시 상태를 구현하는 코드를 작성한다.

    public override void Enter()
    {
        base.Enter();
    
        //대쉬중
        stateTimer = player.dashDuration;
    }
    public override void Update()
    {
        base.Update();
    
        player.SetVelocity(player.dashSpeed * player.facingDir, rb.linearVelocityY);
    
        if (stateTimer < 0)
            stateMachine.ChangeState(player.idleState); 
    }
    

     

     

    Player 스크립트로 이동한다.

     

    플레이어가 대시할 때 필요한 변수를 선언한다.

    대시 쿨타임, 타이머, 속도, 지속 시간, 방향을 정한다.

    dashCooldown 대시 쿨타임 private (Inspector 표시됨)
    dashUsageTimer 쿨타임 체크용 타이머 private
    dashSpeed 대시 속도 public
    dashDuration 대시 지속 시간 public
    dashDir 대시 방향 public get / private set
    [Header("대시 정보")]
    [SerializeField] private float dashCooldown;
    private float dashUsageTimer;
    public float dashSpeed;
    public float dashDuration;
    public float dashDir { get; private set; }
    

     

     

     

     

    이제 대시 입력을 감지하고, 상태로 전환하는 코드를 작성한다.

     

    dashUsageTimer -= Time.deltaTime;

    이 코드는 매 타임마다 대시 타이머를 줄인다는 것이다. (쿨타임 체크용)

     

    if 조건문에서는 LeftShift를 누르면서 쿨타임이 끝났는지 확인이 되면 조건이 수락된다.

     

    dashUsageTimer = dashCooldown;

    이 코드는 대시를 썼으니 타이머를 다시 채운다는 것이다.

     

    dashDir = Input.GetAxisRaw("Horizontal");

    왼쪽(-1), 중립(0), 오른쪽(1) 입력값을 가져온다.

     

    if (dashDir == 0) dashDir = facingDir;

    방향키를 누르지 않아도 캐릭터가 바라보는 방향으로 대시를 하게 한다.

     

    stateMachine.ChangeState(dashState);

    상태머신을 이용해 캐릭터 상태를 대시로 변경한다.

    private void CheckForDashInput()
    {
    
        dashUsageTimer -= Time.deltaTime;
    
        if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer < 0)
        {
    
            dashUsageTimer = dashCooldown;
    
            dashDir = Input.GetAxisRaw("Horizontal");
    
            if (dashDir == 0)
                dashDir = facingDir;
    
            stateMachine.ChangeState(dashState);
        }
    
    }
    

     

     

     

     

    Update() 메서드에서는 위의 CheckForDashInput() 메서드를 실행한다.

    private void Update()
    {
        stateMachine.currentState.Update();
    
        CheckForDashInput();
    
    }
    

     

     

     

     

    Player 전체 스크립트이다.

    using UnityEngine;
    
    public class Player : MonoBehaviour
    {
    
        [Header("이동 정보")]
        public float moveSpeed = 12f;
        public float jumpForce;
    
        //추가된거
        [Header("대시 정보")]
        [SerializeField] private float dashCooldown;
        private float dashUsageTimer;
        public float dashSpeed;
        public float dashDuration;
        public float dashDir { get; private set; }
    
        [Header("충돌 정보")]
        [SerializeField] private Transform groundCheck;
        [SerializeField] private float groundCheckDistance;
        [SerializeField] private Transform wallCheck;
        [SerializeField] private float wallCheckDistance;
        [SerializeField] private LayerMask whatIsGround;
    
        public int facingDir { get; private set; } = 1;
        private bool facingRight = true;
    
        #region Components
        public Animator anim { get; private set; }
        public Rigidbody2D rb { get; private set; }
        #endregion
    
        #region States
        // 플레이어의 상태를 관리하는 상태 머신
        public PlayerStateMachine stateMachine { get; private set; }
    
        // 플레이어의 상태 (대기 상태, 이동 상태)
        public PlayerIdleState idleState { get; private set; }
        public PlayerMoveState moveState { get; private set; }
        public PlayerJumpState jumpState { get; private set; }
        public PlayerAirState airState { get; private set; }
        public PlayerDashState dashState { get; private set; }
    
        #endregion
        
    
        private void Awake()
        {
            // 상태 머신 인스턴스 생성
            stateMachine = new PlayerStateMachine();
    
            // 각 상태 인스턴스 생성 (this: 플레이어 객체, stateMachine: 상태 머신, "Idle"/"Move": 상태 이름)
            idleState = new PlayerIdleState(this, stateMachine, "Idle");
            moveState = new PlayerMoveState(this, stateMachine, "Move");
            jumpState = new PlayerJumpState(this, stateMachine, "Jump");
            airState  = new PlayerAirState(this, stateMachine, "Jump");
            dashState = new PlayerDashState(this, stateMachine, "Dash"); 
    
        }
    
        private void Start()
        {
    
            anim = GetComponentInChildren<Animator>();
            rb = GetComponent<Rigidbody2D>();
    
            // 게임 시작 시 초기 상태를 대기 상태(idleState)로 설정
            stateMachine.Initialize(idleState);
        }
    
       
    
        private void Update()
        {
            stateMachine.currentState.Update();
    
            CheckForDashInput();
    
        }
    
        //추가
        private void CheckForDashInput()
        {
    
            dashUsageTimer -= Time.deltaTime;
    
            if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer < 0)
            {
    
                dashUsageTimer = dashCooldown;
    
                dashDir = Input.GetAxisRaw("Horizontal");
    
                if (dashDir == 0)
                    dashDir = facingDir;
    
                stateMachine.ChangeState(dashState);
            }
    
        }
    
        public void SetVelocity(float _xVelocity, float _yVelocity)
        {
            rb.linearVelocity = new Vector2(_xVelocity, _yVelocity);
            FlipController(_xVelocity);
        }
    
        public bool IsGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
    
        private void OnDrawGizmos()
        {
            Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
            Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckDistance, wallCheck.position.y));
        }
    
        public void Flip()
        {
            facingDir = facingDir * -1;
            facingRight = !facingRight;
            transform.Rotate(0, 180, 0);
        }
    
        public void FlipController(float _x)
        {
            if (_x > 0 && !facingRight)
                Flip();
            else if (_x< 0 && facingRight)
                Flip();
    
        }
    
    }
    
    

     

     

     

     

    이렇게 Player 스크립트의 Inspector에 대시 정보가 보인다.

     

     

     

     

    플레이어가 공중에 떠 있으면 airState 상태로 전환하는 코드를 추가한다.

    기존에 있던 대시상태로 변경하는 것은 삭제한다.

    if (!player.IsGroundDetected())
                stateMachine.ChangeState(player.airState);
    

     

     

     

     

    PlayerGroundedState 전체 스크립트이다.

    using UnityEngine;
    
    public class PlayerGroundedState : PlayerState
    {
        public PlayerGroundedState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) 
            : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
        }
    
        public override void Exit()
        {
            base.Exit();
        }
    
        public override void Update()
        {
            base.Update();
    
            if (!player.IsGroundDetected())
                stateMachine.ChangeState(player.airState);
    
            if (Input.GetKeyDown(KeyCode.Space) && player.IsGroundDetected())
                stateMachine.ChangeState(player.jumpState);
        }
    }
    

     

     

     

     

    PlayerDashState 전체 스크립트

    using UnityEngine;
    
    public class PlayerDashState : PlayerState
    {
        public PlayerDashState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
        {
        }
    
        public override void Enter()
        {
            base.Enter();
    
            stateTimer = player.dashDuration;
        }
    
       
        public override void Update()
        {
            base.Update();
    
            player.SetVelocity(player.dashSpeed * player.dashDir, 0);
    
            if (stateTimer < 0)
                stateMachine.ChangeState(player.idleState);
        }
    
        public override void Exit()
        {
            base.Exit();
            player.SetVelocity(0, rb.linearVelocityY);
    
        }
    
    }
    

     

     

     

     

     


     

     

    상태 머신을 통한 간단한 플레이어의 이동, 점프 및 하강, 대시를 구현해 보았다.

     

    그리고 이전에 했던 벽과 바닥을 체크하는 것과 플레이어 방향 전환을

    다시 복습하는 겸 작성해 보았다.

     

    이제 이후엔 벽 점프, 공격에 대해서 배울 것이니!!

    차근차근 앞으로 나아가보자!

     

     


    목차