Posted in: Unity, 프로젝트 When the Spring comes

14. 플레이어 기본 근접 공격 1타 2타 구분 짓기

전에 만들었던 플레이어 공격 기능은 타수 구분이 없었다. 몬스터가 범위 안으로 들어오고 공격 버튼을 광클하면 광클한 만큼 체력이 감소되었다.

하지만 우리가 만들고 있는 게임의 기본 근접 공격은 타수가 존재한다. 1타 2타가 존재하여 공격 시간이 다르고 공격 모션도 다르다. 공격 모션은 애니메이션으로 해결해야겠지만 1타 2타 공격 기능은 스크립트로 만들어야 했다.

/*공격 기능 스크립트*/

private void Update() {
    checkTime += Time.deltaTime;
}

private void FixedUpdate() {
    if(!playerAnime.secondAttack)
    {
        if(Input.GetKeyDown(KeyCode.K))
        {
            if(checkTime >= attackDelay)
            {
                GameManager.GetInstance().playerIsBasicAttack = true;

                if(attack) BasicAttack(monsterStat); 

                checkTime = 0;
            }
        }            
    }
    else if(playerAnime.secondAttack)
    {
        //2타 애니메이션 시간 0.267
        if(checkTime >= 0.27f)
        {
            if(attack) BasicAttack(monsterStat);

            checkTime = 0;
        }
        checkTime = 0;
    }
}

공격 타수를 적용하기 위해 여러 방법이 있겠지만 나는 위와 같은 방법으로 적용하였다. 핵심 기능은 Update() 에서 시간을 측정해주는 checkTime 변수와 공격 딜레이 시간을 결정하는 attackDelay 변수이다.

공격 버튼을 입력하면 공격 딜레이 시간과 checkTime을 비교해 checkTime이 공격 딜레이 시간보다 더 크면 공격 기능이 발동한다. 물론 checkTime이 0으로 초기화되지 않고 계속 증가한다면 끝도 없이 공격 기능을 호출할 수 있으므로 공격 기능이 발동할 때 checkTime을 0으로 초기화해야 한다.

중요한 것은 2타 공격인데 2타 공격은 애니메이션과 연계해서 만들었다.

/* 애니메이션 스크립트 */

