Unity 프로젝트 이슈 해결 - 범위내 적 타게팅 최적화

2025. 11. 4. 16:25·Unity

수 많은 적을 탐지해 리스트에 담는 경우

 

뱀서라이크와 They are billions를 혼합한 디펜스 장르의 팀 프로젝트를 진행하던 도 중

Sphere collider를 이용해 타워 근처에 접근하는 적을 감지하여 공격하는 코드를 팀원이 작성한 바 있다.

 

using System.Collections.Generic;
using UnityEngine;

public class TowerTrigger : MonoBehaviour
{
    [SerializeField] private Transform _towerHead;
    [SerializeField] private float _rotationSpeed;
    private List<Transform> _targetEnemies = new List<Transform>();
    private Transform currentEnemy;

    private void Update()
    {
        RotationUpdate();
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Enemy"))
        {
            if (!_targetEnemies.Contains(other.transform))
            {
                _targetEnemies.Add(other.transform);
            }
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Enemy"))
        {
            if (_targetEnemies.Contains(other.transform))
            {
                _targetEnemies.Remove(other.transform);
            }
        }
    }
    public void CheckEmpty()
    {
        _targetEnemies.RemoveAll(emptyEnemy => emptyEnemy == null || !emptyEnemy.gameObject.activeSelf);
    }

    public Transform GetCurrentEnemy()
    {
        CheckEmpty();

        if (_targetEnemies.Count > 0)
        {
            return _targetEnemies[0];
        }
        else
        {
            return null;
        }
    }

    private void RotationUpdate()
    {
        if(_towerHead == null)
        {
            return;
        }
        currentEnemy = GetCurrentEnemy();
        if (currentEnemy != null)
        {
            Vector3 direction = currentEnemy.position - _towerHead.position;
            direction.y = 0f;

            if (direction.sqrMagnitude > 0.01f)
            {
                Quaternion rotation = Quaternion.LookRotation(direction);
                _towerHead.rotation = Quaternion.Lerp
                    (_towerHead.rotation,
                    rotation,
                    Time.deltaTime * _rotationSpeed
                    );
            }
        }
        else
        {
            _towerHead.rotation = Quaternion.Lerp
                    (_towerHead.rotation,
                    Quaternion.LookRotation(Vector3.forward),
                    Time.deltaTime * _rotationSpeed
                    );
        }
    }
}

 

해당 부분 전문이다.

간결하게 요약하자면

 

Collider에 들어온 오브젝트를 OnTriggerEnter를 통해 감지한다

Enemy 태그를 이용해 해당 오브젝트가 적인지 판별 후, 기존 List에 존재하는지 판단하여 중복을 예방한다.

그 후 List에 추가한다.

 

Exit를 이용해 범위를 나가는 적을 감지후 처리하는 부분도 있으며,

Destroy나 비활성화로 인해 OntTriggerExit 감지에서 벗어나는 경우에 대한 잔여 리스트 처리도 구현돼있다.

 

일반적인 상황에서 이 코드는 크게 문제를 발생키시키지 않으나

대량의 물량을 컨셉으로 한 프로젝트였기에, 많은 수의 적이 접근시 상당한 병목을 유발한다.

이 병목 현상에는 타워에만 책임있는 게 아닌, 저 수 많은 적들도 각각 Collider를 보유한 체로 주변 적을 탐색하고 있는 부분도 있지만 우선 타워 쪽을 해결해보자.

 

 

OnTrigger에서의 수많은 Collider 감지로 인해 병목이 생기는 부분과

List로 범위내 적을 관리하는 과정에서, 추가-제거-탐색 모두 병목을 유발할 여지가 커 보이는 동작이다.

 

 

해결 아이디어

우선 몇가지 아이디어를 생각해 봤다.

 

순서가 상관이 없으니 Hashset 같은 탐색, 추가, 제거 가 O(1)인 자료구조를 사용하는 경우 병목을 줄일 수 있을 것 같았다.

하지만 근본적으로 자료구조를 써야하는가? 에 대한 의문을 품었다.

 

- isTargeting이 false일 때만
    - 범위 내 적 존재시 isTageting on
    - 그놈 정보, 그놈 공격
- isTargeting이 true일 때
    - 타겟 정보가 내 범위 안에 있을 때
        - 계속 공격
    - 범위 안에 없을 때
        - isTargeting = flase
        - 타겟 정보 null

 

이런 식의 로직과,

적 탐지를 OnTrigger 가 아닌 Physics.OverlapSphere 를 이용해 필요한 순간에만 사용,

또한 코루틴을 이용해 탐지 간격을 줄이는 쪽으로 해결할 수 있을 것 같아 보였다.

 

문제시 한 List 부하, OnTrigger 부하를 모두 잡을 수 있는 아이디어였다.

바로 실천해보자.

 

개선 코드

public class TowerTrigger : MonoBehaviour
{
    [SerializeField] private Transform _towerHead;
    [SerializeField] private float _rotationSpeed;

