Unity 최적화

JJW·2024년 12월 18일
0

Unity

목록 보기
18/34

오늘은 Unity에서의 최적화를 구현해보려 합니다.
저번에 구현했었던 LOD와 Occlusion Culling 을 적용하였으나 렌더링만 안될 뿐
메모리에는 올라가서 추가적으로 최적화 방식에 대해 고민하였습니다.


최적화 계획

  • 저와 같은 경우에는 기존 맵에 수백 ~ 수만개의 오브젝트가 존재하여 게임을 실행시키면 수천 ~ 수만개의 메모리가 올라갑니다.
  • 전부 메모리에 들고 있기는 부담스럽고 씬을 영역을 나누어 비동기로 씬을 로드하는 건
    작업이 생각보다 커질 것 같아서 카메라와 일정 범위에 오브젝트들만
    생성 및 활성화가 되도록 계획하였습니다.

구현 목표

    1. 오브젝트 풀 사용 (각 프리팹 마다 10개씩 사용 추가로 필요한 경우 생성해서 사용)
    1. 맵에 있는 오브젝트들 삭제
    1. 맵에 있는 오브젝트들이 삭제되기 전 내 상태와 위치를 기록하기 위한 매니저 클래스

구현

1. Public Enums

#region Object Pool
public enum ObjectType
{
    Cliff_A,
    Cliff_B,
    Cliff_C,
    Cliff_D,
    Cliff_E,
    Cliff_F,

    RockCluster_A,
    RockCluster_B,
    RockCluster_C,
    RockCluster_D,

    Tree_A,
    Tree_B,
    Tree_C,

    Birch_A,
    Birch_B,
    Birch_C,

    Groundcover_Dalsies,
    Groundcover_Poppies,
    Groundcover_Leaves,
    Groundcover_Clovers,

    Bush_A,
    Bush_B,
    Bush_C,

    Fern,

    Speedwell,

    Deadelions,

    Brachch_A,
    Brachch_B,

    BranchCluster,
    DaisyCluster,
    ChervilCiuster,
    RedPoppyCluster,
    BluePoppyCluster,

    Grass,
    GrassThick,
    GrassTall,

    Ivy_Climbing,
    Ivy_Cylinder,
    Ivy_GroundCover,
    Ivy_Hanging_A,
    Ivy_Hanging_B,
    Ivy_Ledge,

    Lotus,

    Lilypad,
    Lilypads_A,
    Lilypads_B,

    Spruce_A,
    Spruce_B,
    Spruce_C,

    Willow_A,
    Willow_B,

    Pine_A,

    Palm_A,
    Palm_B,
    Palm_C,

    ElephantEar_A,
    ElephantEar_B,
}

#endregion
  • 우선 각 프리팹의 타입을 정해 줄 Enum의 목록을 만들었습니다.

2. Object State

using UnityEngine;

[System.Serializable]
public class ObjectState
{
    public int id;                                  // 고유 ID
    public Vector3 position;                        // 위치
    public Quaternion rotation;                     // 방향
    public Vector3 scale;                           // 크기
    public ObjectType objectType;                   // 프리팹 타입
    public float lastStateChangeTime;               // 이전에 상태가 변경된 시간
    public bool isProcessing;                       // 현재 활성 요청, 비활성 요청 중
    public Transform originalParent;                // 원래 부모
}
  • Object의 필요한 정보들을 선언해주었습니다.

3. Managed Object

using System.Runtime.Serialization;
using UnityEngine;

public class ManagedObject : MonoBehaviour
{
    public ObjectType objectType;

    private ObjectState objectState;

    void Start()
    {
        // ObjectState 생성 및 등록
        objectState = new ObjectState
        {
            position = transform.position,
            rotation = transform.rotation,
            scale = transform.localScale,
            objectType = objectType,
            lastStateChangeTime = -ManagedObjectManager.Instance.stateChangeCooldown, 
            isProcessing = false,
            originalParent = transform.parent 
            // id는 ManagedObjectManager에서 할당
        };

        ManagedObjectManager.Instance.RegisterObjectState(objectState);

        // 원래의 오브젝트를 씬에서 제거 (Destroy)
        Destroy(gameObject);
    }
}
  • 실제 프리팹에 들어갈 클래스입니다.
  • 내 프리팹의 타입을 정하고 생성자 함수를 통해 초기 값을 정해줍니다. 이후 ManagedObjectManager의 자신을 등록하고 제거시킵니다.

4. Managed ObjectPool

using System.Collections.Generic;
using System.Runtime.Serialization;
using UnityEngine;

