[20260207] NavMesh 성능 테스트

SmartBear·2026년 2월 7일
post-thumbnail

팀 프로젝트에서 나온 질문 중 하나가, NavMesh 에 대한 성능 저하에 대한 질문이 있었다.
실제로 얼마나 성능이 저하되는지 알고 싶어 테스트를 진행하게 되었다.

테스트 데이터

Unity5 기준으로 진행하여, NavMesh 1.1.7 버전을 이용하여 테스트를 진행하였다.

Map

그림의 우측 상단에서 4개의 Potal 에서 각각 고블린이 등장한다. 등장 간격은 1초로 하였다. 좌측 하단의 파란 물체가 Target1, 녹색 물체가 Target2 이다.

일단 잘 움직이는 것은 확인 되었다.

Enemy

Inspector

딱히 크게 변경한 사항은 없다. 속력과 가속, 각속도를 빠르게 한 이유는 고블린이 빠르게 오게 하기 위해서다.

Move Script

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

public class MoveEnemy : MonoBehaviour
{
    public Transform target1;
    public Transform target2;
    private NavMeshAgent agent;
    private WaitForSeconds _wait = new WaitForSeconds(2f);

    private void Awake()
    {
        // agent = GetComponent<NavMeshAgent>();
    }
    private void Start()
    {
        // if (target1 != null) agent.SetDestination(target1.position);
        // StartCoroutine(ChangeTarget());
    }

    private void Update()
    {
        // if (target1 != null) agent.SetDestination(target1.position);
    }

    private IEnumerator ChangeTarget()
    {
        while (true)
        {
            if (target1 != null) agent.SetDestination(target1.position);
            Debug.Log("모든 몬스터의 타겟은 <color=red>Target 1</color>");
            yield return _wait;
            if (target2 != null) agent.SetDestination(target2.position);
            Debug.Log("모든 몬스터의 타겟은 <color=blue>Target 2</color>");
        }
    }
}

테스트 케이스에 필요한 코드를 쓰고 필요 없는 내용은 CommentOut 하며 진행하여 코드가 다소 지저분 하다 (양해 부탁 드립니다)

SpawnManager

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

public class SpawnManager : MonoBehaviour
{
    [SerializeField] private Transform _target1;
    [SerializeField] private Transform _target2;
    [SerializeField] private GameObject _enemy;
    [SerializeField] private List<Transform> _potalTfs;
    private List<GameObject> _enemies = new();
    [SerializeField][Range(100, 1000)] private int _maxEnemies;
    private WaitForSeconds _wait = new WaitForSeconds(1f);

    private void Start()
    {
        StartCoroutine(ExecuteSpwan());
    }

    private IEnumerator ExecuteSpwan()
    {
        while (_enemies.Count <= _maxEnemies)
        {
            foreach (Transform tf in _potalTfs)
            {
                GameObject tempEnemy = Instantiate(_enemy, tf.position, tf.rotation);
                MoveEnemy moveEnemy = tempEnemy.GetComponent<MoveEnemy>();
                moveEnemy.target1 = _target1;
                moveEnemy.target2 = _target2;
                _enemies.Add(tempEnemy);
            }
            yield return _wait;    
        }
        Debug.Log("모든 몬스터가 스폰 되었습니다.");
        yield return null;
    }
}

1초마다 각 포털에서 1마리씩 소환한다.

테스트 진행 및 결과

테스트 진행을 위한 확인 내용

게임 화면에 있는 State 혹은 통계를 눌러 나오는 통계화면을 활용했다. 여기의 CPU 와 FPS 를 중심으로 테스트를 진행하였다.

테스트 1.

총 100개의 Agent 에 대한 SetDestination을

  1. Start 에서 한번만 하였을 때
    • CPU: 27-30ms (35-40 FPS)
  2. Update 에서 같은 Target 위치에 대해서만 호출 할때
    • CPU: 27-30ms (35-40 FPS)
  3. Courtine 에서 다른 Target 위치에 대해 호출 할때 (2초 간격)
    • CPU: 27-30ms (35-40 FPS)
  4. 플레이 도중 Target 의 위치를 임의로 변경하였을 때 (Update 에서 계속 호출)
    • CPU: 27-30ms (35-40 FPS)

