320 lines
19 KiB
Markdown
320 lines
19 KiB
Markdown
# Addressables — Bootstrap & tóm tắt nghiệp vụ
|
||
|
||
Tài liệu này tóm tắt ghi chép **Addressables** (catalog / cache / remote) và các script runtime trong project **`perfect-world-unity`**: `AddressablesCatalogUpdater`, `AddressablesRuntimeUrlRewriter`, `GameContentBootstrap`, cùng chỉnh sửa `AddressableManager`.
|
||
|
||
Tham chiếu thêm: bản ghi chép tổng quan ban đầu (`Addressables-Tong-hop.md` nếu có trong repo hoặc bản copy ngoài project).
|
||
|
||
---
|
||
|
||
## 0. Hai scene — Content trước, Bootstrap sau (khuyến nghị)
|
||
|
||
Tách **tải Addressables / catalog / bulk** khỏi scene game để `Bootstrap.unity` (UI, `CECUIManager`, `AddressableManager`, …) không bị ảnh hưởng.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[GameContentBootstrap.unity index 0] -->|sync OK LoadScene Single| B[Bootstrap.unity]
|
||
B -->|BootstrapSceneController ~1s| C[LoginScene]
|
||
```
|
||
|
||
| Scene | Build index | Chứa gì |
|
||
|-------|-------------|---------|
|
||
| **GameContentBootstrap** | **0** | Chỉ `GameContentBootstrap` (+ UI loading tuỳ chọn) |
|
||
| **Bootstrap** | 1 | `BootstrapSceneController`, `AddressableManager`, `CECUIManager`, … **không** gắn `GameContentBootstrap` |
|
||
| **LoginScene** | … | Load từ `BootstrapSceneController._nextSceneName` như cũ |
|
||
|
||
**Setup một lần trong Editor:** menu **Perfect World → Addressables → Setup Two-Scene Bootstrap**
|
||
|
||
- Tạo `Assets/PerfectWorld/Scene/GameContentBootstrap.unity` (nếu chưa có)
|
||
- Gỡ `GameContentBootstrap` khỏi `Bootstrap.unity`
|
||
- Đưa scene Content lên **index 0** Build Settings
|
||
|
||
**Trên component `GameContentBootstrap` (scene Content):**
|
||
|
||
| Field | Gợi ý |
|
||
|--------|--------|
|
||
| `_loadNextSceneAfterSuccess` | **Bật** |
|
||
| `_nextSceneName` | `Bootstrap` (tên scene trong Build Settings) |
|
||
| `_stayOnSceneWhenSyncFails` | **Bật** — lỗi thì không vào game |
|
||
|
||
Sau khi sync thành công: `GameContentBootstrapSession.IsContentReady = true`, Addressables đã init → scene **Bootstrap** chỉ cần `AddressableManager` / `AUIManager` gọi `AddressablesInitService` (không chờ gate).
|
||
|
||
---
|
||
|
||
### Cấu hình component (`GameContentBootstrap`)
|
||
|
||
### Hai chế độ
|
||
|
||
| Chế độ | `_useServerForVersionInfo` | Bạn làm gì |
|
||
|--------|---------------------------|------------|
|
||
| **Test / không API** | **Tắt** | Chỉ cần hardcode (bảng dưới). Không cần `_versionEndpointUrl`. |
|
||
| **Production** | **Bật** | Điền URL API + (tuỳ chọn) POST body. Server **sau khi kiểm tra** dữ liệu trả JSON có `contentVersion` và (nếu cần) `assetsBaseUrl`. |
|
||
|
||
**Editor:** bật `_skipRemoteCallInEditor` nếu muốn **không gọi mạng** trong Editor; lúc đó luôn dùng hardcode / `_editorFakeContentVersion` như checklist cuối tài liệu.
|
||
|
||
### Các biến cần biết (theo thứ tự ưu tiên đọc)
|
||
|
||
| Biến | Khi nào cần | Điền gì (ngắn gọn) |
|
||
|------|-------------|---------------------|
|
||
| `_useServerForVersionInfo` | Luôn quyết định trước | Test → **tắt**. Live → **bật**. |
|
||
| `_hardcodedContentVersion` | Server tắt hoặc Editor skip | Chuỗi version (vd `1`, `20250514`). Đổi số này = game coi như có content mới → catalog + bulk download. |
|
||
| `_hardcodedAssetsBaseUrl` | Test CDN / không dùng server | **Prefix URL** thư mục remote Addressables trên CDN (vd `https://my-cdn.com/PW/Android/`). Phải khớp cấu trúc upload `ServerData/<Platform>/`. Để trống nếu bundle đã trỏ đúng URL lúc build, không cần đổi host. |
|
||
| `_versionEndpointUrl` | **Chỉ khi** server **bật** | URL đầy đủ một endpoint do backend cung cấp, vd `https://api.game.com/v1/addressables/resolve`. |
|
||
| `_usePostForVersionRequest` | Server bật | **Bật** nếu luồng của bạn là: client gửi JSON lên → server validate → trả link/prefix. **Tắt** = GET (không body). |
|
||
| `_versionPostBody` | POST bật | JSON một dòng hoặc nhiều dòng (vd `{"platform":"Android","token":"..."}`). Để trống → client gửi `{}`. |
|
||
| `_bakedRemoteUrlPrefixForRewrite` | Khi dùng `assetsBaseUrl` (từ server **hoặc** hardcode) | **Đúng** phần đầu URL đã **bake** trong catalog (trùng `Remote.LoadPath` lúc build), vd `https://old-build-cdn.example.com/Android/`. Runtime thay prefix này bằng `assetsBaseUrl`. Để trống nếu **không** rewrite. Phải khớp đầu mọi `InternalId` remote cần đổi host. |
|
||
| `_remoteBulkDownloadLabel` | Gần như luôn | Trùng **label** trên group Addressables remote cần tải full (mặc định `RemoteContent`). |
|
||
| `_holdAddressablesInitUntilVersionChecked` | Khuyến nghị | Giữ **bật** để Addressables init sau khi có version + rewrite. |
|
||
| `_skipRemoteCallInEditor` / `_editorFakeContentVersion` | Editor | Bật skip + fake version khi không muốn HTTP trong Editor. |
|
||
|
||
|
||
### Quan trọng: Profile Addressables vs field trên `GameContentBootstrap`
|
||
|
||
**URL thật sự tải catalog/bundle** được **ghi cứng vào catalog lúc build** từ profile **Remote.LoadPath** (vd `https://prefect-world-asset.wcscdn51.v1.wcsapi.com`). Runtime **không** đọc profile Editor.
|
||
|
||
| Field bootstrap | Có dùng không? | Ghi chú |
|
||
|-----------------|----------------|---------|
|
||
| `_versionEndpointUrl` | Chỉ khi **`_useServerForVersionInfo` bật** | Test tắt server → field này **bị bỏ qua**. Không phải URL tải bundle. |
|
||
| `_hardcodedAssetsBaseUrl` | Chỉ khi server tắt / Editor skip | Là **URL đích** (`assetsBaseUrl`) cho rewrite, không thay thế profile. |
|
||
| `_bakedRemoteUrlPrefixForRewrite` | Chỉ khi có `assetsBaseUrl` **và** prefix **khớp** URL trong catalog | Phải = **Remote.LoadPath lúc build bản APK đang chạy**, không phải URL LAN/test đích. Sai prefix → rewrite không chạy → vẫn tải từ host trong catalog (profile cũ). |
|
||
|
||
**Ví dụ lỗi thường gặp (LoadScene):** cả `_hardcodedAssetsBaseUrl` và `_bakedRemoteUrlPrefixForRewrite` đều `http://192.168.x.x/...` trong khi catalog build với `https://prefect-world-asset....wcsapi.com` → game **vẫn gọi wcsapi**, không gọi IP local.
|
||
|
||
**Cấu hình test LAN đúng:**
|
||
|
||
1. `_useServerForVersionInfo` = **tắt**
|
||
2. `_hardcodedContentVersion` = version test
|
||
3. `_bakedRemoteUrlPrefixForRewrite` = prefix **trong catalog** (copy từ profile **Remote.LoadPath** lúc build)
|
||
4. `_hardcodedAssetsBaseUrl` = `http://192.168.x.x:8080/Android/` (server LAN của bạn)
|
||
|
||
Log mong đợi: `URL rewrite | from=https://prefect-world-asset... → to=http://192.168...`
|
||
|
||
|
||
### `_versionEndpointUrl` vs `_bakedRemoteUrlPrefixForRewrite` — ví dụ một dòng
|
||
|
||
- **`_versionEndpointUrl`**: không phải link tải từng bundle; là **URL gọi API** (GET hoặc POST) để lấy **metadata**: `contentVersion` + tuỳ chọn `assetsBaseUrl`.
|
||
- **`_bakedRemoteUrlPrefixForRewrite`**: không gọi trực tiếp; là **chuỗi copy từ build** (prefix URL trong catalog). Ví dụ build profile `Remote.LoadPath` = `https://cdn-a.com/Android` → điền y hệt vào field này; server trả `assetsBaseUrl` = `https://cdn-b.com/Android` → mọi bundle path đổi từ `cdn-a` sang `cdn-b`.
|
||
|
||
### Luồng bạn muốn (POST → server kiểm tra → trả “link”)
|
||
|
||
1. Bật `_useServerForVersionInfo`, điền `_versionEndpointUrl`, bật `_usePostForVersionRequest`, điền `_versionPostBody` (vd token, platform, build channel).
|
||
2. Client **POST** `Content-Type: application/json` tới URL đó.
|
||
3. Server validate; nếu OK trả **cùng format** như trước (để `JsonUtility` parse được):
|
||
|
||
```json
|
||
{
|
||
"contentVersion": "12",
|
||
"assetsBaseUrl": "https://cdn.example.com/PW/Android/"
|
||
}
|
||
```
|
||
|
||
`assetsBaseUrl` ở đây là **base / prefix** cho remote Addressables (nơi có `catalog_*.json` và bundle), không nhất thiết là một URL file đơn lẻ — Addressables vẫn dùng catalog + download theo label như cũ.
|
||
|
||
### Option test: luôn dùng link hardcode để download
|
||
|
||
1. Tắt `_useServerForVersionInfo`.
|
||
2. Điền `_hardcodedContentVersion` (vd `test1`).
|
||
3. Nếu cần trỏ CDN test khác với URL đã bake trong catalog: điền `_hardcodedAssetsBaseUrl` **và** `_bakedRemoteUrlPrefixForRewrite` như bảng trên.
|
||
4. Editor: có thể bật `_skipRemoteCallInEditor` để khỏi gọi server khi test trong Editor.
|
||
|
||
---
|
||
|
||
## 1. Khái niệm nhanh (catalog / bundle / cache)
|
||
|
||
| Thành phần | Vai trò |
|
||
|------------|---------|
|
||
| **Catalog** (`catalog_*.json` + `.hash`) | Danh mục: address → bundle, URL, hash/CRC… Catalog mới = tín hiệu “có nội dung mới”. |
|
||
| **AssetBundle** | Chứa dữ liệu asset thực tế. |
|
||
| **Nhóm Local / Remote** | Local: kèm build / `StreamingAssets`. Remote: tải qua HTTP(S). |
|
||
| **Player / Editor** | Bundle **theo platform** — không dùng bundle Windows cho Android. |
|
||
|
||
**Cập nhật trên máy người chơi**
|
||
|
||
- Catalog trỏ tới bundle/hash/CRC **khác** lần trước **và** luồng runtime thực hiện **tải catalog mới** / **cập nhật catalog** / **load bundle** thành công.
|
||
- **Không** tự cập nhật chỉ vì file trên server đổi nếu game **không** tải catalog mới hoặc không load đúng key.
|
||
- **`Release`** (RAM): asset có thể hết trong bộ nhớ; lần load sau có thể **đọc lại từ bundle đã cache trên đĩa** — **không** đồng nghĩa “tải lại toàn bộ từ CDN”.
|
||
|
||
**Cache bundle**
|
||
|
||
- Nếu bundle đã cache **hợp lệ** theo catalog (CRC/hash đúng cấu hình), hệ thống **ưu tiên dùng bản local**, không tải lại cả file từ server.
|
||
|
||
**Runtime đổi CDN / base URL**
|
||
|
||
- `Remote.LoadPath` trong profile là **lúc build** (URL ghi vào catalog).
|
||
- Đổi host/path **lúc chạy**: dùng **`Addressables.InternalIdTransformFunc`** (rewrite `InternalId`) — **trước** `InitializeAsync` / load remote nhiều nhất có thể.
|
||
|
||
**HTTP / HTTPS**
|
||
|
||
- Player Settings: **Allow downloads over HTTP** nếu dùng `http://` (thường chỉ dev).
|
||
- Production: **HTTPS**; Android/iOS có thể chặn cleartext.
|
||
|
||
---
|
||
|
||
## 2. Script trong project
|
||
|
||
### 2.1. `AddressablesCatalogUpdater.cs`
|
||
|
||
Bọc API Addressables (package ~2.7.x):
|
||
|
||
- `EnsureInitializedAsync`
|
||
- `CheckForCatalogUpdatesAsync` / `UpdateCatalogsAsync` / `CheckAndUpdateCatalogsIfNeededAsync`
|
||
- `GetDownloadSizeBytesAsync`
|
||
- `DownloadDependenciesAsync`
|
||
- `CleanBundleCacheAsync`
|
||
|
||
### 2.2. `AddressablesRuntimeUrlRewriter.cs`
|
||
|
||
- `InstallPrefixRewrite(fromPrefix, toPrefix)` — thay prefix URL trong `InternalId` (CDN mới).
|
||
- `ClearInstalledRewrite`
|
||
- `ChainPreviousTransform` — gọi transform cũ trước (nếu có).
|
||
|
||
### 2.3. `GameContentVersionServerClient.cs`
|
||
|
||
- Struct **`GameContentVersionFetchResult`** (`Ok`, `ContentVersion`, `AssetsBaseUrl`, `Error`).
|
||
- **`GameContentVersionServerClient.FetchAsync(url, timeoutSeconds, usePost, postJsonBody)`** — GET **hoặc** POST JSON (`Content-Type: application/json`), parse JSON phẳng (`JsonUtility`).
|
||
|
||
### 2.4. `GameContentBootstrap.cs` (scene bootstrap)
|
||
|
||
**Nguồn phiên bản / URL**
|
||
|
||
- **`_useServerForVersionInfo`**: tắt = không gọi server, dùng **`_hardcodedContentVersion`** / **`_hardcodedAssetsBaseUrl`** (test, API chưa chốt).
|
||
- Bật = gọi **`GameContentVersionServerClient.FetchAsync`** tới **`_versionEndpointUrl`** (GET hoặc POST theo **`_usePostForVersionRequest`** + **`_versionPostBody`**) (trừ khi Editor bật skip bên dưới).
|
||
- **`_skipRemoteCallInEditor`**: Editor không HTTP; ưu tiên hardcode nếu có version, không thì **`_editorFakeContentVersion`**.
|
||
- Bật server nhưng URL trống → fallback hardcode (cảnh báo log).
|
||
|
||
**Luồng**
|
||
|
||
1. Resolve version (server hoặc hardcode theo trên).
|
||
2. Nếu có `assetsBaseUrl` **và** **`_bakedRemoteUrlPrefixForRewrite`** → `InstallPrefixRewrite`.
|
||
3. **Mở gate** → cho phép `AddressableManager` gọi `Addressables.InitializeAsync`.
|
||
4. `EnsureInitializedAsync`, so sánh version với `PlayerPrefs`:
|
||
- **Lần đầu** hoặc **`contentVersion` khác** bản đã lưu → `CheckAndUpdateCatalogsIfNeededAsync` + `DownloadDependenciesAsync` theo **`_remoteBulkDownloadLabel`**.
|
||
- **Trùng version** (và không cần làm content work) → **không** catalog/bulk download.
|
||
|
||
**PlayerPrefs**
|
||
|
||
- `PW_GameContent_FirstRemoteSyncDone`
|
||
- `PW_GameContent_LastContentVersion`
|
||
|
||
**Event / API**
|
||
|
||
- `Finished` (`BootstrapResult`: `Success`, `DidContentWork`, `ErrorMessage`, `ServerContentVersion`).
|
||
- `RunInternalAsync()` — gọi từ code khác nếu cần.
|
||
|
||
**JSON mẫu server (phẳng, đúng `JsonUtility`)**
|
||
|
||
```json
|
||
{
|
||
"contentVersion": "12",
|
||
"assetsBaseUrl": "https://cdn.example.com/Android"
|
||
}
|
||
```
|
||
|
||
### 2.5. `AddressableManager.cs`
|
||
|
||
- Trước `Addressables.InitializeAsync()` **await** `GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync()` để không init Addressables trước khi bootstrap xong HTTP + rewrite (khi gate được tạo).
|
||
|
||
---
|
||
|
||
## 3. Bảng liên hệ “MD nghiệp vụ ↔ code”
|
||
|
||
| Nghiệp vụ | Code |
|
||
|-----------|------|
|
||
| Kiểm tra / cập nhật catalog | `AddressablesCatalogUpdater` + gọi trong `GameContentBootstrap` |
|
||
| Đổi CDN runtime | `AddressablesRuntimeUrlRewriter` + `assetsBaseUrl` từ API |
|
||
| First run / mỗi lần mở + so version (server hoặc hardcode) | `GameContentBootstrap` + `GameContentVersionServerClient` + `PlayerPrefs` |
|
||
| Init Addressables sau rewrite | Gate + `AddressablesInitService` + `AddressableManager` chờ gate |
|
||
| Init tập trung (tránh init sớm) | `AddressablesInitService` — **không** gọi `Addressables.InitializeAsync()` trực tiếp; `AUIManager` đã chuyển sang service |
|
||
|
||
---
|
||
|
||
## 4. Checklist cấu hình (team)
|
||
|
||
- [ ] Scene bootstrap có **`GameContentBootstrap`**: tắt **`_useServerForVersionInfo`** + gán hardcode khi test; production bật server + **`_versionEndpointUrl`**.
|
||
- [ ] **`_bakedRemoteUrlPrefixForRewrite`** trùng prefix URL **đã bake trong bản build đang chạy** (không chỉ `Remote.LoadPath` hiện tại trong Editor). Ví dụ build cũ trỏ `https://prefect-world-asset....wcsapi.com/` → phải điền đúng prefix đó; **`_hardcodedAssetsBaseUrl`** / server `assetsBaseUrl` = CDN mới (vd `https://pw-assets.brewmonster.vn/Android/`).
|
||
- [ ] Mọi entry remote cần “tải full” lần đầu / khi đổi version đều có **cùng label** với **`_remoteBulkDownloadLabel`** (mặc định `RemoteContent`).
|
||
- [ ] Build Addressables đúng platform → upload đúng **`ServerData/<Platform>`** lên host (nếu dùng remote).
|
||
- [ ] Có luồng **catalog** khi hot-update (bootstrap đã gọi khi version đổi / first run).
|
||
- [ ] Production dùng **HTTPS**; mọi chỗ load Addressables dùng **`AddressablesInitService`** (hoặc `AddressableManager.WaitUntilInitializedAsync`), không `InitializeAsync().WaitForCompletion()` trực tiếp.
|
||
- [ ] **Hai scene:** index 0 = `GameContentBootstrap`, **không** gắn `GameContentBootstrap` trên `Bootstrap.unity` (chạy menu Setup Two-Scene Bootstrap).
|
||
- [ ] `_nextSceneName` = `Bootstrap`; `BootstrapSceneController._nextSceneName` vẫn trỏ `LoginScene` (hoặc scene in-game) như cũ.
|
||
|
||
---
|
||
|
||
## 5. Mobile: `SSL CA certificate error` / catalog URL CDN cũ
|
||
|
||
**Triệu chứng:** `CheckForCatalogUpdates` / tải `catalog_*.hash` lỗi `ConnectionError : SSL CA certificate error`, URL vẫn là host cũ (vd `prefect-world-asset....wcsapi.com`) dù profile Editor đã đổi sang CDN khác.
|
||
|
||
**Nguyên nhân thường gặp:**
|
||
|
||
1. **Init sớm:** UI (`AUIManager` / `CECUIManager.Awake`) gọi `Addressables.InitializeAsync()` **trước** `GameContentBootstrap` gắn `InternalIdTransformFunc` → request vẫn tới CDN bake trong catalog build.
|
||
2. **Thiếu rewrite:** `assetsBaseUrl` có nhưng `_bakedRemoteUrlPrefixForRewrite` trống → không đổi host.
|
||
3. **Chứng chỉ CDN:** Chuỗi SSL/intermediate trên host đích không tin cậy được trên Android (sửa phía CDN / Let's Encrypt full chain).
|
||
|
||
**Đã xử lý trong code:** `AddressablesInitService` chờ gate bootstrap; `AUIManager` dùng service thay vì init trực tiếp.
|
||
|
||
**Bạn cần kiểm tra trên scene mobile:**
|
||
|
||
| Field | Gợi ý |
|
||
|--------|--------|
|
||
| `_bakedRemoteUrlPrefixForRewrite` | Prefix y hệt URL trong catalog bản APK (vd `https://prefect-world-asset.wcscdn51.v1.wcsapi.com/`) |
|
||
| `_hardcodedAssetsBaseUrl` | CDN đích HTTPS hợp lệ (vd `https://pw-assets.brewmonster.vn/Android/`) |
|
||
|
||
Log mong đợi trước init: `[Cuong] GameContentBootstrap: URL rewrite | from=... → to=...`
|
||
|
||
Các file khác vẫn có thể gọi `InitializeAsync().WaitForCompletion()` (`EC_Game`, `EC_HPWorkNavigate`, …) — nên chuyển dần sang `AddressablesInitService`.
|
||
|
||
---
|
||
|
||
## 6. Ghi chú Editor
|
||
|
||
- Bật **`_skipRemoteCallInEditor`** để không gọi mạng; dùng **`_hardcodedContentVersion`** / **`_hardcodedAssetsBaseUrl`** hoặc **`_editorFakeContentVersion`** (khi hardcode version trống).
|
||
|
||
---
|
||
|
||
## 7. Debug log runtime (`[Cuong]`)
|
||
|
||
Trong Console Unity, lọc `[Cuong]` để theo dõi bootstrap / tải remote.
|
||
|
||
### Tiến độ tải (% + MB)
|
||
|
||
`AddressablesCatalogUpdater.DownloadDependenciesAsync` log **mỗi 5%** (mặc định) và lúc **bắt đầu / kết thúc 100%**, kèm dung lượng khi Addressables báo được `TotalBytes`:
|
||
|
||
```text
|
||
[Cuong] AddressablesCatalogUpdater: Bắt đầu tải dependencies (key=RemoteContent)...
|
||
[Cuong] AddressablesCatalogUpdater: 0% (0.0/512.3 MB) — key=RemoteContent
|
||
[Cuong] AddressablesCatalogUpdater: 5% (25.6/512.3 MB) — key=RemoteContent
|
||
[Cuong] AddressablesCatalogUpdater: 10% (51.2/512.3 MB) — key=RemoteContent
|
||
...
|
||
[Cuong] AddressablesCatalogUpdater: 100% (512.3/512.3 MB) — key=RemoteContent
|
||
[Cuong] AddressablesCatalogUpdater: Tải xong dependencies (key=RemoteContent).
|
||
```
|
||
|
||
Trước khi bulk download, `GameContentBootstrap` gọi `GetDownloadSizeBytesAsync` và log **dung lượng cần tải** (ước lượng theo catalog + cache):
|
||
|
||
```text
|
||
[Cuong] GameContentBootstrap: Đang tải remote content (label=RemoteContent), dung lượng cần tải ~512.3 MB...
|
||
```
|
||
|
||
Nếu đã cache đủ: `dung lượng cần tải ~0 MB` hoặc log “đã có trong cache”.
|
||
|
||
Đổi bước log %: gọi `DownloadDependenciesAsync(key, progressLogStepPercent: 10)` (1–100).
|
||
|
||
### Bảng log theo giai đoạn
|
||
|
||
| Giai đoạn | Ví dụ log |
|
||
|-----------|-----------|
|
||
| Bắt đầu bootstrap | `GameContentBootstrap: Bắt đầu bootstrap Addressables...` |
|
||
| Đang lấy version | `Đang lấy contentVersion / assetsBaseUrl...` |
|
||
| Đang init / catalog | `Đang khởi tạo Addressables...`, `Đang kiểm tra / cập nhật catalog...` |
|
||
| Trước bulk | `dung lượng cần tải ~N MB` (label=...) |
|
||
| Đang tải bulk | `AddressablesCatalogUpdater: N% (x/y MB) — key=...` (mỗi ~5%) |
|
||
| Xong (không cần tải) | `Hoàn tất — không cần tải catalog/bulk (version không đổi).` |
|
||
| Xong (đã tải hết) | `Bootstrap hoàn tất — đã tải xong hết nội dung` |
|
||
| AddressableManager | `Bootstrap gate xong`, `InitializeAsync xong — sẵn sàng load asset` |
|
||
|
||
Lỗi dùng `Debug.LogError` cùng tiền tố `[Cuong]`.
|
||
|
||
---
|
||
|
||
*Tài liệu mô tả hành vi và cấu hình trong repo; chi tiết API theo đúng phiên bản `com.unity.addressables` nên đối chiếu [Unity Manual: Addressables](https://docs.unity3d.com/Packages/com.unity.addressables@latest).*
|