프로젝트/프로젝트 A

#019 Enemy (3) (whit. Combat)

효따 2025. 8. 24. 17:47

안녕하세요. 😃

 

오늘은 지난번에 이어 Enemy 작업을 해주기로 했습니다.

 

 

우선 먼저 enemy가 포탑에 근접하였을 때

 

포탑을 향해 공격을 하며 HpBar를 깎아주는 작업입니다.

 

 

지난번 작성한 EnumyMovement > EnemyMovement 스크립트에서 변수를 추가해 주었습니다.

using UnityEngine;
using UnityEngine.AI;

public class EnemyMovement : MonoBehaviour
{
    [SerializeField] private EnemyStats enemyStats;
    [SerializeField] private Animator enumyAnim;
    [SerializeField] private NavMeshAgent agent;
    [SerializeField] private string minionTag = "EnemyMinion";
    [SerializeField] private string turretTag = "Turret";

    [Header("Targeting Settings")]
    [SerializeField] private float stopDistance = 2.0f;
    [SerializeField] private float aggroRange = 5.0f;
    [SerializeField] private float targetSwitchInterval = 2.0f;

    [Header("Attack Settings")]
    [SerializeField] private float attackCooldown = 1.5f;  
    private float lastAttackTime = 0f;                      

    private Transform currentTarget;
    private float timeSinceLastTargetSwitch = 0.0f;

    void Start()
    {
        agent.stoppingDistance = stopDistance;
        UpdateTarget();
    }

    void Update()
    {
        timeSinceLastTargetSwitch += Time.deltaTime;

        if (timeSinceLastTargetSwitch >= targetSwitchInterval)
        {
            UpdateTarget();
            timeSinceLastTargetSwitch = 0.0f;
        }

        if (currentTarget != null)
        {
            agent.SetDestination(currentTarget.position);

            float distance = Vector3.Distance(transform.position, currentTarget.position);

            // 사거리 안에 들어왔을 때 공격
            if (distance <= stopDistance + 0.1f)
            {
                TryAttack();
            }
        }
        else
        {
            Debug.LogWarning($"{gameObject.name} has no target to move towards.");
        }
    }

    private void TryAttack()
    {
        print("222");
        if (Time.time >= lastAttackTime + attackCooldown)
        {
            lastAttackTime = Time.time;
            if (currentTarget.tag == minionTag)
            {
                currentTarget.GetComponent<EnemyStats>().TakeDamage(enemyStats.enemyDamage);
            }
            else if (currentTarget.tag == turretTag)
            {
                currentTarget.GetComponent<TurrentStats>().TakeDamage(enemyStats.enemyDamage);
            }
            enumyAnim.SetTrigger("IsAttack");
        }
    }

    private void UpdateTarget()
    {
        Transform closestMinion = GetClosestObjectInRadius(GameObject.FindGameObjectsWithTag(minionTag), aggroRange);

        if (closestMinion != null)
        {
            currentTarget = closestMinion;
        }
        else
        {
            currentTarget = GetClosestObject(GameObject.FindGameObjectsWithTag(turretTag));
        }
    }

    private Transform GetClosestObject(GameObject[] objects)
    {
        float closestDistance = Mathf.Infinity;
        Transform closestObject = null;
        Vector3 currentPosition = transform.position;

        foreach (var obj in objects)
        {
            float distance = Vector3.Distance(currentPosition, obj.transform.position);

            if (distance < closestDistance)
            {
                closestDistance = distance;
                closestObject = obj.transform;
            }
        }
        return closestObject;
    }

    private Transform GetClosestObjectInRadius(GameObject[] objects, float radius)
    {
        float closestDistance = Mathf.Infinity;
        Transform closestObject = null;
        Vector3 currentPosition = transform.position;

        foreach (var obj in objects)
        {
            float distance = Vector3.Distance(currentPosition, obj.transform.position);

            if (distance < closestDistance && distance <= radius)
            {
                closestDistance = distance;
                closestObject = obj.transform;
            }
        }
        return closestObject;
    }
}

 


 

