Hướng Dẫn Tối Ưu Performance Game Unity Cho Mobile: Từ A Đến Z
Phát triển game mobile với Unity là một thách thức lớn về mặt hiệu năng. Thiết bị mobile có phần cứng hạn chế hơn nhiều so với PC hay console, nhưng người chơi vẫn kỳ vọng trải nghiệm mượt mà ở 60 FPS. Bài viết này sẽ hướng dẫn bạn từng bước tối ưu game Unity cho mobile, từ những kỹ thuật cơ bản đến nâng cao. Nếu bạn muốn xem các dự án game mobile mẫu đã được tối ưu, hãy tham khảo kho source game của LamGame.
1. Hiểu Về Bottleneck: CPU-bound vs GPU-bound
Trước khi bắt đầu tối ưu, bạn cần xác định game của mình đang bị giới hạn bởi CPU hay GPU. Đây là bước quan trọng nhất vì nó quyết định hướng tối ưu của bạn. Sử dụng Unity Profiler (Window → Analysis → Profiler) để phân tích. Nếu CPU frame time cao hơn GPU frame time, game bạn đang CPU-bound. Ngược lại, nếu GPU frame time cao hơn, game đang GPU-bound. Nhiều developer mắc sai lầm khi tối ưu sai phần, ví dụ giảm polygon count trong khi vấn đề thực sự nằm ở garbage collection trên CPU.
Unity Profiler cung cấp nhiều module phân tích khác nhau: CPU Usage, GPU Usage, Memory, Rendering, Physics, và Audio. Hãy chú ý đặc biệt đến các spike bất thường trong biểu đồ - đó thường là dấu hiệu của garbage collection hoặc các operation tốn kém đang chạy trên main thread. Bạn cũng nên test trực tiếp trên thiết bị thật thay vì chỉ dựa vào Editor profiling, vì hiệu năng trong Editor khác biệt rất lớn so với build thực tế.
2. Tối Ưu Draw Calls Và Batching
Draw calls là một trong những nguyên nhân phổ biến nhất gây giảm hiệu năng trên mobile. Mỗi draw call là một lần CPU gửi lệnh render đến GPU, và quá trình này có overhead đáng kể. Trên mobile, bạn nên giữ draw calls dưới 100-200 cho game 2D và dưới 300-500 cho game 3D đơn giản.
Static Batching là kỹ thuật đơn giản nhất - Unity sẽ gộp các mesh tĩnh sử dụng cùng material thành một draw call duy nhất. Chỉ cần đánh dấu các GameObject không di chuyển là Static trong Inspector. Tuy nhiên, static batching tăng memory usage vì Unity phải lưu trữ combined mesh data.
Dynamic Batching tự động gộp các mesh nhỏ (dưới 300 vertices) di chuyển được. Tuy nhiên, dynamic batching có CPU overhead riêng và không phải lúc nào cũng hiệu quả. Trong nhiều trường hợp, GPU Instancing là lựa chọn tốt hơn cho việc render nhiều object giống nhau (như cây cối, đá, đạn). Bật GPU Instancing trong material settings bằng cách check "Enable GPU Instancing".
SRP Batcher trong URP là một cải tiến lớn giúp giảm CPU overhead cho draw calls. SRP Batcher không thực sự giảm số draw calls, nhưng nó tối ưu cách Unity gửi data đến GPU, giúp mỗi draw call nhanh hơn đáng kể. Đảm bảo shader của bạn tương thích với SRP Batcher bằng cách sử dụng CBUFFER blocks đúng cách.
3. Texture Compression Và Atlas
Texture là thành phần chiếm nhiều memory nhất trong game mobile. Sử dụng đúng format compression có thể giảm memory usage từ 4-8 lần mà gần như không ảnh hưởng đến chất lượng hình ảnh. Các format compression được khuyến nghị cho mobile:
- ASTC - Format tốt nhất cho cả Android và iOS hiện đại. Hỗ trợ nhiều block size (4x4 đến 12x12) cho phép cân bằng giữa chất lượng và dung lượng. ASTC 6x6 là lựa chọn cân bằng tốt cho hầu hết texture.
- ETC2 - Format chuẩn cho Android (OpenGL ES 3.0+). Chất lượng tốt cho texture không có alpha phức tạp. Sử dụng ETC2 RGBA cho texture cần alpha channel.
- PVRTC - Format truyền thống cho iOS nhưng đang dần được thay thế bởi ASTC. Yêu cầu texture phải là power-of-two và vuông.
Sử dụng Sprite Atlas để gộp nhiều sprite nhỏ vào một texture lớn, giảm draw calls và tận dụng batching hiệu quả hơn. Trong Unity, tạo Sprite Atlas qua Assets → Create → 2D → Sprite Atlas. Nhóm các sprite thường xuất hiện cùng nhau vào một atlas, ví dụ tất cả UI elements vào một atlas, tất cả character sprites vào atlas khác.
4. Object Pooling
Instantiate và Destroy là hai operation rất tốn kém trên mobile, đặc biệt khi gọi thường xuyên (như tạo đạn, particle, enemy). Object Pooling là pattern tái sử dụng object thay vì tạo mới và hủy liên tục. Unity 2021+ cung cấp built-in ObjectPool class:
using UnityEngine;
using UnityEngine.Pool;
public class BulletPool : MonoBehaviour
{
public GameObject bulletPrefab;
private ObjectPool<GameObject> pool;
void Awake()
{
pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(bulletPrefab),
actionOnGet: obj => obj.SetActive(true),
actionOnRelease: obj => obj.SetActive(false),
actionOnDestroy: obj => Destroy(obj),
defaultCapacity: 20,
maxSize: 100
);
}
public GameObject GetBullet() => pool.Get();
public void ReturnBullet(GameObject bullet) => pool.Release(bullet);
}
Object pooling không chỉ giảm CPU spike từ Instantiate/Destroy mà còn giảm đáng kể garbage collection pressure. Trên mobile, GC pause có thể gây ra frame drop rõ rệt, đặc biệt trên các thiết bị Android cũ với Dalvik runtime.
5. Memory Management Và Garbage Collection
Garbage Collection (GC) là kẻ thù số một của smooth gameplay trên mobile. Mỗi lần GC chạy, game có thể bị freeze từ vài millisecond đến hàng chục millisecond. Để giảm thiểu GC, hãy tuân thủ các nguyên tắc sau:
- Tránh allocate trong Update/FixedUpdate: Không tạo new object, string concatenation, hoặc LINQ query trong các hàm chạy mỗi frame. Cache kết quả và tái sử dụng.
- Sử dụng struct thay vì class cho các data nhỏ, thường xuyên tạo mới như Vector, Color, hoặc custom data types. Struct được allocate trên stack thay vì heap.
- StringBuilder cho string operations: Thay vì
string result = a + b + c;hãy dùngStringBuildervà cache nó. - Tránh boxing: Không truyền value type vào parameter kiểu object. Sử dụng generic collections thay vì ArrayList.
- Cache component references: Gọi
GetComponent<T>()trong Awake/Start và lưu vào biến, không gọi lại mỗi frame.
// ❌ Sai - allocate mỗi frame
void Update()
{
string info = "HP: " + health + " / " + maxHealth;
var enemies = FindObjectsOfType<Enemy>();
foreach (var e in enemies.Where(e => e.IsAlive)) { }
}
// ✅ Đúng - cache và tái sử dụng
private StringBuilder sb = new StringBuilder(64);
private List<Enemy> cachedEnemies = new List<Enemy>();
void Update()
{
sb.Clear();
sb.Append("HP: ").Append(health).Append(" / ").Append(maxHealth);
// Sử dụng cached list thay vì FindObjectsOfType mỗi frame
}
6. Physics Optimization
Physics engine là một nguồn tiêu tốn CPU đáng kể. Trên mobile, hãy áp dụng các tối ưu sau: giảm Fixed Timestep từ 0.02 (50Hz) xuống 0.04 (25Hz) nếu game không yêu cầu physics chính xác cao. Sử dụng Layer Collision Matrix để loại bỏ các collision check không cần thiết giữa các layer. Ưu tiên sử dụng simple collider (Box, Sphere, Capsule) thay vì Mesh Collider. Nếu phải dùng Mesh Collider, bật Convex option. Tắt Rigidbody trên các object không cần physics simulation bằng cách set isKinematic = true hoặc remove Rigidbody component hoàn toàn.
7. UI Optimization Với Canvas
Unity UI (uGUI) có thể gây ra performance issues nghiêm trọng nếu không được tối ưu đúng cách. Mỗi khi một element trong Canvas thay đổi, toàn bộ Canvas phải rebuild mesh - quá trình này gọi là Canvas rebuild và rất tốn CPU. Giải pháp là chia UI thành nhiều Canvas con: một Canvas cho static elements (background, borders), một Canvas cho dynamic elements (health bar, score), và một Canvas cho rarely-changing elements (menu buttons). Như vậy, khi health bar cập nhật, chỉ Canvas chứa nó phải rebuild.
Tắt Raycast Target trên các UI element không cần nhận input (text labels, decorative images). Mỗi element có Raycast Target bật sẽ được check mỗi frame khi có touch input, gây overhead không cần thiết. Sử dụng Canvas Group để ẩn/hiện UI thay vì enable/disable individual elements.
8. Audio Optimization
Audio thường bị bỏ qua trong quá trình tối ưu nhưng có thể chiếm đáng kể memory và CPU. Sử dụng format Vorbis cho music (streaming) và ADPCM cho sound effects ngắn. Set Load Type thành "Streaming" cho file audio dài (nhạc nền) và "Decompress On Load" cho sound effects ngắn thường xuyên phát. Giảm sample rate xuống 22050Hz cho sound effects không yêu cầu chất lượng cao. Giới hạn số audio source phát đồng thời bằng Audio Mixer và priority system.
9. Build Settings Và Platform-Specific
Cấu hình build đúng cách có thể cải thiện đáng kể hiệu năng. Sử dụng IL2CPP thay vì Mono cho production build - IL2CPP compile C# thành C++ rồi native code, cho hiệu năng tốt hơn 20-50%. Bật Managed Stripping Level ở mức High để giảm build size. Trên Android, sử dụng ARM64 architecture và Vulkan graphics API cho thiết bị hiện đại. Trên iOS, bật Metal graphics API và bitcode.
Cuối cùng, hãy luôn test trên thiết bị thật, đặc biệt là các thiết bị low-end phổ biến tại thị trường mục tiêu. Sử dụng Unity Remote hoặc build trực tiếp để profiling. Nếu bạn đang tìm kiếm cơ hội làm việc trong lĩnh vực game mobile, hãy xem các vị trí tuyển dụng game tại LamGame. Chúc bạn tối ưu game thành công!