One_KWS

게임 개발 일지 #6 - 플레이어 공격 개선 본문

게임 개발

게임 개발 일지 #6 - 플레이어 공격 개선

One-Kim 2023. 2. 8. 00:08

일주일에 하나는 쓰려고 했는데 저번주에 쓰지 못했다.. 짧더라도 매주 꾸준히 글 하나씩은 쓰도록 노력해야겠다. 일지가 아니더라도 공부한 내용들도 정리해서 올려야겠다. (Velog랑 Notion에 정리했던 것들도 옮겨야되는데 언제 옮기지 ..)


플레이어 공격 수정

기존에 구현했던 플레이어의 공격이 다른 행동들(이동, 대쉬)과 이어질 때 자연스럽게 이어지지 않은 문제와 간혹 공격 중에 공격이 먹통되는 문제가 있어서 이것을 수정하는 작업을 진행했다. 수정하는 김에 공격 애니메이션 모션과 이펙트도 수정했다. 

 

PlayerCombat 

공격 부분 로직은 전부 수정했다. AnimatorStateInfo를 이용하여 현재 애니메이션 상태를 체크하여 다음 공격으로 넘어가게 했다. currentCombo 값이 0이라면 attack_1을 true로 바꿔주고 currentCombo 값에 1을 더해준다. currentCombo가 1 또는 2일 경우에는 현재 애니메이션 상태가 attack_1 또는 attack_2 인지 그리고 애니메이션의 진행상태가 30~70% 사이인지 체크(normalizedTime이 0.3~0.7)하여 만족하는 경우에 다음 공격으로 넘어가도록 했다. 

...

private readonly int ATTACK_1 = Animator.StringToHash("attack_1")
private readonly int ATTACK_2 = Animator.StringToHash("attack_2");
private readonly int ATTACK_3 = Animator.StringToHash("attack_3");

...

private void Attack() {
    switch (currentCombo) {
        case 0:
            animator.SetBool(ATTACK_1, true);
            isAttacking = true;
            currentCombo += 1;
            break;
        case 1 when IsOngoingAttack(ATTACK_1):
            animator.SetBool(ATTACK_2, true);
            animator.SetBool(ATTACK_1, false);
            currentCombo += 1;
            break;
        case 2 when IsOngoingAttack(ATTACK_2):
            animator.SetBool(ATTACK_3, true);
            animator.SetBool(ATTACK_2, false);
            currentCombo += 1;
            break;
    }
    
    //currentCombo가 3이상이 되지 않도록 제한
    currentCombo = Mathf.Clamp(currentCombo, 0, 3);
}

private bool IsOngoingAttack(int animatorHash) {
    return GetAnimatorState().shortNameHash == animatorHash &&
           GetAnimatorState().normalizedTime > 0.3f &&
           GetAnimatorState().normalizedTime < 0.7f;
}

//매번 animator.GetCurrentAnimatorStateInfo(0)를 쓰면 코드가 길어져서 함수를 사용했다.
private AnimatorStateInfo GetAnimatorState() => animator.GetCurrentAnimatorStateInfo(0);

 

공격시 조이스틱의 방향으로 플레이어가 바라보고 공격할 수 있도록 Vector3 값을 받아 해당 방향으로 플레이어를 회전시키도록 했다.

private void LookAtTarget(Vector3 direction) {
    if (direction == Vector3.zero) {
        return;
    }

    transform.localRotation = Quaternion.LookRotation(direction);
}

 

현재 애니메이션 상태가 attack_1 ~ 3가 아니라면 false를 리턴하는 함수도 추가했다. 앞에서 attack_1 ~ 3에 대한 hash를 readonly로 선언해 놓았기 때문에 shortNameHash를 사용하여 현재 애니메이션 상태를 체크했다. (IsName("attack_1 ~ 3")과 같다)

private bool IsEndAttack() {
    return GetAnimatorState().shortNameHash != ATTACK_1 &&
           GetAnimatorState().shortNameHash != ATTACK_2 &&
           GetAnimatorState().shortNameHash != ATTACK_3;
}

 

위 함수들을 이용하여 Execute 함수를 아래와 같이 수정했다.

public void Execute(Vector3 direction) {
    if (IsEndAttack()) {
        ResetComboCount();
    }

    LookAtTarget(direction);
    Attack();
}

 

이전 공격은 정지 상태에서 공격을 하다보니 이동과 공격을 반복할 때 움직임이 답답하고 부자연스러운 느낌이 들었다. 그래서 공격시 공격 방향으로 나아가면서 공격하도록 수정하여 좀 더 자연스럽게 보이도록 했다. 각 애니메이션의 모션이 달라서 공격 방향으로 움직이는 시간은 각 애니메이션에 맞에 조정했다. 애니메이션 상태가 70% 일때는 attack_1 ~3 상태를 false로 변경하여 Idle 상태로 돌아가도록 구현했다. 

[SerializeField] private float attackMove = 8f;

public void FixedUpdate() {
    if (GetAnimatorState().shortNameHash == ATTACK_1 && GetAnimatorState().normalizedTime < 0.1f) {
        transform.position += transform.forward.normalized * (Time.deltaTime * attackMove);
    }

    if (GetAnimatorState().shortNameHash == ATTACK_2 && GetAnimatorState().normalizedTime < 0.6f) {
        transform.position += transform.forward.normalized * (Time.deltaTime * attackMove);
    }

    if (GetAnimatorState().shortNameHash == ATTACK_3 && GetAnimatorState().normalizedTime < 0.5f) {
        transform.position += transform.forward.normalized * (Time.deltaTime * attackMove);
    }

    if (isAttacking && GetAnimatorState().IsName($"attack_{currentCombo}") &&
        GetAnimatorState().normalizedTime > 0.7f) {
        isAttacking = false;
        ResetComboCount();
    }
}

 

애니메이션과 이펙트 

이펙트는 몬스터에 데미지를 입히는 시점에 같이 플레이 되도록 했다. 애니메이션에  OnHit 이벤트를 붙여 몬스터에 데미지를 입히고 이펙트도 플레이되도록 했다. 

[SerializeField] private ParticleSystem attackEffect_1;
[SerializeField] private ParticleSystem attackEffect_2;
[SerializeField] private ParticleSystem attackEffect_3;

private void OnHit() {
    TakeDamageToEnemies();
    ShowAttackEffect();
}

private void ShowAttackEffect() {
    if (IsOngoingAttack(ATTACK_1)) {
        attackEffect_1.Play();
    } else if (IsOngoingAttack(ATTACK_2)) {
        attackEffect_2.Play();
    } else if (IsOngoingAttack(ATTACK_3)) {
        attackEffect_3.Play();
    }
}

 

결과

확실히 이전 움직임 보다는 더 자연스러워진 것 같다. 공격 모션도 더 빨라져서 그런지 답답한 느낌도 나아진 것 같다 ..

수정되기 전(왼쪽)과 수정된 후(오른쪽)

 

사용 에셋

Character

POLYGON Modular Fantasy Hero Characters (Synty Studios)

POLYGON Fantasy Rivals (Synty Studios)

 

Animation

Oriental Sword AnimSet (wemakethegame)

 

VFX

Magic Arsenal (Magic Arsenal)

 

UI

GUI PRO Kit - Fantasy RPG (Layer Lab)