프로젝트/프로젝트 A

# 008 Attack (with. Animation)

효따 2025. 8. 15. 02:46

안녕하세요. 💪

 

이번 작업은 플레이어의 기본 공격과 애니메이션 작업을 병행해 주었습니다.

 

 

가장 먼저 애니메이션은 믹사모에서 무료로 가져와 주었습니다.

링크 : https://www.mixamo.com

 

Mixamo

 

www.mixamo.com

 

 


 

 

다음은 플레이어의 애니메이터에 "IsAttack" 파라미터를 생성해 주어

 

애니메이션 "Attack"의 전이 조건으로 true/false로 설정해 주었습니다.

 

 


 

다음은 스크립트입니다.

 

"PlayerAttack" 스크립트를 만들어 주었으며 

하나씩 살펴보겠습니다.

using System.Collections;
using UnityEngine;

public class PlayerAttack : MonoBehaviour
{    
    [Header("Player Components")]
    [SerializeField] private PlayerMovement playerMovement;
    [SerializeField] private PlayerStats playerStats;
    [SerializeField] private Animator playerAnim;

    [Header("Player Setting Value")]
    public bool performMeleeAttack = true;
    private float attackInterval;
    private float nextAttackTime = 0;

    [Header("Enumy Target Setting")]
    public GameObject enumyGameobject;
    [SerializeField] private  GameObject[] basicAttackObjects;
    private GameObject currentBasicAttackObject;
    [SerializeField] private Vector3 attackSpawnPoint;

    void Update()
    {
        attackInterval = playerStats.playerAttackSpeed / ((500 + playerStats.playerAttackSpeed) * 0.01f);

        if (playerMovement.enumyGameobject != null)
        {
            enumyGameobject = playerMovement.enumyGameobject.gameObject;
        }
        else
        {
            enumyGameobject = null;
        }

        if (enumyGameobject != null && performMeleeAttack && Time.time > nextAttackTime)
        {
            if (Vector3.Distance(transform.position, enumyGameobject.transform.position) <= playerMovement.stopDistance)
            {
                StartCoroutine(PlayerAttackInterval());
            }
        }
    }
    private IEnumerator PlayerAttackInterval()
    {
        performMeleeAttack = false;
        playerAnim.SetBool("IsAttack", true);
        yield return new WaitForSeconds(attackInterval);
        if (enumyGameobject == null)
        {
            playerAnim.SetBool("IsAttack", false);
            performMeleeAttack = true;
        }
    }

    public void OnPlayerAttack()
    {
        if (enumyGameobject != null)
        {
            GetBasicAttacjObject();

            if (currentBasicAttackObject != null)
            {
                PlayerTargetObject playerTargetObject = currentBasicAttackObject.GetComponent<PlayerTargetObject>();

                if (playerTargetObject != null)
                {
                    playerTargetObject.SetTarget(enumyGameobject.transform);
                }
            }

            nextAttackTime = Time.time + attackInterval;
            performMeleeAttack = true;
            playerAnim.SetBool("IsAttack", false);
        }
    }

    private void GetBasicAttacjObject()
    {
        foreach (var basicAttackObject in basicAttackObjects)
        {
            if (!basicAttackObject.activeSelf)
            {
                basicAttackObject.transform.position = transform.position + attackSpawnPoint;
                currentBasicAttackObject = basicAttackObject;
                basicAttackObject.SetActive(true);
                return;
            }
        }
    }
}

 

 

1. 플레어와 관련된 클래스와 컴포넌트입니다.

playerMovement : 플레이어 이동과 적 추적 정보를 가져오기 위함입니다.

playerStats : 플레이어의 기본 스텟을 가져오기 위함입니다.

playerAnim : 플레이어의 애니메이션을 사용하기 위함입니다.

    [SerializeField] private PlayerMovement playerMovement;
    [SerializeField] private PlayerStats playerStats;
    [SerializeField] private Animator playerPlayerAnim;

 

 

2. 공격 상태와 쿨타임 변수입니다.

perFormMeleeAttack : 공격 중인지 판단할 플래그입니다.

attackInterval : 현재 공격 간격입니다.

nextAttackTime : 다음 공격 가능 시간입니다. (Time.time 기준)

    public bool performMeleeAttack = true;
    private float attackInterval;
    private float nextAttackTime = 0;

 

 

