[Charon] #9. 대시(Dash) 선입력(Input Buffer) 기능 추가하기

2022. 11. 16. 23:18Projects/Charon

 

 

 

1. 선입력(Input Buffer)이 필요한 이유

 

게임에서 로직의 실행 단위는 프레임(Frame)입니다. 게임을 하다보면 FPS(Frames Per Second)란 수치를 볼 수 있는데, 1초에 몇 프레임이 실행되는지를 나타냅니다. 좋은 고사양 장비인 경우에는 200 프레임 이상의 수치를 보여주곤 합니다.

 

우리 게임에서 대시 애니메이션이 30fps이 조금 넘습니다. 이것은 선입력을 통한 버퍼가 없다면, 플레이어가 저 짧은 시간 내에 타이밍을 맞추어 입력을 해야 한다는 의미가 됩니다. 대시를 하는데 그 정도의 집중력을 쓰게 되면 피로도가 빨리 쌓이게 되겠죠.

 

철권과 같은 격투 게임은 커맨드(Command)라는 게 존재하는데, 입력 버퍼가 존재하기에 편안하게 콤보를 입력할 수 있습니다. 우리 게임에도 이러한 게 필요하여 넣어볼 생각을 하게 되었죠.

 

그리고 기존에 구현했던 대시는 선입력이 없었기에 대시 애니메이션의 일정 프레임 사이에서만 재입력을 받게

하였는데, 버퍼가 없으니 바로 모션이 재실행이 되어 뚝뚝 끊기는 듯한 부자연스러운 움직임도 보여줬었습니다.

이것도 해결할 겁니다.

 

문제가 있는 N단 대시

 

 

 


2. DashState 클래스 수정하기

 

저는 대시 선입력을 큐(Queue)를 통해 구현하였습니다. 입력을 했을 당시에 대한 방향 정보만 필요하기에 Vector3만 저장하는 Queue를 선언해주었습니다.

Queue<Vector3> inputDirectionBuffer = new Queue<Vector3>();

 

그리고 전반적으로 수정을 많이 하였죠.

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

namespace CharacterController
{
    public class DashState : BaseState
    {
        public int CurrentDashCount { get; set; } = 0;
        public bool CanAddInputBuffer { get; set; }     // 버퍼 입력이 가능한가?
        public bool CanDashAttack { get; set; }
        public bool IsDash { get; set; }
        public int Hash_DashTrigger { get; private set; }
        public int Hash_IsDashBool { get; private set; }
        public int Hash_DashPlaySpeedFloat { get; private set; }
        public Queue<Vector3> inputDirectionBuffer { get; private set; }

        public const float DEFAULT_ANIMATION_SPEED = 2f;
        public readonly float dashPower;
        public readonly float dashTetanyTime;
        public readonly float dashCooltime;

        public DashState(float dashPower, float dashTetanyTime, float dashCoolTime)
        {
            inputDirectionBuffer = new Queue<Vector3>();
            this.dashPower = dashPower;
            this.dashTetanyTime = dashTetanyTime;
            this.dashCooltime = dashCoolTime;
            Hash_DashTrigger = Animator.StringToHash("Dash");
            Hash_IsDashBool = Animator.StringToHash("IsDashing");
            Hash_DashPlaySpeedFloat = Animator.StringToHash("DashPlaySpeed"); 
        }

        public override void OnEnterState()
        {
            IsDash = true;
            CanAddInputBuffer = false;
            CanDashAttack = false;
            Player.Instance.animator.applyRootMotion = false;
            Dash();
        }

        private void Dash()
        {
            Vector3 dashDirection = inputDirectionBuffer.Dequeue();
            dashDirection = (dashDirection == Vector3.zero) ? Player.Instance.Controller.transform.forward : dashDirection;

            Player.Instance.animator.SetBool(Hash_IsDashBool, true);
            Player.Instance.animator.SetTrigger(Hash_DashTrigger);
            Player.Instance.Controller.LookAt(new Vector3(dashDirection.x, 0f, dashDirection.z));

            float dashAnimationPlaySpeed = DEFAULT_ANIMATION_SPEED + (Player.Instance.MoveSpeed * MoveState.CONVERT_UNIT_VALUE - MoveState.DEFAULT_CONVERT_MOVESPEED) * 0.1f;
            Player.Instance.animator.SetFloat(Hash_DashPlaySpeedFloat, dashAnimationPlaySpeed);
            Player.Instance.rigidBody.velocity = dashDirection * (Player.Instance.MoveSpeed * MoveState.CONVERT_UNIT_VALUE) * dashPower;
        }

        public override void OnUpdateState()
        {

        }

        public override void OnFixedUpdateState()
        {

        }

        public override void OnExitState()
        {
            Player.Instance.rigidBody.velocity = Vector3.zero;
            Player.Instance.animator.applyRootMotion = true;
            Player.Instance.animator.SetBool(Hash_IsDashBool, false);
        }
    }
}

 

