Under Construction

Prometheus Lab

Portfolio & Resources

Back to Blog
Unity15 min19 views

Unity Addressables

Author

Minh Khoa

Author

Unity Addressables (Từ Cơ Bản Đến Nâng Cao)

1. Giới thiệu chung (Introduction)

Addressable Asset System (hoặc Addressables) là một package do chính Unity phát triển cung cấp một phương pháp dễ dàng để load các asset bằng "địa chỉ" (address). Hệ thống này giải quyết các vấn đề phức tạp của AssetBundles (dependencies) trong khi vẫn mang lại sức mạnh của việc quản lý bộ nhớ, tải từ xa (remote loading) và cập nhật nội dung (content update) một cách linh hoạt.

2. Sự Khác Biệt Cốt Lõi: Addressables, Resources và AssetBundles

Nhiều lập trình viên thích dùng Addressables ngay cả khi game ở định dạng Offline (chỉ dùng Local) với mục đích Lazy Load nhằm chống giật lag (bottleneck). Dưới đây là bức tranh so sánh chi tiết khi sử dụng ở dạng Local:

Trạng thái lưu trữ (Ổ cứng / Disk)

  • Resources: Tất cả asset trong thư mục Resources sẽ bị vón cục thành các file nguyên khối rất lớn định dạng resources.assets (giấu trong gói cài APK/IPA).
  • Addressables (Local): Asset được đóng gói thành nhiều mảnh file AssetBundles gọn gàng và đặt tự động vào một thư mục được quy định (thường là StreamingAssets).

Khi Game vừa khởi động (RAM & Tốc độ Khởi Động)

  • Resources: Ngay lúc game vừa chớp lên, Unity lập tức bắt HĐH phải đọc và nạp toàn bộ cây thư mục (Index/Catalog) của resources.assets và cấu trúc nó vào RAM. Càng nhét nhiều đồ vào Resources, thời gian màn hình đen khi bật game càng lâu. Lượng RAM chứa mục lục này sẽ bị chiếm giữ vĩnh viễn không thể giải phóng.
  • Addressables: Unity chỉ nạp 1 list Map Catalog siêu nhỏ chứa địa chỉ. Game bật lên tức thì dù dự án có 100 hay 100.000 file. Khi chưa tiến hành Load, asset hoàn toàn ngủ yên dưới ổ cứng (Disk) không ăn lẹm 1 byte RAM nào.

Quá trình Load Asset (Chống Bottleneck / Khựng Hình)

  • Resources: Sở hữu lệnh Resources.Load() mang đặc tính Đồng Bộ (Synchronous). Nó sẽ bắt Luồng Chính (Main Thread) đứng hình 100% để đợi nó tìm file > đọc ổ cứng > Giải nén > Nhồi vào RAM. Đây chính là nguyên nhân lớn nhất gây ra Bottleneck (Cổ chai), sụt FPS, giật khựng khung hình khi spawn quái vật, màn chơi.
  • Addressables: Mang bản chất Lazy Load & Bất Đồng Bộ (Asynchronous). Khi bạn gọi lệnh LoadAssetAsync, việc mở luồng đọc I/O từ ổ cứng và giải nén được thao tác ngầm luồng dưới (Worker Thread). FPS game của bạn hoàn toàn mượt mà. Đọc từ Disk xong xuôi thì nó đưa cái Object đó lên RAM rồi sinh ra cho bạn xài qua logic Callback.

Quá trình Unload (Giải phóng bộ nhớ RAM)

  • Resources: Asset sau khi đưa lên RAM chỉ được thu hồi sạch khi sang Scene mới, hoặc dùng lệnh Resources.UnloadUnusedAssets(). Quá trình của hàm lệnh này là quét và đối chiếu định kì lại toàn bộ không gian bộ nhớ của Game, làm main thread khựng 1 cú rất mạnh.
  • Addressables: Quá trình giải phóng RAM được xử lý qua Đếm Tham Chiếu (Reference Counting) đan xen ở quy mô vi mô. Gọi Addressables.Release, hệ thống dọn dẹp file đó khỏi RAM ngay tắp lự, độc lập & không gây khựng hệ thống.