3. 타깃 및 기본 공격 관련 변수입니다.

enumyGameobject : 현재 공격 대상입니다.

 basicAttackObjects : 풀링 공격 오브젝트 배열입니다.

currentBasicObject : 공격에 사용할 현재 오브젝트입니다.

attackSpawnPoint : 플레이어 기준 공격 오브젝트의 초기 위치입니다.

    public GameObject enumyGameobject;
    [SerializeField] private  GameObject[] basicAttackObjects;
    private GameObject currentBasicAttackObject;
    [SerializeField] private Vector3 attackSpawnPoint;

 

 

4. Update 함수에서는 공격 실행 조건을 확인하며 타깃을 가져와 줍니다.

기본 ㅇㅇ는 뭐 뭐로 잡았고 현재 playerAttackSpeed는 3입니다.

공식을 보면 

(500 + 3) * 0.01 = 503 * 0.01

즉 5.03초가 됩니다.

3 / 5.03초를 하게 되면 0.596초가 됩니다.

아직 아이템이 추가되진 않았지만, 공격속도를 퍼센트로 관리할 생각이며 약 10%가 향상이 될 땐

float buffRatio = 0.9f; // 공격속도 +10%
float buffedAttackSpeed = playerStats.playerAttackSpeed * buffRatio;
attackInterval = buffedAttackSpeed / ((500 + buffedAttackSpeed) * 0.01f);

와 같은 방식으로 구현할 예정입니다.

void Update()
    {
        attackInterval = playerStats.playerAttackSpeed / ((500 + playerStats.playerAttackSpeed) * 0.01f);

        if (playerMovement.enumyGameobject != null)
        {
            enumyGameobject = playerMovement.enumyGameobject.gameObject;
        }
        else
        {
            enumyGameobject = null;
        }

        if (enumyGameobject != null && performMeleeAttack && Time.time > nextAttackTime)
        {
            if (Vector3.Distance(transform.position, enumyGameobject.transform.position) <= playerMovement.stopDistance)
            {
                StartCoroutine(PlayerAttackInterval());
            }
        }
    }

 

 

4-1 타깃 가져오기입니다.

플레이어 이동 컴포넌트에서 현재 추적 중인 적을 가져오며, 적이 없으면 공격 대상을 null로 처리해 주었습니다.

        if (playerMovement.enumyGameobject != null)
        {
            enumyGameobject = playerMovement.enumyGameobject.gameObject;
        }
        else
        {
            enumyGameobject = null;
        }

 

 

4-2 공격 실행 조건입니다.

조건문을 보면

if (공격 대상이 존재할 때, 공격 중이 아니고, 공격 가능한지 체크) 

를 합니다.

또한 거리체크는 Distance()를 활용해 현재 위치에서 타깃 오브젝트의 거리가 플레이어가 멈춰야 할 거리보다 작다면 

공격 범위에 해당한다고 판단하여 코루틴으로 공격을 실행해 줬습니다.

        if (enumyGameobject != null && performMeleeAttack && Time.time > nextAttackTime)
        {
            if (Vector3.Distance(transform.position, enumyGameobject.transform.position) <= playerMovement.stopDistance)
            {
                StartCoroutine(PlayerAttackInterval());
            }
        }

 

 

5. 공격 코루틴입니다.

공격 시작 시 performMeleeAttack을 즉시 false로 바꿔주어 중첩 실행을 방지하여 주었으며

공격 간격만큼 대기를 하게 되어 원하는 시간만큼 대기를 하게 해 주었습니다.

또한 만일 타깃 오브젝트가 null로 사라진다면 = 사용자가 "Ground"를 클릭했을 때

공격은 멈추게 해 주었습니다.

    private IEnumerator PlayerAttackInterval()
    {
        performMeleeAttack = false;
        playerAnim.SetBool("IsAttack", true);
        yield return new WaitForSeconds(attackInterval);
        if (enumyGameobject == null)
        {
            playerAnim.SetBool("IsAttack", false);
            performMeleeAttack = true;
        }
    }

 

 

6. 애니메이션 함수를 이벤트로 연결해 주었습니다.

