60fps를 사수하는 Unity 성능 최적화 실전 가이드
"게임이 PC에서는 잘 돌아가는데, 모바일에서는 10fps밖에 안 나와요."
Unity는 쉽게 시작할 수 있지만, 최적화는 어렵습니다. 특히 60fps를 유지하려면 16.6ms 안에 모든 것을 처리해야 합니다. CPU 로직, GPU 렌더링, 물리 시뮬레이션, 가비지 컬렉션까지.
이 글은 Unity의 내부 동작 방식부터 최신 DOTS(Data-Oriented Technology Stack)까지, 실전에서 바로 써먹을 수 있는 최적화 기법을 다룹니다.
프레임 시작
│
├─ Awake() ← 모든 GameObject (순서 불확정)
├─ OnEnable()
├─ Start() ← 첫 Update 전 1회
│
├─ FixedUpdate() ← 고정 시간 간격 (0.02초)
│ └─ 물리 시뮬레이션
│ └─ OnCollisionXXX()
│ └─ OnTriggerXXX()
│
├─ Update() ← 매 프레임
│ └─ 입력 처리
│ └─ 게임 로직
│
├─ LateUpdate() ← 모든 Update 후
│ └─ 카메라 추적
│
├─ OnPreRender() ← 렌더링 직전 (카메라별)
├─ OnRenderObject()
├─ OnPostRender()
│
├─ OnGUI() ← 레거시 GUI (매우 느림!)
│
└─ OnApplicationQuit()
└─ OnDisable()
└─ OnDestroy()
public class LifecycleExample : MonoBehaviour
{
// Update: 매 프레임 (가변 주기)
void Update()
{
// 프레임마다 다른 deltaTime
// 60fps: 0.0166초
// 30fps: 0.0333초
// 용도:
// - 입력 처리
// - 카메라 컨트롤
// - UI 업데이트
// - 일반 게임 로직
if (Input.GetKeyDown(KeyCode.Space))
{
Jump();
}
transform.position += velocity * Time.deltaTime;
}
// FixedUpdate: 고정 시간 간격 (기본 0.02초 = 50Hz)
void FixedUpdate()
{
// 항상 같은 Time.fixedDeltaTime (0.02초)
// 용도:
// - 물리 계산
// - Rigidbody 조작
// ❌ Update에서 물리
// rigidbody.AddForce(Vector3.forward); // 프레임마다 다른 힘!
// ✅ FixedUpdate에서 물리
rigidbody.AddForce(Vector3.forward); // 항상 일정한 힘
}
// LateUpdate: 모든 Update 후
void LateUpdate()
{
// 용도:
// - 카메라 추적 (플레이어가 먼저 움직인 후)
// - 타겟 따라가기
// - Procedural Animation
// 플레이어 위치는 Update에서 이미 변경됨
Vector3 desiredPosition = player.position + offset;
transform.position = Vector3.Lerp(
transform.position,
desiredPosition,
Time.deltaTime * smoothSpeed
);
}
}
프레임 타임라인:
시간 0ms 16ms 32ms 48ms 64ms
프레임 ├────────┼────────┼────────┼────────┼────
Frame 1 Frame 2 Frame 3 Frame 4
Frame 1 (16ms):
├─ FixedUpdate (20ms 경과했으면)
├─ FixedUpdate (40ms 경과했으면)
├─ Update
└─ LateUpdate
프레임 드롭 시 (50ms):
├─ FixedUpdate (20ms)
├─ FixedUpdate (40ms)
├─ FixedUpdate (60ms) ← 3번 호출!
├─ Update
└─ LateUpdate
// Script Execution Order 설정
// Edit → Project Settings → Script Execution Order
// 또는 속성으로
[DefaultExecutionOrder(-100)] // 다른 스크립트보다 먼저
public class InputManager : MonoBehaviour
{
void Update()
{
// 입력 처리 (다른 스크립트보다 먼저)
}
}
[DefaultExecutionOrder(100)] // 다른 스크립트보다 나중에
public class CameraController : MonoBehaviour
{
void LateUpdate()
{
// 카메라 업데이트 (플레이어 움직임 후)
}
}
public class UpdateOptimization : MonoBehaviour
{
// ❌ 빈 Update는 오버헤드
void Update()
{
// 아무것도 안 함
}
// → 삭제하세요!
// ❌ 매 프레임 무거운 연산
void Update()
{
GameObject enemy = GameObject.Find("Enemy"); // 매우 느림!
Transform target = GameObject.FindWithTag("Player").transform; // 느림!
}
// ✅ 캐싱
private GameObject enemy;
private Transform target;
void Start()
{
enemy = GameObject.Find("Enemy"); // 1회만
target = GameObject.FindWithTag("Player").transform;
}
void Update()
{
// 캐시된 참조 사용
}
// ✅ 필요할 때만 실행
private float updateInterval = 0.1f;
private float lastUpdate = 0f;
void Update()
{
if (Time.time - lastUpdate < updateInterval)
return;
lastUpdate = Time.time;
// 0.1초마다만 실행
ExpensiveOperation();
}
}
Unity는 C#의 Garbage Collector를 사용합니다. 힙에 할당된 메모리 중 더 이상 참조되지 않는 것을 자동으로 해제합니다.
문제는 타이밍입니다:
GC 실행 중에는 게임이 멈춥니다!
60fps (16.6ms per frame):
├─ 게임 로직: 5ms
├─ 렌더링: 8ms
├─ GC: 0ms ← 정상
└─ 여유: 3.6ms
GC 발생:
├─ 게임 로직: 5ms
├─ 렌더링: 8ms
├─ GC: 50ms ← 💥 프레임 드롭!
└─ 총: 63ms → 15fps로 추락
public class GCAllocations : MonoBehaviour
{
// ❌ 매 프레임 할당
void Update()
{
string text = "Score: " + score; // 문자열 연결 → 새 문자열 생성
Vector3[] positions = new Vector3[100]; // 배열 생성
var result = GetEnemies(); // List 생성
}
List<Enemy> GetEnemies()
{
return new List<Enemy>(); // 매번 새 List
}
// ❌ 박싱 (Boxing)
void BadBoxing()
{
int value = 42;
object obj = value; // int → object (힙 할당!)
// 특히 조심: foreach + Dictionary/Hashtable
foreach (var key in dictionary.Keys) // IEnumerator 박싱!
{
// ...
}
}
// ❌ 람다 캡처
void BadLambda()
{
int localVar = 10;
button.onClick.AddListener(() => {
Debug.Log(localVar); // 캡처 → 클로저 생성 (힙 할당)
});
}
}
public class GCOptimizations : MonoBehaviour
{
// ✅ StringBuilder (문자열 연결)
private StringBuilder sb = new StringBuilder(100);
void UpdateScore(int score)
{
sb.Clear();
sb.Append("Score: ");
sb.Append(score);
scoreText.text = sb.ToString(); // 1번만 할당
}
// ✅ 오브젝트 풀
private List<Enemy> enemyPool = new List<Enemy>(100);
private List<Enemy> activeEnemies = new List<Enemy>(100);
List<Enemy> GetEnemies()
{
activeEnemies.Clear(); // 재사용
// enemyPool에서 가져오기
return activeEnemies;
}
// ✅ 구조체 (Stack)
struct EnemyData // class 대신 struct
{
public Vector3 position;
public int health;
}
// ✅ 배열 재사용
private Vector3[] positionsCache = new Vector3[100];
void UpdatePositions()
{
// positionsCache 재사용 (할당 없음)
for (int i = 0; i < enemies.Count; i++)
{
positionsCache[i] = enemies[i].position;
}
}
// ✅ 박싱 방지
void NoBo xing()
{
// List<T>.Enumerator는 struct (박싱 없음)
foreach (var enemy in enemies)
{
// ...
}
// Dictionary도 마찬가지
foreach (var kvp in dictionary)
{
// ...
}
}
// ✅ 람다 캡처 방지
void NoCapture()
{
// 지역 변수 캡처 안 함
button.onClick.AddListener(OnButtonClick);
}
void OnButtonClick()
{
// 멤버 함수 사용
}
}
Unity 2019+는 Incremental GC를 지원합니다.
// Incremental GC:
// - GC를 여러 프레임에 나눠서 실행
// - 한 프레임에 최대 2ms만 사용
// - 프레임 드롭 완화
// 설정
// Player Settings → Other Settings → Garbage Collector
// → Incremental
// 모니터링
void Update()
{
// GC 메모리
long totalMemory = GC.GetTotalMemory(false);
// GC 횟수
int gen0Collections = GC.CollectionCount(0);
int gen1Collections = GC.CollectionCount(1);
int gen2Collections = GC.CollectionCount(2);
Debug.Log($"GC Gen0: {gen0Collections}, Gen1: {gen1Collections}, Gen2: {gen2Collections}");
}
GC 세대:
Gen 0 (신생):
- 새로 할당된 객체
- 자주 수집 (빠름)
- 대부분 여기서 해제
Gen 1 (중간):
- Gen 0에서 살아남은 객체
- 가끔 수집
Gen 2 (노년):
- 오래 살아남은 객체
- 드물게 수집 (느림)
- 여기 도달하면 오래 남음
public class CoroutineExample : MonoBehaviour
{
// 기본 사용
void Start()
{
StartCoroutine(FadeOut());
}
IEnumerator FadeOut()
{
float alpha = 1f;
while (alpha > 0)
{
alpha -= Time.deltaTime;
canvasGroup.alpha = alpha;
yield return null; // 다음 프레임까지 대기
}
Destroy(gameObject);
}
// 시간 대기
IEnumerator DelayedAction()
{
Debug.Log("Start");
yield return new WaitForSeconds(2f); // 2초 대기
Debug.Log("After 2 seconds");
}
// 조건 대기
IEnumerator WaitForCondition()
{
yield return new WaitUntil(() => player.health <= 0);
ShowGameOverScreen();
}
// WWW/UnityWebRequest
IEnumerator DownloadTexture(string url)
{
UnityWebRequest www = UnityWebRequestTexture.GetTexture(url);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
Texture2D texture = DownloadHandlerTexture.GetContent(www);
renderer.material.mainTexture = texture;
}
}
// 중첩 코루틴
IEnumerator Sequence()
{
yield return StartCoroutine(Step1());
yield return StartCoroutine(Step2());
yield return StartCoroutine(Step3());
}
// 코루틴 정지
void StopFading()
{
StopCoroutine(FadeOut());
// 또는
StopAllCoroutines();
}
}
코루틴의 특징:
장점:
+ Unity API 자유롭게 사용
+ 간단한 지연/애니메이션에 적합
+ GC 압박 적음 (yield return null 재사용)
단점:
- 타입 안전성 없음 (IEnumerator)
- 반환값 처리 어려움
- 예외 처리 불편
- GameObject 파괴되면 자동 중단
using System.Threading.Tasks;
using UnityEngine;
public class AsyncExample : MonoBehaviour
{
// 기본 사용
async void Start()
{
await FadeOutAsync();
}
async Task FadeOutAsync()
{
float alpha = 1f;
while (alpha > 0)
{
alpha -= Time.deltaTime;
canvasGroup.alpha = alpha;
await Task.Yield(); // 다음 프레임
}
Destroy(gameObject);
}
// 시간 대기
async Task DelayedActionAsync()
{
Debug.Log("Start");
await Task.Delay(2000); // 2000ms 대기
Debug.Log("After 2 seconds");
}
// 반환값
async Task<int> CalculateScoreAsync()
{
await Task.Delay(1000);
return 100;
}
async void UseScore()
{
int score = await CalculateScoreAsync();
Debug.Log($"Score: {score}");
}
// 병렬 실행
async Task ParallelLoadingAsync()
{
Task loadMesh = LoadMeshAsync();
Task loadTexture = LoadTextureAsync();
Task loadAudio = LoadAudioAsync();
// 모두 완료될 때까지 대기
await Task.WhenAll(loadMesh, loadTexture, loadAudio);
Debug.Log("All loaded!");
}
// 예외 처리
async Task SafeLoadAsync()
{
try
{
await LoadDataAsync();
}
catch (Exception e)
{
Debug.LogError($"Load failed: {e.Message}");
}
}
// 취소 토큰
private CancellationTokenSource cts;
async void StartLongOperation()
{
cts = new CancellationTokenSource();
try
{
await LongOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Operation cancelled");
}
}
async Task LongOperationAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(10, ct);
}
}
void OnDestroy()
{
cts?.Cancel(); // 취소
}
}
Unity 2023+ Awaitable:
// Unity의 공식 async 지원
async Awaitable FadeOutUnity()
{
float alpha = 1f;
while (alpha > 0)
{
alpha -= Time.deltaTime;
canvasGroup.alpha = alpha;
await Awaitable.NextFrameAsync(); // Unity API 안전!
}
}
// 취소 자동 처리
async Awaitable LoadSceneAsync()
{
// GameObject 파괴되면 자동 취소
await Awaitable.WaitForSecondsAsync(2f);
SceneManager.LoadScene("NextScene");
}
코루틴 사용:
✓ 간단한 지연/대기
✓ 애니메이션/Fade
✓ Unity API 많이 사용
✓ GameObject 수명과 함께
async/await 사용:
✓ 복잡한 비동기 로직
✓ 반환값 필요
✓ 예외 처리 필요
✓ 병렬 실행
✓ I/O 작업 (네트워크, 파일)
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int initialSize = 10;
private Queue<GameObject> pool = new Queue<GameObject>();
void Start()
{
// 미리 생성
for (int i = 0; i < initialSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject Get()
{
if (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
else
{
// 풀이 비었으면 새로 생성
return Instantiate(prefab);
}
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
// 사용
public class BulletSpawner : MonoBehaviour
{
private ObjectPool bulletPool;
void Fire()
{
GameObject bullet = bulletPool.Get();
bullet.transform.position = firePoint.position;
bullet.transform.rotation = firePoint.rotation;
// 3초 후 반환
StartCoroutine(ReturnAfterDelay(bullet, 3f));
}
IEnumerator ReturnAfterDelay(GameObject obj, float delay)
{
yield return new WaitForSeconds(delay);
bulletPool.Return(obj);
}
}
public class GenericObjectPool<T> where T : Component
{
private T prefab;
private Queue<T> pool = new Queue<T>();
private Transform parent;
public GenericObjectPool(T prefab, int initialSize, Transform parent = null)
{
this.prefab = prefab;
this.parent = parent;
for (int i = 0; i < initialSize; i++)
{
T obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
public T Get()
{
T obj;
if (pool.Count > 0)
{
obj = pool.Dequeue();
}
else
{
obj = Object.Instantiate(prefab, parent);
}
obj.gameObject.SetActive(true);
return obj;
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
public void Clear()
{
foreach (var obj in pool)
{
Object.Destroy(obj.gameObject);
}
pool.Clear();
}
}
// 사용
public class PoolManager : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private Enemy enemyPrefab;
private GenericObjectPool<Bullet> bulletPool;
private GenericObjectPool<Enemy> enemyPool;
void Start()
{
bulletPool = new GenericObjectPool<Bullet>(bulletPrefab, 50);
enemyPool = new GenericObjectPool<Enemy>(enemyPrefab, 20);
}
public Bullet GetBullet()
{
Bullet bullet = bulletPool.Get();
bullet.OnSpawn(); // 초기화
return bullet;
}
public void ReturnBullet(Bullet bullet)
{
bullet.OnDespawn(); // 정리
bulletPool.Return(bullet);
}
}
using UnityEngine.Pool;
public class ModernPoolExample : MonoBehaviour
{
[SerializeField] private GameObject prefab;
private ObjectPool<GameObject> pool;
void Start()
{
pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(prefab),
actionOnGet: (obj) => obj.SetActive(true),
actionOnRelease: (obj) => obj.SetActive(false),
actionOnDestroy: (obj) => Destroy(obj),
collectionCheck: true, // 이미 풀에 있는지 체크
defaultCapacity: 10,
maxSize: 100
);
}
void Fire()
{
GameObject bullet = pool.Get();
bullet.transform.position = firePoint.position;
// 자동 반환
StartCoroutine(AutoReturn(bullet, 3f));
}
IEnumerator AutoReturn(GameObject obj, float delay)
{
yield return new WaitForSeconds(delay);
pool.Release(obj);
}
void OnDestroy()
{
pool.Dispose(); // 정리
}
}
// ❌ 매 프레임 머티리얼 생성
void Update()
{
Material mat = new Material(shader); // 새 머티리얼
renderer.material = mat; // Draw Call 증가!
}
// ✅ 머티리얼 재사용
private Material sharedMat;
void Start()
{
sharedMat = new Material(shader);
}
void Update()
{
renderer.sharedMaterial = sharedMat; // 같은 머티리얼 공유
}
// ✅ Static Batching
// 절대 움직이지 않는 오브젝트
void Start()
{
gameObject.isStatic = true; // Static으로 마크
// 또는 Inspector에서 Static 체크
}
// ✅ GPU Instancing
// 같은 메시를 여러 개 그리기
// Shader에서 지원 필요:
// #pragma multi_compile_instancing
[SerializeField] private Mesh mesh;
[SerializeField] private Material material;
void Update()
{
Matrix4x4[] matrices = new Matrix4x4[1000];
for (int i = 0; i < 1000; i++)
{
Vector3 pos = new Vector3(i * 2, 0, 0);
matrices[i] = Matrix4x4.TRS(pos, Quaternion.identity, Vector3.one);
}
Graphics.DrawMeshInstanced(mesh, 0, material, matrices);
// 1000개를 1 Draw Call로!
}
// Overdraw: 같은 픽셀을 여러 번 그리기
// ❌ 투명한 UI가 전체 화면 덮음
[SerializeField] private Image backgroundPanel;
void BadUI()
{
// 알파 0.01이어도 전체 픽셀 그림!
backgroundPanel.color = new Color(1, 1, 1, 0.01f);
}
// ✅ Raycast Target 끄기
void GoodUI()
{
// 클릭 필요 없는 UI는 Raycast Target off
backgroundPanel.raycastTarget = false;
// 또는 아예 비활성화
if (backgroundPanel.color.a < 0.01f)
{
backgroundPanel.enabled = false;
}
}
// ✅ Occlusion Culling
// Window → Rendering → Occlusion Culling
// 1. Bake Occlusion Data
// 2. 카메라에 보이지 않는 것 자동 제거
public class LODManager : MonoBehaviour
{
[SerializeField] private Mesh highDetailMesh; // 10000 폴리곤
[SerializeField] private Mesh mediumDetailMesh; // 3000 폴리곤
[SerializeField] private Mesh lowDetailMesh; // 500 폴리곤
private MeshFilter meshFilter;
private Transform cameraTransform;
void Start()
{
meshFilter = GetComponent<MeshFilter>();
cameraTransform = Camera.main.transform;
}
void Update()
{
float distance = Vector3.Distance(transform.position, cameraTransform.position);
if (distance < 10f)
{
meshFilter.mesh = highDetailMesh;
}
else if (distance < 50f)
{
meshFilter.mesh = mediumDetailMesh;
}
else
{
meshFilter.mesh = lowDetailMesh;
}
}
}
// ✅ Unity LOD Group (자동)
// 1. GameObject에 LOD Group 컴포넌트 추가
// 2. LOD 레벨별 메시 할당
// 3. 거리에 따라 자동 전환
// ❌ 복잡한 Fragment Shader
Shader "Custom/Expensive"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 frag(v2f i) : SV_Target
{
// 매 픽셀마다 복잡한 계산
float3 normal = normalize(tex2D(_NormalMap, i.uv).rgb);
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float ndotl = dot(normal, lightDir);
// 반사 계산
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
// 프레넬
float fresnel = pow(1.0 - dot(viewDir, normal), 5.0);
return float4(ndotl + spec + fresnel, 1);
}
ENDCG
}
}
}
// ✅ Vertex Shader로 이동
Shader "Custom/Optimized"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct v2f
{
float4 pos : SV_POSITION;
float3 lighting : TEXCOORD0; // 미리 계산
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Vertex Shader에서 계산 (버텍스 수 << 픽셀 수)
float3 normal = UnityObjectToWorldNormal(v.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
o.lighting = saturate(dot(normal, lightDir));
return o;
}
float4 frag(v2f i) : SV_Target
{
// Fragment Shader는 단순하게
return float4(i.lighting, 1);
}
ENDCG
}
}
}
// Edit → Project Settings → Time → Fixed Timestep
// 기본: 0.02 (50Hz)
// 낮추기: 0.03 (33Hz) → 성능 향상, 정확도 감소
// 높이기: 0.01 (100Hz) → 성능 감소, 정확도 향상
// 동적 조절
void AdjustPhysicsRate()
{
if (Application.isMobilePlatform)
{
Time.fixedDeltaTime = 0.03f; // 모바일: 33Hz
}
else
{
Time.fixedDeltaTime = 0.02f; // PC: 50Hz
}
}
// Edit → Project Settings → Physics → Layer Collision Matrix
// ✅ 불필요한 충돌 비활성화
// 예: Bullet과 Bullet 충돌 불필요
// 예: Enemy와 Enemy 충돌 불필요
// 코드로 체크
void CheckCollisionLayers()
{
int playerLayer = LayerMask.NameToLayer("Player");
int bulletLayer = LayerMask.NameToLayer("Bullet");
// Player와 Bullet 충돌 체크
bool canCollide = !Physics.GetIgnoreLayerCollision(playerLayer, bulletLayer);
if (!canCollide)
{
// 충돌 활성화
Physics.IgnoreLayerCollision(playerLayer, bulletLayer, false);
}
}
public class RigidbodyOptimization : MonoBehaviour
{
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
// ✅ 움직이지 않으면 Kinematic
if (isStaticObstacle)
{
rb.isKinematic = true;
}
// ✅ 회전 안 하면 Freeze
rb.constraints = RigidbodyConstraints.FreezeRotation;
// ✅ Continuous Collision Detection (필요시만)
if (fastMoving)
{
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
}
else
{
rb.collisionDetectionMode = CollisionDetectionMode.Discrete;
}
// ✅ Sleep
rb.sleepThreshold = 0.1f; // 빨리 잠들기
}
// ❌ Transform 직접 수정
void BadMove()
{
transform.position += Vector3.forward * Time.deltaTime;
// Rigidbody가 있으면 물리 엔진과 충돌!
}
// ✅ Rigidbody 사용
void FixedUpdate()
{
rb.MovePosition(rb.position + Vector3.forward * Time.fixedDeltaTime);
// 또는
rb.velocity = Vector3.forward * speed;
}
}
public class RaycastOptimization : MonoBehaviour
{
// ❌ 매 프레임 Raycast
void Update()
{
if (Physics.Raycast(transform.position, Vector3.down))
{
// ...
}
}
// ✅ 간격을 두고 Raycast
private float lastRaycastTime;
void Update()
{
if (Time.time - lastRaycastTime > 0.1f) // 0.1초마다
{
lastRaycastTime = Time.time;
if (Physics.Raycast(transform.position, Vector3.down))
{
// ...
}
}
}
// ✅ LayerMask 사용
void RaycastWithLayer()
{
int layerMask = LayerMask.GetMask("Ground", "Obstacle");
// Ground와 Obstacle만 체크
if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, 10f, layerMask))
{
// ...
}
}
// ✅ RaycastNonAlloc (GC 없음)
private RaycastHit[] results = new RaycastHit[10];
void RaycastNoGC()
{
int hitCount = Physics.RaycastNonAlloc(
transform.position,
Vector3.down,
results,
10f
);
for (int i = 0; i < hitCount; i++)
{
Debug.Log(results[i].collider.name);
}
}
}
// Texture Import Settings:
// PC:
// - Format: DXT5 (투명도) / DXT1 (불투명)
// - Max Size: 2048 이하
// Mobile:
// - Android: ETC2 / ASTC
// - iOS: ASTC
// ✅ 런타임에서 체크
void CheckTextureMemory()
{
Texture2D texture = GetComponent<Renderer>().material.mainTexture as Texture2D;
int width = texture.width;
int height = texture.height;
TextureFormat format = texture.format;
// 메모리 계산
int bytesPerPixel = GetBytesPerPixel(format);
int totalBytes = width * height * bytesPerPixel;
Debug.Log($"Texture memory: {totalBytes / 1024} KB");
}
int GetBytesPerPixel(TextureFormat format)
{
switch (format)
{
case TextureFormat.RGBA32:
return 4;
case TextureFormat.RGB24:
return 3;
case TextureFormat.DXT5:
return 1; // 압축
default:
return 4;
}
}
// 에셋 번들로 메모리 관리
public class AssetBundleLoader : MonoBehaviour
{
async void Start()
{
// 에셋 번들 로드
AssetBundle bundle = await LoadAssetBundleAsync("enemies");
// 프리팹 로드
GameObject enemyPrefab = bundle.LoadAsset<GameObject>("Enemy");
// 인스턴스 생성
Instantiate(enemyPrefab);
// 더 이상 필요 없으면 언로드
bundle.Unload(false); // false: 생성된 인스턴스는 유지
}
async Task<AssetBundle> LoadAssetBundleAsync(string name)
{
string path = Path.Combine(Application.streamingAssetsPath, name);
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
while (!request.isDone)
{
await Task.Yield();
}
return request.assetBundle;
}
}
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressablesExample : MonoBehaviour
{
async void Start()
{
// 주소로 로드
AsyncOperationHandle<GameObject> handle =
Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Enemy.prefab");
await handle.Task;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
GameObject enemyPrefab = handle.Result;
Instantiate(enemyPrefab);
}
// 메모리 해제
Addressables.Release(handle);
}
// 또는 간단하게
async void LoadSimple()
{
GameObject prefab = await Addressables.LoadAssetAsync<GameObject>("Enemy").Task;
Instantiate(prefab);
}
}
using Unity.Jobs;
using Unity.Collections;
public class JobSystemExample : MonoBehaviour
{
struct AddJob : IJob
{
public NativeArray<float> a;
public NativeArray<float> b;
public NativeArray<float> result;
public void Execute()
{
for (int i = 0; i < a.Length; i++)
{
result[i] = a[i] + b[i];
}
}
}
void Start()
{
// Native 배열 (GC 없음)
NativeArray<float> a = new NativeArray<float>(1000, Allocator.TempJob);
NativeArray<float> b = new NativeArray<float>(1000, Allocator.TempJob);
NativeArray<float> result = new NativeArray<float>(1000, Allocator.TempJob);
// 초기화
for (int i = 0; i < 1000; i++)
{
a[i] = i;
b[i] = i * 2;
}
// Job 생성
AddJob job = new AddJob
{
a = a,
b = b,
result = result
};
// Job 스케줄
JobHandle handle = job.Schedule();
// 완료 대기
handle.Complete();
// 결과 확인
for (int i = 0; i < 10; i++)
{
Debug.Log($"result[{i}] = {result[i]}");
}
// 해제
a.Dispose();
b.Dispose();
result.Dispose();
}
}
struct ParallelAddJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float> a;
[ReadOnly] public NativeArray<float> b;
[WriteOnly] public NativeArray<float> result;
public void Execute(int index)
{
result[index] = a[index] + b[index];
}
}
void RunParallel()
{
// ... (배열 준비)
ParallelAddJob job = new ParallelAddJob
{
a = a,
b = b,
result = result
};
// 병렬 실행 (워커 스레드들이 나눠서 처리)
JobHandle handle = job.Schedule(
arrayLength: 1000,
innerloopBatchCount: 64 // 배치 크기
);
handle.Complete();
}
// 성능:
// 싱글스레드: 10ms
// 4코어 병렬: 3ms (3.3배 빠름)
using Unity.Burst;
// Burst: C# → 최적화된 네이티브 코드
[BurstCompile]
struct BurstAddJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float> a;
[ReadOnly] public NativeArray<float> b;
[WriteOnly] public NativeArray<float> result;
public void Execute(int index)
{
result[index] = a[index] + b[index];
}
}
// 성능:
// C# (Mono): 10ms
// C# (IL2CPP): 5ms
// Burst: 0.5ms (20배 빠름!)
[BurstCompile]
struct EnemyAIJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> enemyPositions;
[ReadOnly] public float3 playerPosition;
[WriteOnly] public NativeArray<float3> enemyDirections;
public float speed;
public float deltaTime;
public void Execute(int index)
{
float3 enemyPos = enemyPositions[index];
// 플레이어 방향 계산
float3 direction = math.normalize(playerPosition - enemyPos);
// 이동 벡터
enemyDirections[index] = direction * speed * deltaTime;
}
}
public class EnemyManager : MonoBehaviour
{
private NativeArray<float3> positions;
private NativeArray<float3> directions;
void Update()
{
// Job 생성
EnemyAIJob job = new EnemyAIJob
{
enemyPositions = positions,
playerPosition = player.transform.position,
enemyDirections = directions,
speed = 5f,
deltaTime = Time.deltaTime
};
// 병렬 실행
JobHandle handle = job.Schedule(positions.Length, 64);
handle.Complete();
// 결과 적용
for (int i = 0; i < enemies.Count; i++)
{
enemies[i].transform.position += (Vector3)directions[i];
}
}
}
// Window → Analysis → Profiler
// CPU Usage:
// - Update() 시간
// - Rendering 시간
// - Physics 시간
// - GC.Alloc 크기
// Memory:
// - Texture 메모리
// - Mesh 메모리
// - Audio 메모리
// - GameObject 수
// Rendering:
// - Draw Calls
// - Batches
// - Triangles
// - Vertices
using Unity.Profiling;
public class CustomProfiler : MonoBehaviour
{
// 커스텀 마커
static readonly ProfilerMarker s_MyFunctionMarker = new ProfilerMarker("MyFunction");
void MyFunction()
{
s_MyFunctionMarker.Begin();
// 측정할 코드
ExpensiveOperation();
s_MyFunctionMarker.End();
}
// 또는 using
void MyFunction2()
{
using (s_MyFunctionMarker.Auto())
{
ExpensiveOperation();
}
}
// Deep Profiling (느림, 개발용)
[System.Diagnostics.Conditional("ENABLE_PROFILER")]
void DeepProfile()
{
Profiler.BeginSample("My Operation");
ExpensiveOperation();
Profiler.EndSample();
}
}
// Window → Analysis → Frame Debugger
// 렌더링 순서 분석:
// 1. Draw Call 단계별 확인
// 2. Overdraw 확인
// 3. Batching 확인
// 4. Shader 변경 횟수
public class ResolutionManager : MonoBehaviour
{
void Start()
{
// 해상도 낮추기 (성능 향상)
Screen.SetResolution(1280, 720, true);
// 또는 비율로
float scale = 0.75f; // 75%
int width = (int)(Screen.width * scale);
int height = (int)(Screen.height * scale);
Screen.SetResolution(width, height, true);
}
}
void Start()
{
// 모바일: 30fps로 제한 (배터리 절약)
Application.targetFrameRate = 30;
// 고사양 기기: 60fps
if (SystemInfo.processorCount >= 8)
{
Application.targetFrameRate = 60;
}
}
void Start()
{
// Edit → Project Settings → Quality
// 기기별 설정
if (Application.isMobilePlatform)
{
QualitySettings.SetQualityLevel(0); // Low
QualitySettings.shadows = ShadowQuality.Disable;
QualitySettings.antiAliasing = 0;
}
else
{
QualitySettings.SetQualityLevel(3); // High
}
}
Unity 최적화의 핵심 원칙: