본문 바로가기

게임개발 관련/Tool, 게임엔진

[유니티 질문] 유니티 최적화 팁들 모음

유니티를 조금씩 공부를 하고는 있는데 궁금해서 한 번 질문을 해봤다.

결국엔 약간 바보같은? 질문을 해버린듯? 그래도 궁금증 해소를 위해서...

 

원래의 질문 - 씬이 많아지면 성능이 떨어지는지?

유니티에서 씬(Scene)을 많이 구성하는 것이 성능에 영향을 미칠 수 있지만, 이는 주로 어떻게 구성하고 관리하느냐에 따라 다릅니다.

특히, 모바일 앱 개발에서는 메모리와 성능 최적화가 중요한 요소이므로 다음 사항들을 고려해야 합니다.

 

1. 씬 수가 많아도 성능에 직접적인 영향은 적음

유니티는 한 번에 하나의 씬만 로드되므로, 로드되지 않은 씬은 메모리나 CPU 자원을 소모하지 않습니다.

따라서 씬의 개수 자체는 성능 저하의 주요 원인이 아닙니다.

 

2. 씬 로딩 시간

씬 전환 시 리소스를 로드하는 과정에서 로딩 시간이 발생합니다.

씬에 포함된 에셋의 크기와 개수가 많거나, 동적 객체 생성이 많다면 로딩 시간 증가 및 성능 저하가 생길 수 있습니다.

  • Addressables 또는 Asset Bundle을 활용하여 필요한 리소스만 동적으로 로드.
  • 씬 분할(Chunking): 큰 씬을 여러 개의 작은 씬으로 나누어 점진적으로 로드.

* 현재 문제점

  • 큰 씬을 로드할 때 UI가 멈추는 현상 발생.
  • 로딩 과정에서 플레이어 경험 저하.
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
    public void LoadSceneSynchronously(string sceneName)
    {
        // 동기적으로 씬 로드
        SceneManager.LoadScene(sceneName);
        Debug.Log("Scene loaded synchronously: " + sceneName);
    }
}

 

* 개선 후 : 최적화된 비동기 로딩 방식

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class AsyncSceneLoader : MonoBehaviour
{
    public GameObject loadingUI; // 로딩 화면 UI
    public UnityEngine.UI.Slider progressBar; // 로딩 진행 표시용 슬라이더

    public void LoadSceneAsynchronously(string sceneName)
    {
        // 로딩 UI 활성화
        if (loadingUI != null)
            loadingUI.SetActive(true);

        // 비동기 로드 시작
        StartCoroutine(LoadSceneCoroutine(sceneName));
    }

    private IEnumerator LoadSceneCoroutine(string sceneName)
    {
        // 씬 비동기 로드 시작
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(sceneName);

        // 로드가 완료될 때까지 대기
        while (!asyncOperation.isDone)
        {
            float progress = Mathf.Clamp01(asyncOperation.progress / 0.9f); // 0.9f가 max 값
            if (progressBar != null)
                progressBar.value = progress; // 진행률 업데이트
            Debug.Log("Loading progress: " + progress * 100 + "%");

            yield return null; // 한 프레임 대기
        }

        // 로드 완료 후 로딩 UI 비활성화
        if (loadingUI != null)
            loadingUI.SetActive(false);

        Debug.Log("Scene loaded asynchronously: " + sceneName);
    }
}
public class ExampleUsage : MonoBehaviour
{
    public AsyncSceneLoader asyncLoader;

    public void LoadGameScene()
    {
        // 기존 방식
        //asyncLoader.LoadSceneSynchronously("GameScene");

        // 최적화된 방식
        asyncLoader.LoadSceneAsynchronously("GameScene");
    }
}

 

3. 씬 관리

씬이 많아질수록 프로젝트 관리가 복잡해질 수 있으며, 로직 간의 연결(Dependencies)이 늘어날 수 있습니다.

적절한 씬 분할 전략이 필요합니다:

  • 공유 리소스 씬: UI, 공통 오브젝트 등을 별도 씬으로 분리하고, Additive Load를 활용.
  • 구역 씬: 게임 구역을 씬 단위로 나누어 필요한 구역만 로드.

* 현재의 문제점

  • 씬이 크면 메모리 사용량과 로딩 시간이 크게 증가.
  • 불필요한 오브젝트까지 로드되어 성능 저하.
  • 로딩 중 앱이 멈춘 것처럼 보일 수 있음
using UnityEngine;
using UnityEngine.SceneManagement;

public class LargeSceneLoader : MonoBehaviour
{
    public void LoadLargeScene()
    {
        // 큰 단일 씬을 로드
        SceneManager.LoadScene("LargeScene");
        Debug.Log("Large scene loaded");
    }
}

 