public void AttackAnime()
{
    if(GameManager.GetInstance().playerIsBasicAttack == true)
        animator.SetBool("attacking", true);
    else if(GameManager.GetInstance().playerIsBasicAttack == false)
        animator.SetBool("attacking", false);

    if(!secondAttack)
    {
        if(animator.GetCurrentAnimatorStateInfo(0).IsName("AttackUpLeft_1") && 
            animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f)
        {
            GameManager.GetInstance().playerIsBasicAttack = false;
            animator.SetBool("attacking", false);
        }

        //1타 애니메이션이 끝나기 전
        //짧은 타이밍에 공격 키를 입력한다면
        //2타 애니메이션 출력하게 하는 코드
        if(animator.GetCurrentAnimatorStateInfo(0).IsName("AttackUpLeft_1") &&
            animator.GetCurrentAnimatorStateInfo(0).normalizedTime < 1.0f &&
            animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.7f)
        {
            if(Input.GetKeyDown(KeyCode.K))
            {
                secondAttack = true;
                animator.SetBool("secondAttack", true);
                Debug.Log("2타 애니메이션");
            }
        }

    if(animator.GetCurrentAnimatorStateInfo(0).IsName("AttackUpLeft_2") &&
        animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f)
    {
        //Debug.Log("공격 애니메이션 끝!");
        secondAttack = false;
        animator.SetBool("secondAttack", false);
        animator.SetBool("attacking", false);
        GameManager.GetInstance().playerIsBasicAttack = false;
    }

    if(animator.GetCurrentAnimatorStateInfo(0).IsName("AttackUpLeft_2R") &&
        animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f)
    {
        //Debug.Log("공격 애니메이션 끝!");
        secondAttack = false;
        animator.SetBool("secondAttack", false);
        animator.SetBool("attacking", false);
        GameManager.GetInstance().playerIsBasicAttack = false;
    }

공격 기능이 호출되면 게임매니저의 playerIsBasicAttack 변수가 true가 되고 1타 공격 애니메이션이 호출된다. 공격 애니메이션이 끝난다면 playerIsBasicAttack을 false로 전환해 주는 것이 중요하다. 1타 애니메이션뿐만 아니라 2타 애니메이션이 끝나도 마찬가지다.

2타 애니메이션이 출력되는 과정은 애니메이션 스크립트에서 1타 공격 애니메이션이 끝나기 직전에 다시 공격 키를 누른다면 secondAttack bool 변수가 true가 된다.

secondAttack 변수에 의해 공격 스크립트에서 2타 공격이 되는데 2타 애니메이션 시간에 따라 0.27초 이상이 흘렀을 때 공격 기능이 호출되게 작성하였다. 물론 secondAttack이 계속 true 상태라면 계속 공격 기능이 호출되므로 애니메이션 스크립트에서 2타 애니메이션이 끝나면 false로 바꾸어 주어야 하고 2타 공격이 호출되면 checkTime도 확실하게 0으로 만들어야 한다.

타수 구분이 필요한 공격 기능 스크립트를 만들 때는 공격 딜레이 변수를 통해 공격 가능 시간과 공격 불가 시간을 제어하는 것이 중요하다. 2타 공격은 1타 공격 애니메이션이 출력되고 끝나기 직전에 다시 공격 키를 입력한다면 bool 변수를 통해 공격 기능이 호출되는 식으로 만드는 것이다.

물론 각 타수마다 공격이 끝나고 나서 다시 일정 딜레이 시간이 흐르고 공격이 가능하도록 시간 확인 변수와 bool 변수를 적절하게 제어하는 것이 필요하다.

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

public class Weapon_1 : MonoBehaviour
{
    public float viewAngle;
    public float distance;
    private float maxDistance;
    public LayerMask targetMask;

    public MonsterStat monsterStat;
    public PlayerStat playerStat;
    private SphereCollider sphereCollider;
    public bool attack;
    public float attackDelay;
    public float checkTime;
    private PlayerAnime playerAnime;

    private void Start() {
        maxDistance = 10.0f;

        attackDelay = 0.64f;
        
        playerStat = GetComponentInParent<PlayerStat>();
        sphereCollider = GetComponent<SphereCollider>();
        playerAnime = GetComponentInParent<PlayerAnime>();
    }

    private void Update() {
        DrawView();

        sphereCollider.radius = distance;
        checkTime += Time.deltaTime;
    }

    private void FixedUpdate() {
        if(!playerAnime.secondAttack)
        {
            if(Input.GetKeyDown(KeyCode.K))
            {
                if(checkTime >= attackDelay)
                {
                    GameManager.GetInstance().playerIsBasicAttack = true;

                    if(attack) BasicAttack(monsterStat); 

                    checkTime = 0;
                }
            }            
        }
        else if(playerAnime.secondAttack)
        {
            if(checkTime >= 0.27f)
            {
                if(attack) BasicAttack(monsterStat);

                checkTime = 0;
            }
            checkTime = 0;
        }

    }

    public Vector3 DirFromAngle(float angleInDegrees) {

        if(GameManager.GetInstance().playerDirection == new Vector3(-1.14f, 0, 1.14f))//UpLeft
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(0, 0, 2.0f))//Up
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(1.14f, 0, 1.14f))//UpRight
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(-2.0f, 0, 0))//Left
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(2.0f, 0, 0))//Right
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(-1.14f, 0, -1.14f))//DownLeft
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(0, 0, -2.0f))//Down
            angleInDegrees += GameManager.GetInstance().attackAngle;
        if(GameManager.GetInstance().playerDirection == new Vector3(1.14f, 0, -1.14f))//DownRight
            angleInDegrees += GameManager.GetInstance().attackAngle;

        return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
    }

    public void DrawView() {
        Vector3 leftBoundary = DirFromAngle(-viewAngle / 2);
        Vector3 rightBoundary = DirFromAngle(viewAngle / 2);

        Debug.DrawLine(transform.position, transform.position + leftBoundary * distance, Color.green);
        Debug.DrawLine(transform.position, transform.position + rightBoundary * distance, Color.green);
        Debug.DrawLine(transform.position + leftBoundary * distance, transform.position + rightBoundary * distance, Color.green);
    }

    public void FindVisibleTargets() {
        Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

        for(int i = 0; i < targets.Length; ++i)
        {
            Vector3 dirToTarget = (targets[i].transform.position - transform.position).normalized;

            if(Vector3.Dot(GameManager.GetInstance().playerDirection.normalized, dirToTarget) > Mathf.Cos((viewAngle / 2) * Mathf.Deg2Rad))
            {
                monsterStat = targets[i].GetComponent<MonsterStat>();

                attack = true;
                Debug.DrawLine(transform.position, targets[i].transform.position, Color.red);
            }
            else
            {
                attack = false;
            }
        } 
    }

    void BasicAttack(MonsterStat monster) {
        monster.health -= playerStat.damage;
    }

    private void OnTriggerStay(Collider other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Monster"))
        {
            FindVisibleTargets();
        }
    }
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다