public class ManagedObjectPool : MonoBehaviour
{
    [System.Serializable]
    public class ManagedPool
    {
        public ObjectType type;     // 풀링할 오브젝트 타입
        public GameObject prefab;   // 풀링할 프리팹
        public int initialSize;     // 초기 풀 크기
    }

    public List<ManagedPool> pools;                                         // Inspector에서 설정할 풀 리스트
    private Dictionary<ObjectType, Queue<GameObject>> poolDictionary;
    public Transform poolParent;                                            // 풀에 반환될 때의 부모 (예: "PooledObjects" 빈 오브젝트)

    void Awake()
    {
        poolDictionary = new Dictionary<ObjectType, Queue<GameObject>>();

        foreach (ManagedPool pool in pools)
        {
            Queue<GameObject> objectPool = new Queue<GameObject>();

            for (int i = 0; i < pool.initialSize; i++)
            {
                GameObject obj = Instantiate(pool.prefab, poolParent);

                if(obj.TryGetComponent(out ManagedObject temp))
                {
                    Destroy(temp);
                }

                obj.SetActive(false);
                objectPool.Enqueue(obj);
            }

            poolDictionary.Add(pool.type, objectPool);
        }
    }

    // 오브젝트 가져오기
    public GameObject GetObject(ObjectType type, Vector3 position, Quaternion rotation, Vector3 scale, Transform newParent)
    {
        if (!poolDictionary.ContainsKey(type))
        {
            return null;
        }

        GameObject objectToSpawn;

        if (poolDictionary[type].Count > 0)
        {
            objectToSpawn = poolDictionary[type].Dequeue();
        }
        else
        {
            // 풀에 오브젝트가 없으면 새로 생성
            ManagedPool pool = pools.Find(p => p.type == type);
            if (pool != null && pool.prefab != null)
            {
                objectToSpawn = Instantiate(pool.prefab, poolParent);

                if (objectToSpawn.TryGetComponent(out ManagedObject temp))
                {
                    Destroy(temp);
                }
            }
            else
            {
                return null;
            }
        }

        objectToSpawn.SetActive(true);
        objectToSpawn.transform.position = position;
        objectToSpawn.transform.rotation = rotation;
        objectToSpawn.transform.localScale = scale;
        objectToSpawn.transform.parent = newParent;

        return objectToSpawn;
    }

    // 오브젝트 반환
    public void ReturnObject(ObjectType type, GameObject obj)
    {
        obj.transform.parent = poolParent;
        obj.SetActive(false);
        poolDictionary[type].Enqueue(obj);
    }
}
  • 오브젝트 풀 클래스입니다. 인스펙터에 존재하는 Pools 리스트에 등록된 목록들을 poolDictionary에 집어 넣어줍니다.
  • 넣어줄 때 Managed Object를 가지고 있으면 제거하게끔 해주었습니다.
    (Managed Object의 Start 구문에서 풀로 생성된 경우엔 등록하면 안되기에)
  • 부모를 바꿔 줄 필요는 없지만 가시성을 위해 바꿔주었습니다.

5. Managed Object Manager

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

public class ManagedObjectManager : MonoSingleton<ManagedObjectManager>
{
    private Camera mainCamera;                                                               // 메인 카메라
    public ManagedObjectPool objectPool;                                                    // 오브젝트 풀
    public float checkInterval = 2f;                                                        // 검사 주기 (초)
    public float loadRadius = 50f;                                                          // 로드할 범위 반경
    public float unloadRadius = 80f;                                                        // 언로드할 범위 반경 (로드 반경보다 큼)
    public float stateChangeCooldown = 1f;                                                  // 상태 변경 쿨다운 시간

    private List<ObjectState> allObjectStates = new List<ObjectState>();                    // 모든 오브젝트 상태
    private Dictionary<int, GameObject> activeObjects = new Dictionary<int, GameObject>();  // 활성화된 오브젝트 매핑
    private int nextID = 0;                                                                 // 고유 ID 생성기

    void Start()
    {
        if (mainCamera == null)
            mainCamera = Camera.main;

        if (objectPool == null)
        {
            Debug.LogError("ObjectPool is not assigned.");
            return;
        }

        StartCoroutine(ManageObjectsCoroutine());
    }

    // ManagedObject에서 호출하여 ObjectState를 등록합니다.
    public void RegisterObjectState(ObjectState state)
    {
        state.id = nextID++;
        allObjectStates.Add(state);
    }


    // 주기적으로 오브젝트를 검사합니다.
    IEnumerator ManageObjectsCoroutine()
    {
        while (true)
        {
            ManageObjects();
            yield return new WaitForSeconds(checkInterval);
        }
    }