Sự khác biệt với AssetBundles: Addressables được phát triển xoay quanh hệ sinh thái lõi của mô hình AssetBundle. Hãy coi Addressables là cấp độ "Người quản lý (Manager Layer)" cấp cao nhất, lo cho bạn các thủ tục từ việc tạo file Bundles, lưu Catalog, xử lý luồng Dependency chồng chéo, tự giải phóng lúc object bị Destroy... Nếu sử dụng AssetBundles gốc nguyên thủy, bạn phải mất hàng tuần code chay một Framework rất lớn để làm các việc này.

3. Các Khái Niệm Cốt Lõi (Core Concepts)

  1. Address: Chuỗi định danh duy nhất của Asset. Thay vì đường dẫn dài dòng Assets/Models/Characters/Player.prefab, bạn chỉ cần gọi ngắn gọn "PlayerPrefab".
  2. AssetReference: Một dạng biến cho phép bạn tham chiếu trực tiếp đến một Addressable Asset qua Inspector bằng cách kéo thả, thay vì phải gõ chuỗi Address bằng tay (tránh lỗi đánh máy).
  3. Label: Nhãn phân loại. Rất hữu ích khi bạn muốn tải một nhóm asset cùng lúc. Ví dụ: đánh nhãn "Level1" cho tất cả quái vật và âm thanh của Level 1, sau đó gọi 1 code để tải toàn bộ.
  4. Group: Asset sẽ được gộp vào các Group. Lúc build, Settings của Group sẽ quyết định cách Asset được nén thành AssetBundles (Vd: Group A thành bundle A, Group B thành bundle B). Có thể cấu hình Group nằm ở Local hoặc Remote.
  5. Profile: Cho phép thiết lập các đường dẫn (Path) tương ứng với các giai đoạn phát triển. Ví dụ Profile "Dev" trỏ về localhost, Profile "Production" trỏ về CDN thật của game.

4. Cách Thiết Lập Cơ Bản (Getting Started)

  1. Cài đặt qua Package Manager: Windows -> Package Manager -> Addressables -> Install.
  2. Khởi tạo Addressables Settings: Mở Window -> Asset Management -> Addressables -> Groups, nhấn Create Addressables Settings. Unity sẽ tự tao thư mục AddressableAssetsData.
  3. Đánh dấu Asset: Có 2 cách để biến 1 Asset thành Addressables:
    • Tích vào ô tick box Addressable phía trên cùng trong bảng Inspector khi click vào Asset đó.
    • Kéo thả Asset trực tiếp vào cửa sổ Addressables Groups.

5. Sử Dụng Căn Bản (Basic Usage)

Khởi tạo (Initialization)

Tuy Addressables có thể tự động khởi tạo ngầm khi bạn gọi lệnh load đầu tiên, nhưng ở quy mô thực tế nên chủ động khởi tạo trước lúc loading màn hình:

using UnityEngine.AddressableAssets;

void Start() {
    Addressables.InitializeAsync().Completed += handle => {
        Debug.Log("Khởi tạo Addressables thành công!");
    };
}

Tải một Asset (Load Asset)

Load xong asset vào RAM (nhưng chưa sinh ra ngoài Scene):

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressableExample : MonoBehaviour
{
    public string address = "MyCube";

    void Start() {
        // Tải không đồng bộ để k khựng game
        Addressables.LoadAssetAsync<GameObject>(address).Completed += OnLoadDone;
    }

    private void OnLoadDone(AsyncOperationHandle<GameObject> obj) {
        if (obj.Status == AsyncOperationStatus.Succeeded) {
            GameObject loadedPrefab = obj.Result;
            // Tiến hành sinh ra scene
            Instantiate(loadedPrefab);
        } else {
            Debug.LogError("Lỗi khi load asset!");
        }
    }
}

Khởi tạo trực tiếp (Instantiate Asset)

Vừa load vừa tự động tạo ra Scene cùng lúc:

Addressables.InstantiateAsync("MyCube").Completed += (handle) => {
    // Được gọi khi Instantiate xong. handle.Result chính là GameObject đã có trên scene.
    GameObject myCubeInstance = handle.Result;
};

Sử dụng AssetReference (Khuyên Dùng)

Tránh việc gõ sai chuỗi Address:

public AssetReference playerPrefabRef;

void LoadPlayer() {
    playerPrefabRef.InstantiateAsync().Completed += (handle) => {
        Debug.Log("Player spawned thành công thông qua Asset Reference.");
    };
}

6. Quản Lý Bộ Nhớ (Memory Management - CỰC KỲ QUAN TRỌNG)

Addressables dùng cơ chế Reference Counting (Đếm tham chiếu) để giải phóng bộ nhớ. Khi số tham chiếu (ref count) về 0, asset sẽ bị hủy khỏi RAM (unload).

  • Mỗi lần gọi LoadAssetAsync, ref count tăng 1.
  • Mỗi lần gọi InstantiateAsync, ref count tăng 1 VÀ asset gốc của nó cũng tăng 1.

QUY TẮC VÀNG: Khởi tạo bằng cách nào thì Hủy (Release) bằng hàm tương ứng của cách đó.

// Trường hợp 1: Nếu xài LoadAssetAsync
AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("MyUI");
// --> Khi không cần dùng Prefab đó nữa:
Addressables.Release(handle);

// Trường hợp 2: Nếu xài InstantiateAsync
AsyncOperationHandle<GameObject> handleInst = Addressables.InstantiateAsync("MyEnemy");
GameObject enemyInstance = handleInst.Result;
// --> Khi quái chết, TUYỆT ĐỐI KHÔNG DÙNG Destroy(enemyInstance) trực tiếp bằng script cơ bản của Unity.
// --> Cách đúng để Addressable tự giảm count và gom rác:
Addressables.ReleaseInstance(enemyInstance);
// hoặc Addressables.ReleaseInstance(handleInst);

7. Kiến Thức Nâng Cao Bậc Pro (Advanced Concepts)

Load theo Label (Tải Hàng Loạt)

Khi bạn muốn load 1 lúc 10 loại vũ khí có nhãn "Weapon":

// Hàm callback từng phần tử load xong được truyền vào tham số thứ 2
Addressables.LoadAssetsAsync<GameObject>("Weapon", (loadedWeapon) => {
    // Giao diện: Update thanh loading (% progress) ở đây
    Debug.Log("Loaded từng phần: " + loadedWeapon.name);
}).Completed += (handle) => {
    // Kích hoạt khi TOÀN BỘ weapon đã load xong
    IList<GameObject> allWeapons = handle.Result;
};

Play Mode Scripts (Chế độ chơi thử trên Editor)

Trong cửa sổ Addressables Group -> Play Mode Script (thanh công cụ trên cao), có 3 chế độ test:

  1. Use Asset Database (Fast Mode): Chạy nhanh nhất, copy thẳng từ máy không màng tới Addressable config (không tạo Bundle). Dành cho code / logic test.
  2. Simulate Groups (Virtual Mode): Quan trọng nhất. Giả lập việc load bị delay mạng và tuân thủ chặt phân chia bundle, nhưng vẫn chạy nhanh mà không cần build thật. Lý tưởng để check memory leak và luồng (flow) load.
  3. Use Existing Build: Phải tự bấm lệnh Build -> New Build trong nhóm cửa sổ Groups trước. Game chạy y chang thiết bị thật.

Xử lý Update Nội Dung Từ Xa (Remote/CDN - Hệ thống Update Game)