    private float _range = 15f;
    public Transform currentEnemy;
    private bool _isTargeting = false;

    private void SearchTarget()
    {
        if (_isTargeting == false)
        {
            Collider[] hits = Physics.OverlapSphere(transform.position, _range, 1 << 8, QueryTriggerInteraction.Ignore);

            // 맞은 놈이 있으면
            if (hits.Length > 0)
            {
                // 그 중 가장 가까운 놈 선택할 수도 있지만 반복문 최대한 쓰지 말자
                currentEnemy = hits[0].transform;
                _isTargeting = true;
            }
        }
        else
        {
            if (currentEnemy == null || !currentEnemy.gameObject.activeSelf)
            {
                _isTargeting = false;
                currentEnemy = null;
                return;
            }
        }
    }
}

 

 

 

안타깝게도 해결 안됐다.

진짜 원인은 이 쪽이 아닌듯 하다.

 

 

심지어 타워를 없애봐도 3프레임 대에서 벗어나질 못한다.

 

분명 이론상으로는 최적화 방안으로 보이지만 실제로 게임 실행 도중에 유의미한 차이는 보이지 않았다. (2~4 FPS 유지)

 

 


 

항상 주변을 Collider로 감지하며 이동하고 있는 Enemy

우선 가장 간단한 방법인 enemy 오브젝트들의 shadow cast나, 상호작용이 불필요한 UI들의 interactable, raycasy 등을 비활성화 하는 시도도 해보았지만 유의미한 결과는 없었다. (1~4 FPS 유지)

 

 

Enemy의 초기 구조

모든 Enemy 오브젝트가 각각 적을 탐지하기 위한 SphereCollider 보유

OnTriggerEnter를 이용해 근처의 Player나 Tower를 Tag로 감지해 추적 및 공격

 

문제점

OnTrigger 메서드는 fixedUpdate 주기에 맞춰 실행되기 때문에 초당 약 50회 정도 호출

Enemy 수가 많아지는 경우에 이는 극단적으로 많은 연산이 되어 병목현상을 유발할 가능성이 높다 판단.

Enemy 수 * 초당 50회 * Collider 범위 ( 4/3 * pi * r³ )

 

Collider 내부 구조를 완벽히 알지 못한 체로 대략적으로 계산해도 이 정도 연산을 기본으로 가지는데

이를 기준으로도 Enemy 수가 200이 넘어가고, 감지 거리니 20f 정도를 대입하면 초당 억 단위가 넘어가는 연산이 일어나는 수준...?

 

말이 좀 안되는 숫자 같은데 일단 이렇게 보면 Tower 에서 예상한 낭비되는 연산량 (enemy * tower * List 간섭 수) 보다 아득히 높은 연산량이 될 것은 확실해 보였다.

 

조치

Enemy에 기본적인 충돌 판정을 제외한 나머지 불필요한 Collider 제거

Enemy 오브젝트에서 탐지로 사용하던 SphereCollider 제거

탐지 기능을 OnTrigger를 사용하지 않고 Physics.OverlapSphere로 변경

순간 프레임 피크를 방지하기 위해 Coroutine 대기 시간을 0.7 ~ 1.5 랜덤값 부여

 

protected virtual void Start()
    {
        StartCoroutine(TargetDetectionRoutine());
    }