먼저 GetBasicAttackObject() 함수는 기본 공격할 때 발사될 오브젝트를 간단한 풀링 방식으로 

활성/비활성화하는 함수입니다. (7.)에서 살펴보겠습니다.

 

또한 활성화가 된 오브젝트의 SetTarget() 함수에 접근하여 타깃 Enumy를 넘겨주었으며

nextAttackTime(공격 가능한 시간) = Time.time(흐른 시간) + attackInterval (공격 대기시간)

으로 공격 완료 후 쿨다운 적용 및 공격 상태 복귀를 해주었습니다.

    public void OnPlayerAttack()
    {
        if (enumyGameobject != null)
        {
            GetBasicAttacjObject();

            if (currentBasicAttackObject != null)
            {
                PlayerTargetObject playerTargetObject = currentBasicAttackObject.GetComponent<PlayerTargetObject>();

                if (playerTargetObject != null)
                {
                    playerTargetObject.SetTarget(enumyGameobject.transform);
                }
            }

            nextAttackTime = Time.time + attackInterval;
            performMeleeAttack = true;
            playerAnim.SetBool("IsAttack", false);
        }
    }

 

 

7. 오브젝트 풀링 함수입니다.

간단히 현재 배열을 순차적으로 반복하며 비활성화된 오브젝트가 있을 때

그 해당 오브젝트를 활성화시켜 주며 전역으로 선언한 currentBasicAttackObject에 대입해 줍니다.

 

또한 활성화된 오브젝트가 있다면 바로 return을 해주어 함수를 종료하게 해 주었습니다.

    private void GetBasicAttacjObject()
    {
        foreach (var basicAttackObject in basicAttackObjects)
        {
            if (!basicAttackObject.activeSelf)
            {
                basicAttackObject.transform.position = transform.position + attackSpawnPoint;
                currentBasicAttackObject = basicAttackObject;
                basicAttackObject.SetActive(true);
                return;
            }
        }
    }

 

 


 

다음은 "PlayerTargetObject" 스크립트입니다.

using UnityEngine;

public class PlayerTargetObject : MonoBehaviour
{
    [Header("Components")]
    [SerializeField] private Transform targetEnumyTr;    
    [SerializeField] private Rigidbody basicAttackRb;
    [SerializeField] private PlayerStats playerStats;

    [Header("Setting Value")]
    [SerializeField] private  float basicAttackSpeed = 0f;

    void FixedUpdate()
    {
        if (targetEnumyTr != null)
        {
            Vector3 direction = targetEnumyTr.position - transform.position;
            basicAttackRb.velocity = direction.normalized * basicAttackSpeed;
        }
    }
    public void SetTarget(Transform newTarget)
    {
        targetEnumyTr = newTarget;
    }
    void OnTriggerEnter(Collider other)
    {
        if (targetEnumyTr != null && ReferenceEquals(other.gameObject, targetEnumyTr.gameObject))
        {
            EnumyStats enumyStats = targetEnumyTr.GetComponent<EnumyStats>();
            enumyStats?.TakeDamage(playerStats.playerDamage);
            targetEnumyTr = null;
            this.gameObject.SetActive(false);            
        }
    }
}

 

 

1. 클래스와 컴포넌트 및 기본공격 오브젝트의 이동속도 변수를 선언해 주었습니다.

targetEnumyTr : 현재 공격 목표(Enumy)의 Transform입니다.

basicAttackRb : 기본 공격 오브젝트의 Rigidbody입니다.

playerStats : 플레이어의 기본 스텟이 담긴 스크립트입니다.

basicAttackSpeed : 오브젝트의 이동 속도입니다.

(높을수록 적에게 더 빨리 날아갑니다.)

    [Header("Components")]
    [SerializeField] private Transform targetEnumyTr;    
    [SerializeField] private Rigidbody basicAttackRb;
    [SerializeField] private PlayerStats playerStats;

    [Header("Setting Value")]
    [SerializeField] private  float basicAttackSpeed = 0f;

 

 

2. 기본공격 오브젝트의 이동은 RigidBody를 사용하였으며 FixedUpdate()에서 활용을 해주었습니다.

( FixedUpdate()를 사용한 이유는 물리 연산이 프레임과 독립적이게 안정적으로 수행되도록 하기 위함입니다.)

 