1. 추가된 변수 선언부입니다.

    [SerializeField] private float attackCooldown = 1.5f;   // 공격 간격 (초)
    private float lastAttackTime = 0f;                      // 마지막 공격 시각

 

attackCooldown : 공격 간격

lastAttackTime : 마지막 공격 시각

 


 

 

2. Update() 함수 내부입니다.

            float distance = Vector3.Distance(transform.position, currentTarget.position);

            // 사거리 안에 들어왔을 때 공격
            if (distance <= stopDistance + 0.1f)
            {
                TryAttack();
            }

 

현재 포지션과 Target의 위치를 지역변수 distance에 담아 사용하였습니다.

 

 


 

 

3. TryAttack() 함수입니다.

    private void TryAttack()
    {
        if (Time.time >= lastAttackTime + attackCooldown)
        {
            lastAttackTime = Time.time;
            if (currentTarget.tag == minionTag)
            {
                currentTarget.GetComponent<EnemyStats>().TakeDamage(enemyStats.enemyDamage);
            }
            else if (currentTarget.tag == turretTag)
            {
                currentTarget.GetComponent<TurrentStats>().TakeDamage(enemyStats.enemyDamage);
            }
            enumyAnim.SetTrigger("IsAttack");
        }
    }

 

마지막 공격 시각 + 쿨타임이 흐른 시간보다 크다면 실행되며

 

공격 대상은 Tag를 지정해 사용했습니다.

(미니언 / 포탑)

 

+

애니메이션을 추가시켜 주어 시각적인 효과를 더해주었습니다.

 

 


 

 

TurretStats 스크립트입니다.

using UnityEngine;

public class TurretStats : MonoBehaviour
{
    public float damage = 5.0f;

    public float turretMaxHp = 200.0f;
    public float turretHp = 0f;
    [SerializeField] private TurretHpBar turretHpBar;

    void Start()
    {
        turretMaxHp = turretHp;
    }

    public void TakeDamage(float damage)
    {
        turretHp -= damage;
        turretHpBar.HpVarUpdate(turretHp, turretMaxHp);
        if (turretHp <= 0)
        {
            Destroy(this.gameObject);
        }
    }
}

 


 

TakeDamage() 함수 내에서 현재 포탑의 Hp를 받은 damage 만큼 깎아주며

 

현재는 포탑의 Hp가 0보다 작거나 같다면 Destroy로 파괴를 시켜주고있습니다.

 

 

또한 HpVar가 업데이트되는 부분은 따로 스크립트를 작성하여 관리해 주었습니다.

 


 

TurretHpBar 스크립트입니다.

using UnityEngine;
using UnityEngine.UI;

public class TurretHpBar : MonoBehaviour
{
    [SerializeField] private Slider turretHpBar;

    public void HpVarUpdate(float hp, float maxHp)
    {
        turretHpBar.value = hp / maxHp;
    }
}

 

포탑에 붙어있는 Slider를 가져와 Value를 조정해 주었습니다.

 


 

다음은 Enemy에 더해 적군으로 사용될 Minion 프리팹을 만들어 주었습니다.

 


 

기존 private로 사용하던 Tag 변수를 [SerializeField]를 활용해 인스펙터창에 노출시켜 주었습니다.

 

기존 Enemy Tag

 

새로 생성한 MinionEnemy Tag

 

 

이후 맵에 배치를 해서 각 태그를 맞춰주며 작업을 했습니다.

 

+

포탑(Turret)의 범위는 HpVar가 있는 Canvas에 Image를 추가시켜 주었습니다.

 

 


 

 

 

현재는 enemy들의 공격력은

왼(4) / 오(2)입니다.

 

원래는 공격력이 같지만, 포탑을 때리는 과정까지 조금 시간이 걸리기에

임시로 준 연출효과입니다.

 


 

감사합니다. 😁

 

 

 

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

# 021 Golem (2) (with. PlayerHpBar)  (7) 2025.08.26
# 020 Golem (1) (with. State)  (2) 2025.08.25
# 018 Login (with. Firebase)  (2) 2025.08.23
# 017 Enemy (2) (with. Turret)  (0) 2025.08.23
# 016 Enemy Spawn & movement  (1) 2025.08.21