    private IEnumerator TargetDetectionRoutine()
    {
        yield return new WaitForSeconds(Random.Range(0f, 0.5f));

        while (_isAlive)
        {
            Collider[] hits = Physics.OverlapSphere(transform.position, _ViewRange);

            Transform bestTarget = _targetTransform;
            float bestPriority = GetPriority(_targetTransform);

            foreach (Collider hit in hits)
            {
                if (hit.CompareTag("Player") || hit.CompareTag("Tower") || hit.CompareTag("HQ"))
                {
                    float priority = GetPriority(hit.transform);

                    if (priority > bestPriority)
                    {
                        bestPriority = priority;
                        bestTarget = hit.transform;
                    }
                }
            }

            // Detecting New Target
            if (bestTarget != _targetTransform)
            {
                _targetTransform = bestTarget;
                _Chase = _targetTransform != null;
            }

            // if target is null, stop chase
            if (_targetTransform == null)
                _Chase = false;

            // 0.7~1.5초 랜덤 대기 (탐지 부하 분산)
            yield return new WaitForSeconds(Random.Range(0.7f, 1.5f));
        }
    }

 

이런 형태로 수정했다.

 

 

결과는

 

최소 30FPS 이상으로 회복, Enemy 배치 및 생성을 조정한 것으로 80FPS 이상까지도 회복시키는데에 성공했다.

 

 

결론

자료구조에서 다소의 연산 낭비가 있더라도 Collider가 압도적으로 무겁다.

Collider는 꼭 필요한 경우에만 사용하며, 많은 수에 적용되어야 할 로직에서는 다른 방법을 이용하는 게 좋다.

 

추가적인 최적화 가능성

탐지의 기준점을 많은 수의 Enemy 오브젝트가 아닌, Tower나 Player로 변경하여, 범위 내의 적이 자신에게 공격오도록 로직을 변경하면 일정 시간마다 탐지를 위해 OverlapSphere를 실행하는 횟수 마저도 최적화가 가능하다.

'Unity' 카테고리의 다른 글

Unity - FSM : Finite State Machine - 유한 상태 머신  (0) 2025.11.11
Unity - 6000.2.10f1 - InputSystem 버그  (0) 2025.11.07
Unity - Raycast (레이캐스트)  (0) 2025.10.22
Design Pattern - Observer Pattern (옵저버 패턴)  (0) 2025.10.21
Unity - UGUI : Unity Graphical User Interface  (0) 2025.10.21
'Unity' 카테고리의 다른 글
  • Unity - FSM : Finite State Machine - 유한 상태 머신
  • Unity - 6000.2.10f1 - InputSystem 버그
  • Unity - Raycast (레이캐스트)
  • Design Pattern - Observer Pattern (옵저버 패턴)
ellug
ellug
Web / App / Game Dev
  • ellug
    dev.ellug
    ellug
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • Javascript (React, Next) (1)
      • C# (66)
      • Graphics (1)
      • Unity (25)
      • 프로그래머스 (0)
        • Lv.0 (0)
        • Lv.1 (0)
        • Lv.2 (0)
        • Lv.3 (0)
      • AI Agent (0)
      • StableDiffusion (0)
      • ETC (1)
      • 잡담 (0)
      • 유머 (0)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Abstract
    스택
    stack
    게임
    자료구조
    최적화
    console
    class
    Photon
    delegate
    useState
    Queue
    추상 클래스
    struct
    터미널
    list
    function
    네트워크
    유니티
    unity
    콘솔
    firebase
    알고리즘
    inputsystem
    큐
    C#
    Design Pattern
    클래스
    interface
    Network
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
ellug
Unity 프로젝트 이슈 해결 - 범위내 적 타게팅 최적화
상단으로

티스토리툴바