Queue에서 하나를 가져와서(Dequeue) 해당 방향으로 대시를 해주는 것이죠. 키 입력 처리는 다음과 같이 하였습니다.

// PlayerController

public void OnDashInput(InputAction.CallbackContext context)
{
    // 대시 키인 스페이스 바를 눌렀다 떼었을 경우에 실행되도록 해주었습니다.
    if (context.performed && context.interaction is PressInteraction)
    {
        // 대시 입력을 막아야 하는 상황이 있을 경우 return;
   
        
        if (dashState.CurrentDashCount >= player.DashCount)
            return;

        // 대시 중에 버퍼에 입력 가능한 프레임일 때 입력 받을 경우
        if (dashState.CanAddInputBuffer && isGrounded)
        {
            dashState.CurrentDashCount++;
            dashState.inputDirectionBuffer.Enqueue(calculatedDirection);
            return;
        }

        // Idle 상태에서 대시를 입력받을 경우
        if (!dashState.IsDash && isGrounded)
        {
            dashState.CurrentDashCount++;
            dashState.inputDirectionBuffer.Enqueue(calculatedDirection);
            player.stateMachine.ChangeState(StateName.DASH);
        }
    }
}

 

Idle 상태에서 대시 키를 입력할 경우에는 Enqueue()를 하고, 대시 상태로 전환을 해줍니다.

대시 상태로 전환을 했으니 Dequeue를 통해 바로 뽑아와서 대시 로직을 실행하겠지요.

 

대시 중이고, 버퍼에 입력이 가능한 프레임 구간일 경우에는 대시 카운트 증가Enqueue()만 해줍니다.

대시 카운트가 다 찼다면 return하여 더 이상 받지 말아야 하겠죠.

 

 

그렇다면 대시 버퍼 입력이 가능한 구간대시 애니메이션이 끝났을 때의 처리는 누가 해줄까요?

애니메이션 이벤트 함수를 통해 처리하였습니다.

 

Dash 애니메이션

 

빨간색으로 동그라미 친 애니메이션 이벤트대시 버퍼 입력을 가능하게 해주는 함수를 호출해줍니다.

dashState = Player.Instance.stateMachine.GetState(StateName.DASH) as DashState;

public void OnCanDashAttack()
{
    dashState.CanDashAttack = true;
}

 

파란색으로 동그라미 친 애니메이션 이벤트대시 애니메이션이 끝났을 때의 처리를 담당하는 함수를 호출해줍니다.

private Coroutine dashCoolTimeCoroutine;


public void OnFinishedDash()
{
    if (!dashAttackState.IsDashAttack)
    {
        dashState.CanDashAttack = false;

        // 버퍼에 선입력으로 넣었던 게 남아있다면, 다시 Dash로 상태 전환
        if (dashState.inputDirectionBuffer.Count > 0)
        {
            Player.Instance.stateMachine.ChangeState(StateName.DASH);
            return;
        }

        // 없다면, 버퍼 입력을 종료하고 대시 쿨타임 시작
        dashState.CanAddInputBuffer = false;
        dashState.OnExitState();

        if (dashCoolTimeCoroutine != null)
            StopCoroutine(dashCoolTimeCoroutine);
        dashCoolTimeCoroutine = 
        StartCoroutine(CheckDashReInputLimitTime(dashState.dashCooltime));
     }
}


private IEnumerator CheckDashReInputLimitTime(float limitTime)
{
    float timer = 0f;

    while (true)
    {
        timer += Time.deltaTime;
            
        if(timer > limitTime)
        {
            dashState.IsDash = false;
            dashState.CurrentDashCount = 0;
            Player.Instance.stateMachine.ChangeState(StateName.MOVE);
            break;
        }
        yield return null;
    }
}

 

이렇게 해주면, 대시 선입력 버퍼 만들기가 끝나게 됩니다. 결과로 한 번 보여드리겠습니다.

 

 

 


3. 결과

 

버퍼에 입력이 들어가는 것을 보여드리기 위해, 애니메이션 속도를 늦추어 촬영하였습니다.

 

 

Idle 상태에서 Dash로 전환될 때는 "대시 처음 발동"이란 로그가 찍힙니다.

Dash 중에 선입력 버퍼 입력을 받게 되면, "대시 버퍼에 추가"란 로그가 찍힙니다.

 

위의 GIF의 예시는 제가 (왼쪽, 오른쪽), (왼쪽, 위) 입력을 차례대로 준 모습입니다. 버퍼에 다음 대시의 방향 정보가 저장되어 있고, 현재 대시가 끝나고 바로 버퍼에 있던 방향으로 대시를 하는 것을 볼 수 있습니다.

 

 

728x90
반응형