프로젝트/프로젝트 A

# 017 Enemy (2) (with. Turret)

효따 2025. 8. 23. 01:45

안녕하세요.😎

 

오늘은 Enemy에 관련된 작업 중 포탑(Turret)의 공격과 Enemy의 Hp가 줄어드는 작업을 할 예정입니다.

 

바로 코드부터 보겠습니다.

 

 


 

 

TurretAttackObject Script

이 스크립트는 포탑(Turret)가 발사하는 오브젝트를 제어하는 역할입니다.

using UnityEngine;

public class TurretAttackObject : MonoBehaviour
{
    [SerializeField] private TurrentStats turrentStats;
    [SerializeField] private float moveSpeed = 10.0f;
    private Transform targetTransform;

    public void Initialize(Transform target)
    {
        targetTransform = target;
    }

    private void Update()
    {
        if (targetTransform == null)
        {
            Destroy(gameObject);
            return;
        }

        Vector3 direction = targetTransform.position - transform.position;
        float step = moveSpeed * Time.deltaTime;

        if (direction.magnitude <= step)
        {
            HitTarget(targetTransform.gameObject);
            return;
        }

        transform.Translate(direction.normalized * step, Space.World);
    }

    private void HitTarget(GameObject target)
    {
        print(target.name);
        target.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.transform == targetTransform)
        {
            other.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
            gameObject.SetActive(false);
        }
    }
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 

주 목적은 다음과 같습니다.

 

1) 타겟을 향해 직선으로 이동

2) 타겟과 충돌 시 대미지 적용

3) 타겟이 사라지면 발사체 정리

 


 

1. 변수 선언부

    [SerializeField] private TurrentStats turretStats;
    [SerializeField] private float moveSpeed = 10.0f;
    private Transform targetTransform;

 

turretStats : 타워의 공격력 정보를 가져오기 위해 사용

moveSpeed : 발사체 속도 사용

targetTransform : 발사체가 따라갈 타깃

 

 

2. 타깃 초기화

    public void Initialize(Transform target)
    {
        targetTransform = target;
    }

 

발사체를 생성할 때 어떤 적을 목표로 삼을지 설정

 

 

3. 이동 로직

    private void Update()
    {
        if (targetTransform == null)
        {
            Destroy(gameObject);
            return;
        }

        Vector3 direction = targetTransform.position - transform.position;
        float step = moveSpeed * Time.deltaTime;

        if (direction.magnitude <= step)
        {
            HitTarget(targetTransform.gameObject);
            return;
        }

        transform.Translate(direction.normalized * step, Space.World);
    }

 

1) 타깃이 없으면 발사체 제거 (Destory)

2) 타깃까지의 방향벡터 계산 (direction)

3) 한 프레임 이동 거리 계산 (step)

4) 타깃과 너무 가까우면 바로 타격 적용 (HitTarget)

5) 아니면 transform.Translate로 타겟 방향으로 이동

 

 

4. 타격 처리

    private void HitTarget(GameObject target)
    {
        print(target.name);
        target.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
    }

 

1) 타깃에 데미지 적용

2) 로그를 찍어 디버깅 가능

3) 타겟 자체는 여기서 비활성화/삭제하지 않고 충돌에서 처리

 

 

5. 충돌 처리

    private void OnTriggerEnter(Collider other)
    {
        if (other.transform == targetTransform)
        {
            other.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
            gameObject.SetActive(false);
        }
    }

 

1) Collider 기반 충돌 처리

2) 타깃과 충돌 시 타깃에게 대미지 적용

 


 

 

 

TurretController Scirpt

이 스크립트는 포탑(Turret) 자체를 제어합니다.

using UnityEngine;

public class TurretController : MonoBehaviour
{
    [SerializeField] private TurretAttackObject turretAttackObject;
    [Header("Attack Settings")]
    [SerializeField] private float attackRange = 5.0f;
    [SerializeField] private float attackInterval = 2.0f;
    [SerializeField] private GameObject projectile;
    [SerializeField] private Transform firePoint;

    [Header("Visuals")]
    [SerializeField] private LineRenderer targetingLine;

    private float nextAttackTime = 0f;
    private GameObject currentTarget;

    private void Update()
    {
        if (currentTarget == null || Vector3.Distance(transform.position, currentTarget.transform.position) > attackRange)
        {
            AcquireTarget();
        }

        UpdateTargetingLine();

        if (currentTarget != null && Time.time >= nextAttackTime)
        {
            FireAtTarget();
            nextAttackTime = Time.time + attackInterval;
        }
    }

    private void AcquireTarget()
    {
        GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enumy");
        float closestDistance = float.MaxValue;
        GameObject nearestTarget = null;

        foreach (var enemy in enemies)
        {
            float distance = Vector3.Distance(transform.position, enemy.transform.position);
            if (distance <= attackRange && distance < closestDistance)
            {
                closestDistance = distance;
                nearestTarget = enemy;
            }
        }

        currentTarget = nearestTarget;
    }

    private void UpdateTargetingLine()
    {
        if (currentTarget != null)
        {
            targetingLine.enabled = true;
            targetingLine.SetPosition(0, firePoint.position);
            targetingLine.SetPosition(1, currentTarget.transform.position);
        }
        else
        {
            targetingLine.enabled = false;
        }
    }

    private void FireAtTarget()
    {
        projectile.transform.position = firePoint.position;
        projectile.transform.rotation = Quaternion.identity;
        projectile.gameObject.SetActive(true);
        if (projectile != null)
        {
            turretAttackObject.Initialize(currentTarget.transform);
        }
    }
}

 