타깃이 존재할 때만 이동을 하게 되며

direction : 오브젝트와 타깃 간 방향을 계산해 주고

velocity : Rigidbody의 속도를 이용해 투사체를 이동시켜 주었습니다.

    void FixedUpdate()
    {
        if (targetEnumyTr != null)
        {
            Vector3 direction = targetEnumyTr.position - transform.position;
            basicAttackRb.velocity = direction.normalized * basicAttackSpeed;
        }
    }

 

 

3. 타깃 설정입니다.

위에서 본 PlayerAttack 스크립트에서, 선택된 Enumy를 인자로 넘겨주면

PlayerTargetObject의 SetTarget 함수에서는 그 해당 Enumy를 받아 제어를 해줍니다.

    public void SetTarget(Transform newTarget)
    {
        targetEnumyTr = newTarget;
    }

 

 

4. 충돌 처리입니다.

OnTriggerEnter()를 사용해 주었으며, 오브젝트가 타깃과 충돌했는지 검사합니다.

또한 ReferenceEquals()를 사용하여 정확히 같은 게임오브젝트인지 체크를 해주었습니다.

(ReferenceEquals(A, B) = A와 B 두 변수가 같은 메모리 주소를 가리키는지 검사합니다 

동일하면 true / 다른 객체면 false를 나타내어줍니다.)

 

또한 목표와 충돌하면 적에게 대미지를 적용하며, 오브젝트는 비활성화를 해주어 재사용을 준비하도록 해주었습니다.

    void OnTriggerEnter(Collider other
    {
        if (targetEnumyTr != null && ReferenceEquals(other.gameObject, targetEnumyTr.gameObject))
        {
            EnumyStats enumyStats = targetEnumyTr.GetComponent<EnumyStats>();
            enumyStats?.TakeDamage(playerStats.playerDamage);
            targetEnumyTr = null;
            this.gameObject.SetActive(false);            
        }
    }

 

 


 

 

다음은 "EnumyStats" 스크립트입니다.

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

public class EnumyStats : MonoBehaviour
{
    public float enumyHp = 100f;
    public float enumyDamage = 10f;
    public float enumyAttackSpeed = 3f;

    public void TakeDamage(float damage)
    {
        enumyHp -= damage;
        if (enumyHp <= 0)
        {
            Destroy(this.gameObject);
        }
    }
}

 

 

1. 클래스와 변수를 정의해 주었습니다.

enumyHp : Enumy의 기본 체력입니다.

enumyDamage : Enumy의 기본 대미지로 사용 예정입니다. 

enumyAttackSpeed : Enumy의 공격 속도로 사용 예정입니다.

    public float enumyHp = 100f;
    public float enumyDamage = 10f;
    public float enumyAttackSpeed = 3f;

 

 

2. 외부에서 공격을 받으면 실행될 메서드입니다.

현재의 체력에서 damage를 빼서 체력을 갱신시켜 주며

체력이 0보다 아래 거나 같다면 Destory로 현재 오브젝트를 파괴시켜 줍니다.

    public void TakeDamage(float damage)
    {
        enumyHp -= damage;
        if (enumyHp <= 0)
        {
            Destroy(this.gameObject);
        }
    }

(현재는 UI나 공격 등 아직 작업이 진행되지 않아서, 단순 확인용으로 임시 작성을 하였습니다.)

 

 


- 참고 -

Player의 현재 공격력 : 10

Player의 현재 공격 스피드 : 0.56

Player의 현재 공격 범위 : 5

 

Enumy의 현재 체력 : 100

 

 

 

다음은 UI와 Enumy의 이동 및 공격을 작업하려고 했는데, 시간이 어느덧 예정보다 너무 흘러서

 

잠을 청한뒤 이어서 작업을 해야 할 것 같습니다.😂

 

음...


 

 감사합니다. 😊

'프로젝트 > 프로젝트 A' 카테고리의 다른 글

# 010 Player Mp (with. Runtime Performance Stats)  (9) 2025.08.16
# 009 Enumy Hp (With. Skill Attack)  (4) 2025.08.15
#007 Follow (with. Enumy)  (5) 2025.08.13
#006 Click (with. Object Pooling)  (4) 2025.08.11
#005 Skill Motion 2~4  (1) 2025.08.10