총 400개의 Agent 에 대한 SetDestination을

  1. Start 에서 한번만 하였을 때
    • CPU: 100-110ms (8-10fps)
  2. Update 에서 같은 Target 위치에 대해서만 호출 할때
    • CPU: 100-110ms (8-10 FPS)
  3. 총 400개의 Agent 가 없는 오브젝트 생성
    • CPU: 100-110ms (8-10 FPS)

1차 결론. 위 테스트로는 Agent 의 영향에 대해 파익이 어렵다.
그 이유는 Rendering 을 생각했다.
현재 게임 맵에서 보이는 View 상, Randering 을 할 물체가 너무 많이 보여 위와 같은 결과가 나오는 것 아닐까?라는 추측이다.

따라서 MainCamera 에 대한 조정을 위와 같이 한 후 다시 테스트를 진행하였다.

테스트 2.

총 100개의 Agent 에 대한 SetDestination을

  1. Update 에서 같은 Target 위치에 대해서만 호출 할때
    • CPU: 27-30ms (35-40 FPS)

크게 다르지 않은 테스트 결과를 얻었다.

테스트 3.

내가 확인해야 하는 데이터가 잘못 된 것은 아닐까?

그래서 AI 의 힘을 빌려 확인해본 결과, Profile 기능내에서 확인을 하는 것에 대해 조언을 얻었다.

위 그림은 100개의 Agent에서 Update 함수에서 매번 SetDestination을 실행하고 있는 코드에 대한 SetDestination의 호출 횟수 및 여러 데이터에 대한 내용이다.
위 결과로는 사실 유효한 결과를 얻지는 못 하였다. Agent 를 400 개나 늘리면 당연히 점유량은 늘어날 것이지만, 이것이 성능에 중대하게 미치는 영향이 있지는 않을 것 같다. 오히려 GameObject 자체가 많아지는 것이 더 문제일 것으로 보인다.

그렇다면,

NavMesh 는 성능 문제가 많이 없다. 단순히 움직이는 Object 의 개수가 성능 영향을 더 받는다?

라고 보면 될까?

그것은 아니다. 결국 매번 이 함수를 호출해 연산을 하고 있더는 것 자체는 문제가 있다고 볼 수 있다.
(그리고 테스트 방법 자체가 잘못된 것일수도 있다.)

부하를 줄이는 방법

최소한 내가 아는 부하를 줄이는 방법은 크게 3가지가 있다.

  1. 연산 타이밍을 어긋나게 하기
  2. 최대 연산 개수를 정해 그만큼만 연산하고 나머지는 버리기
  3. 캐싱하기

나의 아이디어

간단히 말하자면 Time-division. 동시에 연산할 Object 개수를 정하고, 모든 Object가 연산될 총 시간대비 동시 연산 그룹의 연산회수를 지정해주면 될 것으로 보인다.

즉,

Coroutine 등으로 연산 앞에 일정 시간의 딜레이를 주는 것이다. 여기서 (t+1)-t 는 "지정한 시간 간격" 이라 보면 된다. 일부 게임은 milliseconds 단위로 움직여야 하는 것들도 있지만, 최소한 내가 목표로 하는 다수의 적을 막는 디펜스류 게임은 적이 1~2초 정도 굼뜨게 행동한다고 혼나지 않을 것이라 생각한다.

그렇다면 이 이론으로, 1초 계산시 1,000 마리의 Object 를 1초내 동시연산한다고 하고, 이를 10등분으로 할 경우 0.1초당 100개의 Object 만 연산하면 되므로, 마리당 연산은 1ms 정도이다. CPU 가 ns 단위에서 연산하는 것을 감안하면 이정도는... 괜찮지 않을까?? 😅

코드

