R2CDN — Hướng dẫn cho team
Cloudflare R2 làm primary, Unity CCD làm fallback. Phân phối nội dung Addressables cho iOS + Android, có gate kiểm tra offline trước khi vào màn. Hướng dẫn cho cả senior và junior — đọc xong dùng được ngay.
README ARCHITECTURE BUILD-PROCESS INTEGRATION PORTING-GUIDE TROUBLESHOOTING CHANGELOG
1. Tổng quan
R2CDN là gì? Là package Unity (nằm trong Assets/R2CDN/) thay thế Cloud Content Delivery (CCD) mặc định bằng Cloudflare R2 (rẻ hơn nhiều). Khi R2 lỗi thì tự fallback sang CCD nên người chơi không bị gián đoạn.
| Tính năng | Làm gì |
|---|---|
| R2 primary, CCD fallback | Mỗi bundle tải từ R2 trước. Lỗi → tự thử CCD. |
| Pre-build Verify tab | 15+ check trước khi build APK/IPA: hash khớp, file đủ, link.xml có… |
| Level-entry gate | Trước khi vào màn, đảm bảo bundle đã tải xong. Mất mạng → popup → quay home. |
| Background preload | Mỗi lần vào màn, tải sẵn bundle kế tiếp ngầm. Chi tiết Section 6. |
| Event content API | EventContent SO + helper. Plug-and-play cho event. |
2. Cài đặt (làm 1 lần / máy)
Điều kiện trước
- File
.r2-credentials.jsonở thư mục gốc repo (không phải trongAssets/). Đã gitignore:
Lấy từ team lead — không bao giờ commit.{ "accessKeyId": "…", "secretAccessKey": "…", "endpoint": "https://<acct>.r2.cloudflarestorage.com", "buckets": { "dev": "matchbuddies-dev", "prod": "matchbuddies-prod" }, "publicUrls": { "dev": "https://pub-…r2.dev", "prod": "https://pub-…r2.dev" } } - Cloudflare Dashboard → R2 → bucket → Settings → bật Allow R2.dev subdomain. Không bật → mọi request trả 403.
- Vào
Window → R2CDN → Manager → Settings→ kiểm tra credentials = ✅ Loaded. - File
Assets/R2CDN/Runtime/link.xmlphải tồn tại. Thiếu → IL2CPP strip code R2 → app không tải được.
Auto-Fix Addressables (mỗi platform)
Mở Manager → Build Setup → Auto-Fix All (Re-apply). Tool tự enforce setting:
| Setting | Giá trị | Vì sao |
|---|---|---|
| Build with Player | Off | Build Addressables riêng, không trộn player build |
| Remote BuildPath | ServerData/[BuildTarget] | Output bundle ra ngoài Library/ |
| Remote LoadPath | {R2 base}/[BuildTarget] | Player tải về từ URL R2 |
| Bundle naming | Append Hash to Filename | Đổi nội dung → đổi filename → không cache cũ |
| Provider trên Remote groups | R2FallbackBundleProvider | Có R2 → CCD fallback |
RemoteTexture_* mới.3. Quy trình hằng ngày
4. Manager Tabs
Mở qua Window → R2CDN → Manager. Top bar: tab strip + platform badge + toggle Dev/Prod + reload + nút ? (mở guide này).
| Tab | Dùng để | Khi nào dùng |
|---|---|---|
| Settings | Edit credentials, R2 URLs, env mode (Auto/Dev/Production), timeout. Test connection. | Lần đầu setup, hoặc đổi env build. |
| Addressables | Xem read-only các Addressables groups. | Debug — kiểm tra group nào remote. |
| Build Setup | Auto-Fix All — enforce mọi setting. | Sau khi switch platform / pull branch. |
| Upload | Build + upload Addressables lên R2. | Mỗi lần asset đổi. |
| Verify | 15+ check trước khi build player. | Bắt buộc trước Bước 4. |
| Browse | Liệt kê file trong R2 bucket. | Debug — kiểm tra bundle có thực sự trên server. |
Auto= theoDebug.isDebugBuild(Development Build = ✓ → dev, không tick → prod).Dev= ép dùng dev URL (cho QA build).Production= ép dùng prod URL (cho store build).
5. Kiến trúc & Flow
Build & Upload — chuyện gì xảy ra
BuildPlayerContent() tạo 2 thư mục output. Uploader walk cả 2, diff với manifest (MD5), PUT cái nào đổi lên R2.Lúc người chơi vào màn — chuyện gì xảy ra
Toàn bộ logic này nằm trong R2Gate.EnsureLevelReadyCoroutine. Watchdog: không có progress trong 15s → abort với mã "download_stuck"; tổng 60s → "download_timeout".
R2 → CCD fallback (mỗi bundle)
6. Background Preload — tải ngầm bundle kế tiếp
Đây là cơ chế quan trọng giúp người chơi vào màn instant không phải đợi tải. Chạy ngầm, không hiện UI gì.
Ý tưởng
Khi người chơi đang ở bundle hiện tại (vd L41-L50 builtin trong APK), tự động tải bundle kế tiếp (L51-L60 remote) trong background. Tới lúc họ vượt qua L50 và bấm Play L51, bundle đã có sẵn → vào màn instant.
Cơ chế
| Khi nào trigger | Làm gì |
|---|---|
Mỗi lần MIMGameController.LoadLevel được gọi | AddressablesPreloader.PreloadUpcomingLevels(currentLevel) chạy ngầm |
| Đang preload bundle target X | Trigger lại với cùng X → SKIP (không restart) |
| Đang preload target X, trigger với target Y khác | Start coroutine MỚI cho Y, song song với X |
| Bundle đã preload xong | Marker giữ lại, gọi lại với same target → SKIP |
Đọc log preload
# Vào L41 lần đầu
[AddressablesPreloader] Preload START currentLevel=41 nextBundleStart=50
# tải bundle 51-60 ngầm...
# Vào L42 (cùng bundle, cùng target)
[AddressablesPreloader] Preload skip — already preloading bundle starting at 50
# ... tiếp tục L43..L49 → đều skip
# Vào L51 (bundle đã cache)
[R2Gate] EnsureLevelReady BEGIN displayLevel=51
[R2Gate] STEP 2 size=0 bytes
[R2Gate] All atlases cached — PASS ← instant entry!
7. Kết nối với game
Public API surface
Toàn bộ "mặt tiền" mà game code chạm vào R2CDN. Không có gì khác.
namespace R2CDN.Runtime
{
public interface IRemoteLevelSource
{
// Game implement: với 1 level number, trả về list AssetReference cần load
IEnumerator GetLevelDependenciesAsync(int displayLevel,
Action<List<AssetReference>> onResult);
}
public static class R2Gate
{
public static void Initialize(IRemoteLevelSource source);
// Gate cho normal level (qua IRemoteLevelSource)
public static IEnumerator EnsureLevelReadyCoroutine(int displayLevel,
Action<bool, string> onComplete, Action<float> onProgress = null);
// Gate generic (event, store, daily reward, ...)
public static IEnumerator EnsureRefsReadyCoroutine(List<AssetReference> deps,
Action<bool, string> onComplete, Action<float> onProgress = null);
}
public sealed class RemoteContentOverlay : MonoBehaviour
{
// Popup runtime, không cần prefab
public static void ShowError(string title, string body, string btn, Action onDismiss);
}
}
3 file game-side đụng vào R2CDN
| File | Đụng gì | Số dòng R2 |
|---|---|---|
MatchSquadLevelSource.cs | Adapter — implement IRemoteLevelSource | Cả file (~110 dòng) |
AddressablesPreloader.cs | R2Gate.Initialize(...) ở Awake | 1 dòng |
MIMGameController.cs | GateThenLoadLevel coroutine trong LoadLevel | ~25 dòng |
[R2Gate] debug log do R2 package phát ra nội bộ. Game code không chứa log nào của R2 — sạch sẽ.8. Hệ thống Events (Match Race, Hat Race, …)
Mọi event remote dùng chung 1 pattern: ScriptableObject manifest + helper preloader. Không cần đụng R2Gate per event.
Cách wire-up 1 event mới (3 bước)
Bước 1: tạo EventContent asset trong Project:
Assets → Create → MIM → Events → Event Content
- Set
eventId: lowercase string ổn định, vd"match_race". - Drag mọi addressable asset event cần vào
dependencies: prefab, sprite atlas, texture, ScriptableObject, audio... Mix kiểu nào cũng được.
Bước 2: trong ScreenHome, khi event button show ra (do remote config + level unlock), gọi preload:
[SerializeField] private EventContent matchRaceContent;
private void RefreshMatchRaceButton()
{
bool unlocked = MatchRaceManager.Instance != null && IsRemoteConfigEnabled("match_race");
if (unlocked) matchRaceButton.Show();
else matchRaceButton.Hide(true);
// Fire-and-forget — tự dedupe, gọi nhiều lần OK.
if (unlocked && matchRaceContent != null)
EventContentPreloader.PreloadIfNeeded(matchRaceContent, this);
}
Bước 3: khi player tap event button, gate trước khi navigate:
private IEnumerator EnterMatchRace()
{
bool ok = false; string err = null;
yield return EventContentPreloader.EnsureReady(matchRaceContent,
(s, e) => { ok = s; err = e; });
if (ok) { UIController.ShowPage<ScreenMatchRace>(); yield break; }
R2CDN.Runtime.RemoteContentOverlay.ShowError(
"Connection Required",
"Match Race needs internet to download content. Please connect and try again.",
"OK", onDismiss: null);
}
RemoteEvent_<EventName>, chạy Auto-Fix, build + upload. Verify tab tự bắt cross-bundle leak.9. Khắc phục lỗi thường gặp
Sprite trắng ở level N (N ≥ 51) trên thiết bị
- Log adb: tìm
[R2Gate]. Không thấy log init = IL2CPP đã strip → checkAssets/R2CDN/Runtime/link.xmltồn tại. - Init log thấy
env=productionmà bạn upload lên dev → mismatch. Settings tab → épDev, hoặc upload prod. [R2Gate] STEP 1 FAIL→ catalog reference đến bundle không có trên R2. Re-upload + Verify.
Download stuck (STEP 4 STUCK)
- Tìm URL trong log
WebRequest → GET ….curl -IURL đó. Mong 200. - 403 → bucket Public Access chưa bật.
- 404 → bundle không có trên R2. Manager → Upload, rồi Verify.
- 200 từ máy bạn nhưng device hang → vấn đề mạng device (proxy, IPv6, captive portal).
Verify báo Local hash != R2 hash
Đã build Addressables nhưng chưa upload, hoặc upload sai env. Manager → Upload đúng env, rồi Verify lại.
Bundle download thành công nhưng tile trắng
Cross-bundle dependency. Window → Asset Management → Addressables → Reports → Build Layout. Tìm bundle thiếu hoặc bị split. Move asset về đúng group, rebuild.
10. Tham khảo nhanh
Đường dẫn quan trọng
| Cái gì | Đâu |
|---|---|
| Credentials (gitignored) | <repo>/.r2-credentials.json |
| Runtime config | Assets/R2CDN/Runtime/Resources/R2FallbackConfig.asset |
| IL2CPP link xml | Assets/R2CDN/Runtime/link.xml |
| Built catalog | Library/com.unity.addressables/aa/<Platform>/ |
| Built remote bundles | ServerData/<Platform>/ |
| Upload manifest (gitignored) | <repo>/.r2-upload-manifest-<env>.json |
| Manager window | Window → R2CDN → Manager |
| Guide này | Window → R2CDN → Open Documentation |
Log prefixes — filter logcat
| Prefix | Nguồn | Lệnh filter |
|---|---|---|
[R2Gate] | R2 gate, providers, init | adb logcat -s Unity | grep R2Gate |
[R2Fallback] | R2 → CCD fallback events | … | grep R2Fallback |
[AddressablesPreloader] | Game preload (next bundle) | … | grep AddressablesPreloader |
[EventPreload] | Event content download | … | grep EventPreload |
[R2] | Editor uploader | (Manager Upload tab log) |
README.md ARCHITECTURE.md BUILD-PROCESS.md INTEGRATION.md PORTING-GUIDE.md TROUBLESHOOTING.md CHANGELOG.md
Tài liệu này dùng tiếng Việt cho phần giải thích, giữ nguyên tiếng Anh cho thuật ngữ chuyên ngành (Addressables, bundle, catalog, gate, AssetReference, …) để khi search code/docs Unity vẫn khớp.