* 씬 분할 및 Additive (첨가제? 첨가물?) 방식

  • BaseScene: 공통 UI, 카메라, 기본 환경 포함.
  • EnvironmentScene: 배경, 지형 등 환경 오브젝트 포함.
  • EnemyScene: 적 캐릭터 포함.
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class SplitSceneLoader : MonoBehaviour
{
    public void LoadBaseScene()
    {
        // 기본 베이스 씬 로드
        SceneManager.LoadScene("BaseScene", LoadSceneMode.Single);
        Debug.Log("Base scene loaded");
    }

    public void LoadAdditionalScene(string sceneName)
    {
        StartCoroutine(LoadSceneAdditively(sceneName));
    }

    public void UnloadAdditionalScene(string sceneName)
    {
        StartCoroutine(UnloadScene(sceneName));
    }

    private IEnumerator LoadSceneAdditively(string sceneName)
    {
        // 추가 씬을 비동기로 로드
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
        while (!asyncOperation.isDone)
        {
            Debug.Log($"Loading {sceneName}: {asyncOperation.progress * 100}%");
            yield return null;
        }

        Debug.Log($"Additional scene {sceneName} loaded");
    }

    private IEnumerator UnloadScene(string sceneName)
    {
        // 추가 씬 언로드
        AsyncOperation asyncOperation = SceneManager.UnloadSceneAsync(sceneName);
        while (!asyncOperation.isDone)
        {
            Debug.Log($"Unloading {sceneName}: {asyncOperation.progress * 100}%");
            yield return null;
        }

        Debug.Log($"Additional scene {sceneName} unloaded");
    }
}

 

public class GameManager : MonoBehaviour
{
    public SplitSceneLoader sceneLoader;

    void Start()
    {
        // 베이스 씬 로드
        sceneLoader.LoadBaseScene();

        // 필요한 씬 추가 로드
        sceneLoader.LoadAdditionalScene("EnvironmentScene");
        sceneLoader.LoadAdditionalScene("EnemyScene");
    }

    public void OnPlayerExitZone()
    {
        // 필요 없어진 씬 언로드
        sceneLoader.UnloadAdditionalScene("EnemyScene");
    }
}

 

* 추가적인 최적화 팁

  • 씬별 리소스 관리:
    • 큰 에셋은 Addressables로 관리하여 필요할 때만 로드.
  • 씬 간 데이터 전달:
    • PlayerPrefs, DontDestroyOnLoad, 또는 ScriptableObject를 활용하여 데이터를 유지.
  • 로딩 중 UX 개선:
    • 씬 로드 중 로딩 화면 표시 및 진행 상태 보여주기.

 

4.모바일 앱 성능 최적화 고려사항

  • 메모리 사용량: 모바일은 메모리 용량이 제한적이므로, 씬 전환 시 불필요한 리소스 정리가 필요합니다. Resources.UnloadUnusedAssets()를 호출하여 사용하지 않는 에셋 정리.
  • 씬 전환 효과: 부드러운 전환을 위해 비동기 로드(SceneManager.LoadSceneAsync)를 사용.
  • 최소화된 폴리곤과 텍스처: 씬 내부의 오브젝트가 많으면 GPU 부담이 커질 수 있으므로 경량화 필요.

* 현재 문제점

  • 씬 전환 후 이전 씬의 리소스가 메모리에 남아 있을 수 있음.
  • 불필요한 오브젝트의 참조가 남아 메모리 누수 발생.
public class PoorMemoryManagement : MonoBehaviour
{
    public void LoadNewScene(string sceneName)
    {
        // 씬 전환 (리소스 정리 없이 실행)
        SceneManager.LoadScene(sceneName);
        Debug.Log("Scene loaded without memory cleanup");
    }

    public void DestroyObject(GameObject obj)
    {
        // 게임 오브젝트 파괴
        Destroy(obj);
        Debug.Log("Object destroyed without clearing references");
    }
}

 

* 개선 사항 : 리소스를 명시적으로 해제하고 메모리 정리를 수행

using UnityEngine;
using UnityEngine.SceneManagement;

public class EffectiveMemoryManagement : MonoBehaviour
{
    public void LoadNewScene(string sceneName)
    {
        // 사용하지 않는 리소스를 먼저 정리하고 새로운 씬 로드
        StartCoroutine(LoadSceneWithMemoryCleanup(sceneName));
    }

    public void DestroyObject(GameObject obj)
    {
        // 게임 오브젝트 파괴 후 메모리 정리
        if (obj != null)
        {
            Destroy(obj);
            Resources.UnloadUnusedAssets();
            Debug.Log("Object destroyed and unused assets unloaded");
        }
    }