// Movement.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MoveEnemy : MonoBehaviour
{
    public Transform target1;
    public Transform target2;
    private NavMeshAgent agent;
    public float Delay;
    public float Interval;

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
    }
    private void Start()
    {
        StartCoroutine(CalculatePath());
    }
    
    private IEnumerator CalculatePath()
    {
        yield return new WaitForSecondsRealtime(Delay);
        while (true)
        {
            if (target1 != null) agent.SetDestination(target1.position);
            yield return new WaitForSecondsRealtime(Interval);
        }
    }
}

대략 이런 느낌이 되지 않을까 싶다.
어차피 Croutine 으로 실행되니 실행 단위 대비 실제 실행 Shift 단위는 같을 것으로 예상된다.

// Spawner.cs
private IEnumerator ExecuteSpwan()
{
    while (_enemies.Count <= _maxEnemies)
    {
        for (int i = 0; i < _potalTfs.Count; i++)
        {
            float delay = (i % _potalTfs.Count) * (1000 / (float)_potalTfs.Count);
            float interval = 1f;
            GameObject tempEnemy = Instantiate(_enemy, _potalTfs[i].position, _potalTfs[i].rotation);
            MoveEnemy moveEnemy = tempEnemy.GetComponent<MoveEnemy>();
            moveEnemy.target1 = _target1;
            moveEnemy.target2 = _target2;
            moveEnemy.Interval = interval;
            moveEnemy.Delay = delay;
            _enemies.Add(tempEnemy);
        }
        yield return _wait;    
    }
    Debug.Log("모든 몬스터가 스폰 되었습니다.");
    yield return null;
}

Spawn 쪽 코드이다.

이제 어느 정도 분할되서 Call 을 하는지 확인할 시간.

바로 한 Frame 에서 일부 호출만 발생했으며, 다음 Frame 에서는 호출이 발생하지 않은 것이 확인 되었다.

사실 이렇게 어렵게 하는 것 보다는

yield return new WaitForSecondsRealtime(Random.Range(0f, 1f);

이렇게 모든 Object 를 랜덤한 시간에 연산시키는 것도 하나의 방법이다.
하지만 Random 은 Seed 를 이용한 확률적 랜덤일 경우가 많아, 경우의 수가 아주 높지 않으면 한 수로 쏠릴 가능성이 있어 이 점은 참고를 해야 한다.

고찰

어떻게 보면 처음으로 Unity 내에서 디버거를 켜서 이것 저것 확인해본것이 처음이다. 아직 분석능력도, 확인도 어렵지만 이러한 RnD 를 자주 하여 어디서 어떤 병목이 생기는지 확인할 수 있는 스킬을 늘리는 것이 좋을 것 같다는 생각을 하였다.

아울러, 위 버전은 Unity5에서 확인한 것이니 Unity6에서도 주제는 다르지만 다른 내용에 대한 디버깅을 해보면 좋을 것 같다.

P.S
제미나이에게 이 블로깅 관련 짤 하나 그려달라니까 처음에 아래와 같은 그림을 그려주었다...ㅡㅡ 아니.. 이녀석이..??

추가 코드 및 테스트

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;

public class MoveEnemy : MonoBehaviour
{
    public Transform target1;
    public Transform target2;
    private NavMeshAgent agent;
    private Vector3 _cachingPosition;

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
    }
    private void Start()
    {
        _cachingPosition = target1.position;
        if (target1 != null) agent.SetDestination(target1.position);
        StartCoroutine(CalculatePath());
    }
    
    private IEnumerator CalculatePath()
    {
        yield return new WaitForSecondsRealtime(Random.Range(0f, 0.1f));
        while (true)
        {
            if (target1 != null && _cachingPosition != target1.position) agent.SetDestination(target1.position);
            yield return new WaitForSecondsRealtime(0.1f);
        }
    }
}

Caching 에 관해 생각한 내용을 추가한 것이다.
단순히 기존 위치 정보를 비교하는 Logic 으로 했지만,
추후에는 특정 범위를 벗어날 때만 경로 조정을 한다거나 하면 더 좋지 않을까 싶다.

profile
Python Dev with Infra -> Game Programmer

0개의 댓글