Addressables cho phép bạn đẩy asset lên Host/CDN. Lần sau bật game, game tự Update mà không cần App Store bắt tải APK/AAB mới.

  1. Ở Profile, chỉnh RemoteLoadPath trỏ tới Server của bạn (ví dụ: http://mygame.com/[BuildTarget]).
  2. Ở Group chứa Asset, chỉnh Build Path thành RemoteBuildPathLoad Path thành RemoteLoadPath.
  3. Bật tùy chọn Build Remote Catalog trong Addressable Asset Settings.
  4. Cách Code tiến trình kiểm tra bản Update:
// B1: Kiểm tra xem có bản cập nhật catalog (thông tin phiên bản gốc) mới không
Addressables.CheckForCatalogUpdates().Completed += (checkForUpdateHandle) => {
    if (checkForUpdateHandle.Result.Count > 0) {
        // B2: Cập nhật catalog
        Addressables.UpdateCatalogs(checkForUpdateHandle.Result).Completed += (updateHandle) => {
            // Danh sách các ID cần tải mới. Đem danh sách này đi call hàm Download... ở mục dưới
            var locators = updateHandle.Result;
        };
    }
};

Lấy dung lượng & Màn hình Downloading

Phần code để làm thanh Loading với dung lượng (MB):

public IEnumerator CheckAndDownload(string labelToDownload) {
    // B1: Check dung lượng
    var sizeHandle = Addressables.GetDownloadSizeAsync(labelToDownload);
    yield return sizeHandle;
    
    long totalBytes = sizeHandle.Result;
    Addressables.Release(sizeHandle); // Done task size
    
    if (totalBytes > 0) {
        Debug.Log($"Cần tải xuống: {totalBytes / (1024f * 1024f):F2} MB");
        
        // B2: Tắt auto clear cache, tiến hành Download
        var downloadHandle = Addressables.DownloadDependenciesAsync(labelToDownload, false);
        
        // Vòng lặp lấy % hiển thị lên UI
        while (!downloadHandle.IsDone) {
            float percent = downloadHandle.GetDownloadStatus().Percent;
            Debug.Log($"Đang tải... {percent * 100:F0}%");
            yield return null;
        }
        
        Addressables.Release(downloadHandle);
        Debug.Log("Tải hoàn tất!");
    } else {
        Debug.Log("Đã có sẵn ở local bản mới nhất, vào thẳng game.");
    }
}

8. Best Practices & Giải Phóng Hiệu Năng (Tối Ưu Hoá)

  1. Asset Duplication (Tránh nhân bản Asset tốn dung lượng)

    • Tình huống lãng phí: Prefab A (nằm trong Group 1) và Prefab B (nằm trong Group 2) đều dùng chung Material_C (không được Add vào Addressable). Lúc Build, Unity ngầm copy Material_C thành 2 bản nhét vào 2 Group khác nhau -> Phình to dung lượng.
    • Khắc phục: Click đúp vào Tool Analyze (Window -> Asset Management -> Addressables -> Analyze) -> Chạy Rule "Check Duplicate Bundle Dependencies". Tuân thủ chặt rule này, nếu có trùng tài nguyên chung thì tạo 1 Group_SharedResource để đưa cái Material_C đó vào.
  2. Cách cấu trúc Group (Granularity)

    • Không nên dồn tất cả vào 1 Group siêu to: Khó update từng phần nhỏ, lãng phí bộ nhớ vì vô tình load những thứ không xài ở màn hình hiện tại.
    • Không nên chia mỗi asset 1 Group khác nhau: Quá trình load khởi đầu chậm do Overhead quét từng file từ HĐH.
    • Chiến lược chuẩn: Phân chia theo Vòng Đời (Audio_Group, UI_Global_Group load 1 lần tồn tại suốt đời) hoặc chia theo Cụm Tính Năng/Loại Màn (Level_Forest_Group, IAP_Popups_Group,...).
  3. Cẩn trọng Hàm Đồng Bộ (Synchronous)

    • Từ các bản Unity Addressables 1.17+, Unity hỗ trợ Addressables.LoadAssetAsync().WaitForCompletion(). Nó sẽ block main thread (đứng game) cho đến khi file tải xong.
    • Khuyến nghị: Chỉ dùng cái này trên các file Local siêu nhỏ hoặc thật sự cần nghẽn luồng. Tuyệt đối không xài nếu file nằm trên Remote CDN.
  4. Nút thắt UniTask (Recommended)

    • Bắt sự kiện .Completed bằng delegate đôi khi tạo ra callback hell (code thụt lùi lầm rầm khó nhìn). Hãy tích hợp thư viện UniTask để lập trình nó thành luồng await tuyết tính siêu đẹp mắt:
// Dùng UniTask tiết kiệm hàng tá code
GameObject prefab = await Addressables.LoadAssetAsync<GameObject>("Player");
GameObject target = await Addressables.InstantiateAsync(prefab);

9. Tổng Kết

Addressables là trái tim của việc phát triển Game Unity chuyên nghiệp và hệ thống LiveOps hiện đại. Tóm gọn hệ tư tưởng sử dụng:

  • Dứt bỏ vĩnh viễn hệ thống Resources/.
  • Phân chia tài nguyên vào các Group 1 cách khoa học.
  • Bất kì cái gì tải lên (Load / Instantiate) -> Chắc chắn phải ném đi (Release / ReleaseInstance) khi không dùng nữa!
  • Lợi dụng Remote Catalog & AssetBundle Update để tạo cơ chế tải update OTA (Over-The-Air) không cần phụ thuộc vào bước kiểm duyệt trên Apple App Store/CH Play.

Hy vọng cẩm nang này sẽ trở thành kim chỉ nam vững chắc trong hành trình biến bạn thành vua Unity Addressables! Đừng ngại thử nghiệm ở Play Mode chức năng Simulate Groups để hoàn toàn làm chủ nó nhé.