    private IEnumerator LoadSceneWithMemoryCleanup(string sceneName)
    {
        // 메모리 정리
        Resources.UnloadUnusedAssets();
        System.GC.Collect(); // 강제 Garbage Collection 호출
        Debug.Log("Unused assets and garbage collected");

        // 새 씬 로드
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(sceneName);
        while (!asyncOperation.isDone)
        {
            Debug.Log($"Loading {sceneName}: {asyncOperation.progress * 100}%");
            yield return null;
        }

        Debug.Log($"Scene {sceneName} loaded with memory cleanup");
    }
}

 

5. 씬 대신 데이터 기반 로드 사용

많은 씬이 필요한 경우라도, 씬을 나누지 않고 데이터 기반 콘텐츠 로드 방식을 활용할 수 있습니다.

  • 예: JSON, ScriptableObject, XML 등으로 씬 데이터를 관리하고, 하나의 씬에서 동적으로 생성 및 배치.

* 수정 전

  • 씬이 커지고 복잡해질수록 메모리 사용량 증가.
  • 유연성 부족: 오브젝트를 조건에 따라 생성/삭제하기 어려움.
using UnityEngine;

public class HardCodedObjectLoader : MonoBehaviour
{
    public GameObject enemyPrefab;

    void Start()
    {
        // 모든 오브젝트가 씬에 미리 배치되어 있음
        Instantiate(enemyPrefab, new Vector3(0, 0, 0), Quaternion.identity);
        Instantiate(enemyPrefab, new Vector3(2, 0, 0), Quaternion.identity);
        Instantiate(enemyPrefab, new Vector3(-2, 0, 0), Quaternion.identity);
        Debug.Log("Enemies loaded in the scene");
    }
}

 

* 데이터 기반 수정

  • 유연성: JSON 데이터를 수정하는 것만으로도 오브젝트 배치를 변경 가능.
  • 메모리 최적화: 필요한 데이터만 로드하고 생성.
  • 확장성: 새로운 오브젝트나 속성을 쉽게 추가 가능.
[
    { "name": "Enemy1", "x": 0, "y": 0, "z": 0 },
    { "name": "Enemy2", "x": 2, "y": 0, "z": 0 },
    { "name": "Enemy3", "x": -2, "y": 0, "z": 0 }
]

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

[System.Serializable]
public class EnemyData
{
    public string name;
    public float x;
    public float y;
    public float z;
}

public class DataDrivenObjectLoader : MonoBehaviour
{
    public GameObject enemyPrefab;

    void Start()
    {
        // JSON 데이터를 읽어와 적 생성
        string jsonFilePath = Path.Combine(Application.dataPath, "Resources/EnemyData.json");
        string jsonData = File.ReadAllText(jsonFilePath);

        List<EnemyData> enemyList = JsonUtility.FromJson<Wrapper<EnemyData>>(jsonData).items;

        foreach (var enemy in enemyList)
        {
            Vector3 position = new Vector3(enemy.x, enemy.y, enemy.z);
            GameObject enemyObj = Instantiate(enemyPrefab, position, Quaternion.identity);
            enemyObj.name = enemy.name;

            Debug.Log($"Enemy created: {enemy.name} at position {position}");
        }
    }

    [System.Serializable]
    private class Wrapper<T>
    {
        public List<T> items;
    }
}

추가 최적화 팁

  1. ScriptableObject 활용: JSON 대신 ScriptableObject를 사용하면 유니티 에디터에서 쉽게 데이터를 관리 가능.
  2. Addressables 사용: 큰 에셋은 Addressables로 로드하여 메모리 사용량 감소.
  3. 오브젝트 풀링: 자주 생성/삭제되는 오브젝트는 풀링 기법으로 성능 최적화.

 

6. 프로파일링으로 확인

유니티의 Profiler를 사용해 다음 항목들을 모니터링하세요:

  • 씬 전환 시의 메모리 사용량, CPU 부하, GPU 부하.
  • Garbage Collection(GC) 호출 빈도.
  • 오브젝트 생성 및 소멸 시 성능 변화.

요약

씬의 개수 자체는 큰 문제가 되지 않지만, 씬의 로딩 방식과 리소스 관리가 모바일 성능에 큰 영향을 미칩니다. 효율적인 씬 관리와 로딩 최적화를 통해 성능 문제를 줄일 수 있습니다. 씬을 많이 나누되 필요한 순간에만 로드하고, 공통 리소스는 Additive 방식으로 관리하면 더 나은 성능을 얻을 수 있습니다.