주목적은 다음과 같습니다.

 

1) 적을 탐지하고 타깃 지정

2) 타깃을 향해 발사체 발사

3) 시각적으로 공격 범위를 표현

 


 

 

1. 변수 선언부

    [SerializeField] private TurretAttackObject turretAttackObject;
    [Header("Attack Settings")]
    [SerializeField] private float attackRange = 5.0f;
    [SerializeField] private float attackInterval = 2.0f;
    [SerializeField] private GameObject projectile;
    [SerializeField] private Transform firePoint;

    [Header("Visuals")]
    [SerializeField] private LineRenderer targetingLine;

    private float nextAttackTime = 0f;
    private GameObject currentTarget;

 

turretAttackObject : 발사체 스크립트 참조

attackRange : 타겟 감지 거리

attackInterval : 공격 간격

projectile : 발사체 오브젝트

firePoint : 발사 위치

targetingLine : 발사체가 타겟을 조준하는 시각 선

nextAttackTime : 공격 쿨타운 체크

currentTarget : 현재 공격할 적

 

 

2. 타겟 관리 및 공격

    private void Update()
    {
        if (currentTarget == null || Vector3.Distance(transform.position, currentTarget.transform.position) > attackRange)
        {
            AcquireTarget();
        }

        UpdateTargetingLine();

        if (currentTarget != null && Time.time >= nextAttackTime)
        {
            FireAtTarget();
            nextAttackTime = Time.time + attackInterval;
        }
    }

 

1) 타깃이 없거나 공격 범위를 벗어나면 새 타겟 탐색 (AcquireTarget())

2) 시각적으로 타겟 표시 (UpdateTargetingLine())

3) 쿨다운 체크 후 발사 (FireAtTarget())

 

 

3. 타겟 획득

    private void AcquireTarget()
    {
        GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enumy");
        float closestDistance = float.MaxValue;
        GameObject nearestTarget = null;

        foreach (var enemy in enemies)
        {
            float distance = Vector3.Distance(transform.position, enemy.transform.position);
            if (distance <= attackRange && distance < closestDistance)
            {
                closestDistance = distance;
                nearestTarget = enemy;
            }
        }

        currentTarget = nearestTarget;
    }

 

1) 모든 적(Enumy 태그) 탐색

2) 타워 기준 가장 가까운 적을 선택

3) 공격 범위를 넘어가면 타겟 재선정

 

 

4. 타겟 라인 표시

    private void UpdateTargetingLine()
    {
        if (currentTarget != null)
        {
            targetingLine.enabled = true;
            targetingLine.SetPosition(0, firePoint.position);
            targetingLine.SetPosition(1, currentTarget.transform.position);
        }
        else
        {
            targetingLine.enabled = false;
        }
    }

 

1) 타겟이 있으면 LineRenderer로 조준선 표시

2) 타겟이 없으면 라인 비활성화

 

 

5. 발사 처리

    private void FireAtTarget()
    {
        projectile.transform.position = firePoint.position;
        projectile.transform.rotation = Quaternion.identity;
        projectile.gameObject.SetActive(true);
        if (projectile != null)
        {
            turretAttackObject.Initialize(currentTarget.transform);
        }
    }

 

1) 발사체 위치를 발사 지점으로 이동

2) 발사체 활성화

3) TurretAttackObject를 통해 타겟 방향으로 이동하도록 초기화

 

전체 흐름

 

타겟이 없거나 범위 밖 → AcquireTarget()

타겟이 있으면 → UpdateTargetingLine()으로 시각화

쿨다운 체크 → FireAtTarget()으로 발사

발사체가 TurretAttackObject에서 이동 및 충돌 처리

 


 

 

마지막으로..

 

여러 가지 문제들이 많았는데, 그중 가장 기억에 남는 게

 

플레이어가 기본공격을 할 때 포탑에 의해 Enemy가 사라지면 

 

플레이어의 공격 오브젝트가 멈춰있는 문제가 있었습니다.

 

 

 

오브젝트 재사용 문제

 

우선 결론적으로 플레이어의 기본공격 오브젝트의 움직임과 충돌처리를 담당하는

 

기존에 작성했던 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;
        }
        else
        {
            gameObject.SetActive(false);
        }
    }
    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);
        }
    }
}

 

공격 모션을 플레이어가 취하고 오브젝트가 생성되었을 때, 포탑이나 혹은 다른 요소로 인하여 Enemy가 사라진다면

 

비활성화를 되게 로직을 구현하였습니다.

 

 

정말 단순한 문제였지만, 접근을 잘못하여서 OnEnable() 함수를 활용하여

 

오브젝트가 활성화될 때 먼저 모든 오브젝트를 비활성화하는 로직부터 시작해서.. 

 

 

사용자가 마우스 오른쪽(1) 클릭을 할 때 Enemy의 Transform을 받아오는 로직까지

 

수정을 하려고 했었습니다..

 


 

수정 도중 잘 해결이 되지 않아서

 

잠시 기분 전환을 하고 다시 스크립트를 살펴봤는데

 

 

문득 '다 필요 없고 이거 한 줄이면 되는 거 아닌가?'라는 생각에 

 

시도를 해봤는데 해결되었습니다.😂

 

 


 

 

 

 

 

감사합니다.😁

 

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

#019 Enemy (3) (whit. Combat)  (6) 2025.08.24
# 018 Login (with. Firebase)  (2) 2025.08.23
# 016 Enemy Spawn & movement  (1) 2025.08.21
# 015 Development (with. feature additions)  (1) 2025.08.20
#014 Inventory (With. Store)  (2) 2025.08.18