    // 카메라 시야 내외의 오브젝트를 관리합니다.
    void ManageObjects()
    {
        Vector3 cameraPos = mainCamera.transform.position;
        float currentTime = Time.time;

        List<ObjectState> toActivate = new List<ObjectState>();
        List<ObjectState> toDeactivate = new List<ObjectState>();

        foreach (var objState in allObjectStates)
        {
            float distance = Vector3.Distance(cameraPos, objState.position);
            bool shouldActivate = distance <= loadRadius;
            bool shouldDeactivate = distance > unloadRadius;

            if (shouldActivate && !activeObjects.ContainsKey(objState.id) &&
                currentTime - objState.lastStateChangeTime > stateChangeCooldown &&
                !objState.isProcessing)
            {
                toActivate.Add(objState);
                objState.lastStateChangeTime = currentTime;
                objState.isProcessing = true; // 처리 중임을 표시
            }
            else if (shouldDeactivate && activeObjects.ContainsKey(objState.id) &&
                     currentTime - objState.lastStateChangeTime > stateChangeCooldown &&
                     !objState.isProcessing)
            {
                toDeactivate.Add(objState);
                objState.lastStateChangeTime = currentTime;
                objState.isProcessing = true; // 처리 중임을 표시
            }
        }

        // 활성화 및 비활성화를 비동기로 처리
        if (toActivate.Count > 0)
            StartCoroutine(ActivateObjectsCoroutine(toActivate));

        if (toDeactivate.Count > 0)
            StartCoroutine(DeactivateObjectsCoroutine(toDeactivate));
    }

    // 오브젝트를 활성화합니다.
    IEnumerator ActivateObjectsCoroutine(List<ObjectState> objectsToActivate)
    {
        int batchSize = 10; // 한 번에 처리할 오브젝트 수
        int count = 0;

        foreach (var objState in objectsToActivate)
        {
            if (activeObjects.ContainsKey(objState.id))
            {
                objState.isProcessing = false; // 처리 완료
                continue;
            }

            GameObject obj = objectPool.GetObject(objState.objectType, objState.position, objState.rotation, objState.scale, objState.originalParent);

            if (obj != null)
            {
                // 중복 추가 방지
                bool added = activeObjects.TryAdd(objState.id, obj);
                if (!added)
                {
                    objectPool.ReturnObject(objState.objectType, obj);
                }
            }

            objState.isProcessing = false; // 처리 완료

            count++;
            if (count >= batchSize)
            {
                count = 0;
                yield return null; // 다음 프레임으로 넘어갑니다.
            }
        }
    }

    // 오브젝트를  비활성화합니다.
    IEnumerator DeactivateObjectsCoroutine(List<ObjectState> objectsToDeactivate)
    {
        int batchSize = 10; // 한 번에 처리할 오브젝트 수
        int count = 0;

        foreach (var objState in objectsToDeactivate)
        {
            if (activeObjects.TryGetValue(objState.id, out GameObject obj))
            {
                objectPool.ReturnObject(objState.objectType, obj);
                activeObjects.Remove(objState.id);
            }

            objState.isProcessing = false; // 처리 완료

            count++;
            if (count >= batchSize)
            {
                count = 0;
                yield return null; // 다음 프레임으로 넘어갑니다.
            }
        }
    }
}
  • 현재 카메라의 범위에 오브젝트가 있었는 지에 대해 주기적으로 검사하며 있으면 오브젝트 풀에 활성화 요청을 보냅니다. 활성화가 된 오브젝트들은 activeObjects에 추가해주며 관리합니다.

테스트

  • 플레이어가 움직일 때 오브젝트가 나타나거나 사라지는 모습을 확인 할 수 있습니다.

느낀 점

원래는 카메라가 보여주는 범위의 오브젝트만을 보여주려 했는데 생각이 떠오르지 않아 일단 거리로 작업하였습니다. 추후에 카메라 범위로 수정된 버전 올리도록 하겠습니다. 그리고 실제 Memory Profiler를 사용하게 아니라 이 작업으로 얼마나 줄었는지에 대해 확인을 못해본게 아쉽네요.. 카메라 범위로 수정하면서 Memory Profiler 테스트도 해서 올리도록 하겠습니다..!

  • 제가 조사한 내용이 맞지 않거나 잘못 된 경우에 댓글로 잘못된 점 지적해주시면 감사합니다 ! (´._.`)
profile
Unity 게임 개발자를 준비하는 취업준비생입니다..

0개의 댓글