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

- 저와 같은 경우에는 기존 맵에 수백 ~ 수만개의 오브젝트가 존재하여 게임을 실행시키면 수천 ~ 수만개의 메모리가 올라갑니다.
- 전부 메모리에 들고 있기는 부담스럽고 씬을 영역을 나누어 비동기로 씬을 로드하는 건
작업이 생각보다 커질 것 같아서 카메라와 일정 범위에 오브젝트들만
생성 및 활성화가 되도록 계획하였습니다.
- 오브젝트 풀 사용 (각 프리팹 마다 10개씩 사용 추가로 필요한 경우 생성해서 사용)
- 맵에 있는 오브젝트들 삭제
- 맵에 있는 오브젝트들이 삭제되기 전 내 상태와 위치를 기록하기 위한 매니저 클래스
#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의 목록을 만들었습니다.
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의 필요한 정보들을 선언해주었습니다.
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의 자신을 등록하고 제거시킵니다.
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 구문에서 풀로 생성된 경우엔 등록하면 안되기에)- 부모를 바꿔 줄 필요는 없지만 가시성을 위해 바꿔주었습니다.
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 테스트도 해서 올리도록 하겠습니다..!
- 제가 조사한 내용이 맞지 않거나 잘못 된 경우에 댓글로 잘못된 점 지적해주시면 감사합니다 ! (´._.`)