게임 최적화에서 가장 먼저 버려야 할 생각은 “최적화 기법을 많이 적용하면 게임이 빨라진다”는 것이다.
최적화의 본질은 단순하다.
현재 발생하고 있는 병목을 측정하고 -> 원인을 찾고 -> 수정하고 -> 다시 측정한다.
Unity의 Profiler는 CPU, GPU, 메모리, 렌더링 등의 성능 정보를 확인할 수 있는 핵심 도구다. 특히 Unity 6에서는 Rendering Profiler를 통해 Batches, SetPass Calls, Triangle, Vertex 등의 렌더링 지표도 확인할 수 있다.
따라서 최적화는 다음과 같은 흐름으로 접근하는 것이 가장 안전하다.
Profiler -> 병목 확인 -> 원인 분석 -> 최적화 -> 동일 조건 재측정 -> 검증
최적화의 기본 원칙
최적화는 무조건 비용을 줄이는 작업이 아니다.
어떤 작업을 줄이면 다른 비용이 증가할 수 있다.
예를 들어 CPU 계산을 줄이기 위해 데이터를 캐싱하면 메모리 사용량이 증가할 수 있다.
CPU 감소 -> 메모리 증가
텍스처 해상도를 낮추면 메모리와 GPU 대역폭은 줄어들지만 화질이 떨어진다.
GPU Memory 감소 -> 화질 감소
오브젝트를 하나의 Mesh로 합치면 Draw Call을 줄일 수 있지만 개별 오브젝트의 Culling 효율이 떨어질 수 있다.
Draw Call 감소 -> Culling 효율 감소 가능
따라서 최적화는 특정 기술을 무조건 적용하는 작업이 아니라 전체 시스템의 비용 구조를 조정하는 작업이다.
CPU 최적화
CPU 병목은 크게 게임 로직, 물리, 애니메이션, 렌더링 준비 작업, GC 등에서 발생한다.
CPU 병목 -> Script / Physics / Animation / Rendering Preparation / GC
Update 계열 콜백 관리
Update, FixedUpdate, LateUpdate 같은 MonoBehaviour 메시지는 편리하지만 GameObject와 Component가 많아지면 호출 관리 비용도 증가한다.
특히 아무 작업도 하지 않는 콜백은 제거하는 것이 좋다.
private void Update()
{
}
이런 코드는 존재할 이유가 없다.
다만 Update 자체가 느린 것이 아니라 매 프레임 수많은 객체가 각각 Update를 수행하면서 발생하는 전체 비용이 문제다.
예를 들어 수천 개의 객체가 각각 Update를 실행하는 구조라면 중앙 관리 방식으로 바꾸는 것을 고려할 수 있다.
수천 개 MonoBehaviour Update -> 중앙 Update Manager -> 필요한 객체만 Tick
하지만 이것 역시 무조건 적용할 최적화는 아니다.
Profiler에서 실제 비용이 확인될 때 적용해야 한다.
Find 계열 API 최소화
다음과 같은 API를 반복 호출하는 구조는 피하는 것이 좋다.
GameObject.Find(...)
GameObject.FindWithTag(...)
FindObjectOfType<T>()
FindObjectsOfType<T>()
특히 Update에서 반복 호출하는 것은 피해야 한다.
void Update()
{
var player = FindObjectOfType<Player>();
}
대신 초기화 시점에 참조를 확보한다.
private Player player;
private void Awake()
{
player = FindObjectOfType<Player>();
}
더 좋은 방법은 아예 Inspector 또는 명시적인 초기화 구조를 사용하는 것이다.
[SerializeField]
private Player player;
Inspector Reference -> 초기화 -> 캐싱 -> 반복 사용
SendMessage와 BroadcastMessage
문자열 기반 메시지 호출은 직접 메서드를 호출하는 것보다 비용과 추적 가능성 측면에서 불리하다.
SendMessage("TakeDamage", damage);
가능하면 명시적인 참조와 인터페이스 등을 사용하는 것이 낫다.
IDamageable target = ...;
target.TakeDamage(damage);
SendMessage -> Reflection-like lookup -> 대상 호출
보다는
Reference -> Interface / Method Call
구조가 명확하다.
Camera.main 캐싱
Camera.main을 매 프레임 여러 번 호출하는 구조보다는 필요한 참조를 캐싱하는 편이 좋다.
private Camera mainCamera;
private void Awake()
{
mainCamera = Camera.main;
}
다만 최신 Unity에서는 API 자체의 내부 구현과 비용을 과거 버전의 글처럼 단순하게 일반화할 필요는 없다.
핵심은 반복해서 찾을 필요가 없는 객체를 반복 검색하지 않는 것이다.
Garbage Collection 최적화
C# 기반 Unity 게임에서 중요한 문제 중 하나가 GC다.
핵심은 GC를 직접 관리하는 것보다 불필요한 Managed Allocation을 줄이는 것이다.
Managed Allocation 증가 -> Garbage 증가 -> GC 발생 -> CPU Spike
특히 실시간 게임에서 프레임 중 큰 GC가 발생하면 순간적인 프레임 드랍이 발생할 수 있다.
반복적인 new 생성 줄이기
다음과 같은 코드는 반복 호출될 경우 주의해야 한다.
void Update()
{
var data = new SomeData();
}
매 프레임 객체를 생성하고 버리는 구조는 불필요한 Garbage를 만든다.
필요하다면 객체를 재사용한다.
new -> 사용 -> 폐기
보다는
생성 -> 캐싱 -> 재사용
구조를 고려한다.
Object Pooling
반복적으로 생성되고 제거되는 객체는 Object Pooling을 사용할 수 있다.
대표적인 대상은 다음과 같다.
- 총알
- 파티클
- 적 캐릭터
- 데미지 텍스트
- 이펙트
- 투사체
- 임시 UI
기본 구조는 다음과 같다.
Pool 생성 -> 객체 생성 -> 비활성화 -> Pool 반환 -> 재사용
예를 들어 총알을 발사할 때마다
Instantiate()
Destroy()
하는 대신,
Pool -> Get -> Activate -> 사용 -> Deactivate -> Return
구조로 만든다.
단, 모든 객체에 Object Pooling을 적용할 필요는 없다.
생성/삭제 빈도가 낮은 객체까지 풀링하면 코드 복잡도와 메모리 사용량만 증가할 수 있다.
문자열 최적화
문자열은 생각보다 자주 Allocation을 발생시킬 수 있다.
특히 다음과 같은 코드를 매 프레임 실행하는 구조는 주의한다.
scoreText.text = "Score : " + score;
UI가 실제로 매 프레임 갱신될 필요가 없다면 값이 변경될 때만 갱신한다.
매 프레임 UI 갱신 -> 값 변경 시 UI 갱신
예를 들어 점수가 변경될 때만 업데이트한다.
if (score != previousScore)
{
scoreText.text = $"Score : {score}";
previousScore = score;
}
문자열을 반복적으로 조합하는 복잡한 로직에서는 StringBuilder를 고려할 수 있다.
하지만 단순한 문자열 하나를 만들 때마다 무조건 StringBuilder를 사용하는 것도 과도한 최적화다.
Boxing 주의
값 타입을 object나 인터페이스 등 참조 타입으로 변환하는 과정에서 Boxing이 발생할 수 있다.
int value = 10;
object obj = value;
이런 코드가 고빈도 경로에서 반복되면 Allocation이 발생할 수 있다.
특히 다음과 같은 코드를 확인할 필요가 있다.
object사용- 비제네릭 컬렉션
- 값 타입을 사용하는 인터페이스 호출
- 문자열 포맷팅
- 일부 Delegate 사용
- 일부 컬렉션 처리
중요한 것은 “Boxing은 무조건 나쁘다”가 아니라 “고빈도 경로에서 불필요한 Allocation을 만드는 Boxing이 문제”라는 점이다.
foreach에 대한 오래된 오해
과거 Unity/C# 최적화 글에서 foreach는 GC Allocation 때문에 사용을 피해야 한다는 설명이 자주 등장했다.
현재 Unity와 C# 환경에서는 이 내용을 그대로 적용하면 안 된다.
컬렉션과 Enumerator 구현에 따라 동작이 다르고, 과거 버전에서 문제가 되었던 특정 Boxing 사례를 현대 Unity 전체에 일반화할 수 없다.
따라서
foreach = 무조건 금지
가 아니라
Profiler -> 실제 Allocation / CPU 비용 확인 -> 필요한 경우 최적화
가 맞다.
Coroutine 최적화
Coroutine 자체가 나쁜 것은 아니다.
문제는 Coroutine 내부에서 반복적으로 객체를 생성하는 구조다.
예를 들어 다음과 같은 코드는 반복 생성에 주의해야 한다.
yield return new WaitForSeconds(1f);
동일한 대기 시간을 반복적으로 사용하는 경우 캐싱을 고려할 수 있다.
private readonly WaitForSeconds wait = new WaitForSeconds(1f);
그리고
yield return wait;
다만 최신 Unity에서는 Coroutine의 정확한 비용과 사용 패턴을 실제 프로젝트에서 측정해야 한다.
Coroutine은 비동기 게임 로직을 표현하기 위한 도구이지, 성능 최적화 도구가 아니다.
Physics 최적화
Physics는 CPU 비용이 상당할 수 있다.
특히 충돌 대상이 많거나 복잡한 Collider를 사용하는 경우 비용이 증가한다.
따라서 다음을 관리한다.
- 불필요한 Collider 제거
- Collider 단순화
- Layer Collision Matrix 활용
- 불필요한 Rigidbody 제거
- Physics Query 범위 제한
- 필요 없는 Continuous Collision Detection 사용 최소화
- Fixed Timestep 검토
특히 Layer Collision Matrix는 충돌하지 않아도 되는 객체 간 Physics 계산 자체를 줄이는 데 유용하다.
모든 객체 충돌 검사 -> 필요한 Layer끼리만 충돌 검사
렌더링 최적화
렌더링 최적화는 CPU와 GPU를 분리해서 생각해야 한다.
Rendering -> CPU Rendering Cost + GPU Rendering Cost
CPU에서는 Draw Call 및 Render State 관련 작업이 문제가 될 수 있고, GPU에서는 Vertex, Fragment, Texture Sampling, Shader, Overdraw 등이 문제가 될 수 있다.
Draw Call 최적화
과거 Unity 최적화 글에서는 Static Batching과 Dynamic Batching을 중심으로 설명하는 경우가 많았다.
Unity 6에서는 렌더링 파이프라인에 따라 접근 방법이 달라진다.
특히 SRP 기반 프로젝트에서는 SRP Batcher가 중요한 최적화 수단이다. SRP Batcher는 동일한 Shader Variant를 사용하는 Material의 렌더링 준비 과정에서 CPU 비용을 줄이는 방식이다.
따라서 단순히
Draw Call 숫자 줄이기
만 생각하면 안 된다.
현재는 다음과 같은 렌더링 기술을 상황에 따라 선택한다.
SRP Batcher / GPU Instancing / GPU Resident Drawer / Static Batching
Unity 6의 공식 문서에서도 렌더링 환경에 따라 최적화 방법의 선택이 달라진다고 설명한다. Dynamic Batching은 현재 일반적인 최우선 최적화 방법으로 권장되지 않는다.
SRP Batcher
URP나 HDRP를 사용하는 프로젝트에서는 SRP Batcher 호환성을 확인하는 것이 중요하다.
SRP Batcher는 Material 데이터를 GPU 메모리에 유지하고 Render State 변경을 줄이는 방식으로 CPU 렌더링 비용을 줄인다.
따라서 Material을 무조건 하나로 합치는 것보다 Shader Variant와 SRP Batcher 호환성을 고려한 Material 구성이 중요하다.
GPU Instancing
동일한 Mesh와 Material을 대량으로 렌더링하는 경우 GPU Instancing이 효과적이다.
대표적으로
- 나무
- 풀
- 바위
- 장식물
- 반복되는 몬스터
- 대량의 동일 Mesh
등이 있다.
동일 Mesh + 동일 Material + 다수 Instance -> GPU Instancing
GPU Instancing은 여러 Mesh 복사본을 하나의 Draw Call에서 렌더링할 수 있도록 하는 기술이다. 각 Instance마다 색상이나 스케일 같은 일부 프로퍼티를 다르게 지정하는 것도 가능하다.
GPU Resident Drawer
Unity 6의 URP에서는 GPU Resident Drawer가 추가되었다.
GPU Resident Drawer는 BatchRendererGroup을 활용해 GPU Instancing 기반으로 GameObject를 렌더링하고 CPU 렌더링 작업을 줄이는 시스템이다.
대규모 환경에서 많은 Renderable Object를 처리하는 경우 중요한 최적화 수단이 될 수 있다.
따라서 Unity 6에서는 단순히
GameObject 수가 많다 -> Mesh를 합친다
라고 접근하기보다
대량 Object -> Culling -> GPU Instancing / GPU Resident Drawer -> 렌더링
이라는 현대적인 구조도 고려해야 한다.
Material 관리
Material 수가 지나치게 많으면 렌더링 상태 변경과 관리 비용이 증가할 수 있다.
가능하면 동일한 Material을 공유한다.
Object A -> Material A
Object B -> Material A
Object C -> Material A
처럼 공유 가능한 리소스를 공유한다.
텍스처 역시 Texture Atlas 등을 활용해 Material 수를 줄일 수 있다.
다만 URP와 HDRP에서는 MaterialPropertyBlock 사용이 SRP Batcher와 충돌할 수 있으므로 무조건 사용하지 말고 현재 렌더링 구조를 확인해야 한다. Unity 공식 문서 역시 URP/HDRP에서 해당 API 사용을 피하는 방향을 안내하고 있다.
Mesh 최적화
Mesh의 Vertex와 Triangle 수는 GPU 성능에 직접적인 영향을 준다.
하지만 단순히 Triangle 수만 줄이는 것으로 끝나지 않는다.
Mesh의 Vertex는 UV Seam, Normal, Tangent, Hard Edge 등에 의해 분리될 수 있다.
따라서
Geometry -> UV / Normal / Tangent / Hard Edge -> 실제 Vertex 수
구조를 이해해야 한다.
필요 이상의 고해상도 Mesh를 사용하지 않는다.
그리고 카메라에서 멀어지는 객체는 LOD를 사용한다.
Distance 증가 -> LOD 0 -> LOD 1 -> LOD 2 -> Culling
LOD는 동일한 Mesh를 계속 사용하는 것이 아니라 거리별로 다른 수준의 Geometry를 사용하는 방식이다.
LOD와 HLOD
LOD는 개별 Object의 Geometry 복잡도를 줄이는 방식이다.
HLOD는 여러 Object를 하나의 더 큰 단위로 묶어서 관리하는 방식이다.
LOD -> 개별 Object Geometry 최적화
HLOD -> Object Group / Cluster 단위 최적화
대규모 오픈월드에서는 HLOD와 Culling을 함께 고려할 수 있다.
Occlusion Culling
카메라에 보이지 않는 Object를 렌더링하지 않는 것이 Culling이다.
특히 건물이나 벽이 많은 씬에서는 Occlusion Culling의 효과가 크다.
Camera -> Visible Object -> Rendering
대신
Camera -> Visibility Test -> Visible Object만 Rendering
으로 만든다.
Unity 6 URP에서는 GPU Occlusion Culling도 사용할 수 있으며, GPU가 다른 Object에 가려진 Object를 판별하여 CPU 작업을 줄일 수 있다.
다만 모든 씬에서 Occlusion Culling이 이득인 것은 아니다.
넓게 트인 야외 씬처럼 가려지는 Object가 적다면 비용 대비 효과가 낮을 수 있다.
Texture 최적화
Texture는 메모리와 GPU Bandwidth에 직접적인 영향을 준다.
기본적으로 다음을 확인한다.
- 필요 이상의 해상도를 사용하지 않는다.
- 플랫폼에 맞는 Texture Compression을 사용한다.
- 3D 환경에서는 Mipmap을 고려한다.
- 필요하지 않다면 Read/Write Enabled를 켜지 않는다.
- Alpha 채널이 필요하지 않다면 제거를 고려한다.
- 동일 Texture를 공유한다.
특히 Texture Import Settings는 플랫폼별로 다르게 설정하는 것이 중요하다.
원본 Texture -> 플랫폼별 Compression -> Runtime GPU Texture
모든 플랫폼에서 동일한 Texture 포맷을 사용할 필요는 없다.
Mipmap
Mipmap은 Texture의 여러 해상도 버전을 미리 만들어 두는 방식이다.
카메라에서 멀어질수록 작은 Mipmap을 사용한다.
Full Resolution -> Mipmap 1 -> Mipmap 2 -> Mipmap 3 -> ...
이를 통해 멀리 있는 Texture에 불필요하게 높은 해상도를 사용하는 것을 줄일 수 있다.
특히 3D 씬에서 Texture Filtering과 함께 중요하다.
Shader 최적화
Shader는 GPU 비용에 직접적인 영향을 준다.
특히 Fragment Shader는 화면의 픽셀마다 실행될 수 있기 때문에 복잡한 계산이 큰 비용으로 이어질 수 있다.
주의할 항목은 다음과 같다.
- 복잡한 Fragment 연산
- 많은 Texture Sampling
- 높은 Shader Instruction Count
- 불필요한 Shader Variant
- 높은 Overdraw
- 불필요한 Transparent Material
특히 모바일 환경에서는 Fragment Cost와 Overdraw가 매우 중요하다.
Overdraw
Overdraw는 같은 화면 픽셀이 여러 번 그려지는 현상이다.
예를 들어 투명한 UI나 Particle이 겹쳐 있으면 하나의 픽셀이 여러 번 계산될 수 있다.
Background -> Particle -> Transparent Effect -> UI
한 화면 픽셀에 여러 번 Fragment Shader가 실행된다.
따라서
- 불필요한 투명 영역 제거
- Particle 수 조절
- 큰 Transparent Quad 최소화
- UI 겹침 최소화
등을 고려해야 한다.
Lighting 최적화
Lighting은 CPU와 GPU 모두에 영향을 줄 수 있다.
가능한 경우 정적인 조명은 Bake한다.
Static Lighting -> Light Baking -> Runtime Cost 감소
반면 모든 Light를 실시간으로 계산하면 비용이 증가한다.
특히
- Real-time Shadow
- 많은 Light
- 높은 Shadow Resolution
- 많은 Cascade
- 복잡한 Lighting 계산
등을 관리해야 한다.
Shadow 최적화
Shadow는 비용이 큰 렌더링 요소 중 하나다.
특히 여러 Light가 각각 Shadow Map을 생성하면 렌더링 비용이 증가한다.
따라서
- Shadow를 꼭 필요한 Light에만 적용
- Shadow Distance 조절
- Shadow Resolution 조절
- Shadow Cascade 조절
- 멀리 있는 Object의 Shadow 제거
- Static Lighting 활용
등을 고려한다.
Shadow Quality 증가 -> GPU Cost 증가
화질과 성능의 균형이 필요하다.
Particle 최적화
Particle System은 단순히 Particle 개수만 보는 것이 아니다.
다음 항목을 함께 확인해야 한다.
- Particle Count
- Particle Lifetime
- Texture Size
- Overdraw
- Material
- Blend Mode
- Simulation Cost
- Collision
- Lights
- Trails
특히 모바일에서는 Particle이 화면을 크게 덮는 경우 Overdraw가 병목이 되기 쉽다.
Particle Count 감소보다 화면을 덮는 영역 감소가 더 중요한 경우도 있다.
UI 최적화
Unity UI에서는 Canvas 구조가 중요하다.
UI가 자주 변경되는 구조라면 하나의 거대한 Canvas에 모든 UI를 넣는 것보다 변경 빈도에 따라 분리하는 것이 유리할 수 있다.
Static UI -> Static Canvas
Dynamic UI -> Dynamic Canvas
예를 들어 체력바가 계속 변경된다고 해서 전체 HUD가 매번 다시 처리될 필요는 없다.
또한
- 불필요한 Layout Group 제거
- Content Size Fitter 사용 주의
- 불필요한 Graphic Raycaster 제거
- 과도한 UI 중첩 제거
- Sprite Atlas 활용
- UI Texture 해상도 관리
등을 고려한다.
Asset 최적화
에셋은 CPU와 GPU뿐 아니라 메모리와 로딩 시간에도 영향을 준다.
확인해야 할 주요 항목은 다음과 같다.
Texture -> Resolution / Compression / Mipmap / Read Write
Mesh -> Vertex / Triangle / Compression / Read Write
Animation -> Keyframe / Compression / Rig
Audio -> Compression / Sample Rate / Load Type
특히 모바일과 콘솔에서는 플랫폼별 Import Settings가 중요하다.
Mesh Read/Write
CPU에서 Mesh 데이터에 접근할 필요가 없다면 Read/Write를 활성화할 이유가 없다.
Read/Write가 필요한 경우 Unity가 CPU 측 Mesh 데이터도 유지해야 할 수 있기 때문에 메모리 사용량이 증가할 수 있다.
따라서
CPU Mesh Access 필요 없음 -> Read/Write 비활성화
를 기본적인 검토 대상으로 삼는다.
Animation 최적화
Animation은 CPU 비용과 메모리 사용량에 영향을 준다.
불필요한 Bone을 줄이고 Animation Clip을 압축하며 필요 없는 Transform을 애니메이션하지 않는다.
캐릭터가 복잡해질수록
Character Count 증가 -> Animator / Skinning / Transform Cost 증가
가 발생한다.
따라서 대규모 캐릭터 게임에서는 Animation LOD나 Animator 구조 자체를 설계 단계부터 고려해야 한다.
Addressables와 Resources
과거 최적화 글에서는
Resources 대신 AssetBundle
이라는 식으로 설명하는 경우가 많았다.
Unity 6 프로젝트에서는 일반적인 런타임 에셋 관리에서 Addressables를 우선 검토하는 것이 자연스럽다.
Addressables는 에셋의 로딩과 메모리 수명을 명시적으로 관리하고 원격 콘텐츠 배포 등의 구조를 만들 수 있다.
중요한 것은 특정 API 하나를 사용하는 것이 아니라
Asset Reference -> Load -> Use -> Release
라는 생명주기를 명확하게 만드는 것이다.
메모리 최적화
메모리 최적화는 단순히 RAM 사용량을 줄이는 것이 아니다.
중요한 것은
Memory Usage -> Allocation -> Lifetime -> Residency
를 관리하는 것이다.
필요 없는 Asset을 계속 메모리에 유지하지 않는다.
사용이 끝난 Asset은 적절한 시점에 Release한다.
동시에 너무 많은 Asset을 미리 로드하면 메모리 압박이 발생할 수 있다.
반대로 지나치게 자주 로드/언로드하면 로딩 비용과 GC가 증가할 수 있다.
따라서 Asset Lifetime을 설계해야 한다.
GC를 직접 호출하지 말 것
성능 문제가 있다고 해서 무조건 GC를 직접 호출하는 것은 좋은 해결책이 아니다.
GC.Collect();
이런 코드는 GC 자체를 없애는 것이 아니라 GC 실행 시점을 개발자가 강제로 지정하는 것이다.
필요하다면 로딩 화면이나 씬 전환처럼 게임플레이가 아닌 시점에서 GC 정책을 고려할 수 있지만, 일반적인 게임플레이 중에는 Allocation 자체를 줄이는 것이 우선이다.
GC 호출 -> 문제 해결
보다는
Allocation 감소 -> Garbage 감소 -> GC 부담 감소
가 근본적인 접근이다.
Profiler 중심 최적화
최적화에서 가장 중요한 도구는 결국 Profiler다.
Unity 6 Profiler에서는 CPU, Rendering, Memory 등의 데이터를 확인할 수 있으며 Rendering Profiler에서는 Batches, SetPass Calls, Triangles, Vertices 등의 지표를 확인할 수 있다.
예를 들어 CPU 병목이라면
CPU Usage -> Script / Physics / Animation / Rendering
GPU 병목이라면
GPU Usage -> Shader / Overdraw / Resolution / Shadow / Post Processing
메모리 병목이라면
Memory -> Texture / Mesh / Audio / Managed Heap
방향으로 원인을 좁힌다.
CPU Bound와 GPU Bound 구분
게임이 느리다는 사실만으로 CPU 문제인지 GPU 문제인지 알 수 없다.
CPU가 병목이면 GPU가 놀고 있을 수 있다.
GPU가 병목이면 CPU를 아무리 최적화해도 FPS가 크게 증가하지 않는다.
따라서 먼저 병목을 구분해야 한다.
Frame Time -> CPU Bound / GPU Bound
60 FPS를 목표로 한다면 한 프레임의 예산은 약 16.67ms다.
1000ms / 60 = 16.67ms
120 FPS라면 약 8.33ms다.
1000ms / 120 = 8.33ms
즉 목표 FPS가 높아질수록 프레임 예산은 급격하게 줄어든다.
개발 PC가 아닌 실제 타겟에서 측정
에디터에서 빠르게 돌아간다고 실제 빌드도 빠른 것은 아니다.
특히 모바일에서는
- CPU 성능
- GPU 성능
- Memory Bandwidth
- 발열
- Thermal Throttling
- 저장장치 속도
등이 실제 성능에 영향을 준다.
따라서 최적화는 최종 타겟 플랫폼에서 측정해야 한다.
Editor -> Development Build -> Target Device -> Profiler -> 측정
Unity Profiler는 네트워크를 통해 실제 디바이스에 연결하여 성능을 분석할 수도 있다.
최적화 체크리스트
CPU
- 반복적인 Find 계열 호출 제거
- 불필요한 Update 제거
- 대량 Update 구조 검토
- SendMessage/BroadcastMessage 최소화
- Physics Layer 설정
- Physics Query 범위 제한
- 불필요한 Animation 제거
- 반복적인 Allocation 제거
- Boxing 확인
- Object Pooling 적용
- 문자열 생성 최소화
Rendering
- SRP Batcher 확인
- GPU Instancing 검토
- GPU Resident Drawer 검토
- Material 공유
- Shader Variant 관리
- Draw Call 확인
- SetPass Call 확인
- LOD 적용
- HLOD 검토
- Occlusion Culling 검토
- Shadow 최소화
- Overdraw 확인
- Post Processing 비용 확인
GPU
- Vertex 수 관리
- Triangle 수 관리
- Fragment Shader 복잡도 관리
- Texture Sampling 감소
- Transparent Object 최소화
- Particle Overdraw 감소
- Shadow Resolution 조절
- Render Resolution 조절
Texture
- 적절한 해상도 사용
- 플랫폼별 Compression
- Mipmap 검토
- Read/Write 비활성화
- 불필요한 Alpha 제거
- Texture Atlas 활용
- 중복 Texture 제거
Mesh
- 불필요한 Vertex 제거
- 불필요한 Triangle 제거
- LOD 적용
- Read/Write 검토
- Mesh Compression 검토
- 필요 없는 Rig 제거
Memory
- Asset Lifetime 관리
- Addressables 활용
- 불필요한 Asset Release
- Texture Memory 관리
- Mesh Memory 관리
- Audio Memory 관리
- Managed Allocation 감소
- GC Spike 확인
UI
- Canvas 분리
- Layout 연산 최소화
- Content Size Fitter 사용 주의
- Graphic Raycaster 최소화
- UI Overdraw 감소
- Sprite Atlas 활용
- 불필요한 UI 갱신 제거
오래된 Unity 최적화 상식 정리
과거 Unity 최적화 글에는 현재 기준으로 수정해야 할 내용이 상당히 많다.
foreach는 무조건 금지
-> 현재는 잘못된 일반화다.
Dynamic Batching을 적극 활용
-> Unity 6에서는 SRP Batcher, GPU Instancing, GPU Resident Drawer 등 렌더링 구조를 먼저 고려해야 한다. Unity 공식 문서에서도 Dynamic Batching은 더 이상 일반적인 권장 방식이 아니다.
Resources 대신 AssetBundle
-> 현재는 Addressables 기반의 에셋 관리 구조를 우선 검토할 수 있다.
Draw Call 숫자만 줄이면 된다
-> 잘못된 접근이다. CPU Render Thread, SetPass, Shader, GPU, Overdraw, Culling 등 전체 렌더링 비용을 봐야 한다.
Vector3 연산은 무조건 줄여야 한다
-> 의미 없는 수준의 연산까지 줄이는 것은 최적화가 아니다. 실제 Hot Path에서 비용이 확인될 때 최적화한다.
GC를 직접 호출해서 관리한다
-> 근본적인 해결책이 아니다. Allocation 자체를 줄이는 것이 우선이다.
최적화의 본질
Unity 최적화에서 가장 중요한 기술은 특정 API를 외우는 것이 아니다.
성능 비용이 어디에서 발생하는지 측정할 수 있는 능력이다.
게임의 성능은 하나의 숫자로 결정되지 않는다.
Gameplay -> CPU -> Physics -> Animation -> Rendering -> GPU -> Memory -> IO
각 시스템이 서로 영향을 준다.
따라서 최적화의 기본 원칙은 다음과 같다.
Measure -> Identify -> Optimize -> Measure Again
그리고 최적화에는 항상 비용이 따른다.
CPU <-> GPU
Memory <-> CPU
Quality <-> Performance
Code Complexity <-> Runtime Cost
좋은 최적화는 단순히 FPS를 높이는 최적화가 아니다.
필요한 품질을 유지하면서 실제 병목을 제거하고, 그 결과를 수치로 증명하는 최적화다.
Unity 6에서는 특히 전통적인 “Draw Call 줄이기”, “foreach 쓰지 않기”, “Static Batching 사용하기” 같은 단편적인 규칙보다 Profiler 기반의 측정과 SRP 기반 렌더링 구조에 대한 이해가 훨씬 중요하다.