Chương 4: Luồng khởi động (Boot Flow)
Trải nghiệm người chơi mobile đòi hỏi game phải mở lên nhanh chóng và không bao giờ bị treo cứng ở màn hình loading, bất kể mạng chập chờn, mất kết nối hay một SDK nào đó bị sập server. Luồng khởi động của TapBrain Template được thiết kế để vượt qua mọi tình huống ngoại lệ nhờ cơ chế Time-boxed Init Pipeline.
1. Tổng quan luồng khởi động từ BeforeSceneLoad đến Home
flowchart TD
START(["Người chơi mở app"]) --> B1
subgraph P1["Giai đoạn 1 · Bootstrapper (BeforeSceneLoad)"]
B1["Load CoreConfig từ Resources"] --> B2["Tạo GameObject [Core]
(DontDestroyOnLoad)"]
B2 --> B3["Tạo + đăng ký mọi service
vào ServiceLocator"]
B3 --> B4["InitPipeline.Run()
(chạy nền, từng bước có timeout)"]
end
B4 --> L1
subgraph P2["Giai đoạn 2 · Scene Loading + GameEntry (AfterSceneLoad)"]
L1["Loading.unity hiện thanh tiến trình"] --> G1["GameEntry.Init():
GameWallet, LevelProgress,
SetGrantHandler, RC defaults"]
end
G1 --> S1
subgraph P3["Giai đoạn 3 · 10 bước InitPipeline (tuần tự)"]
S1["ATT → Firebase → Auth → Remote Config"] --> S2["RTDB + BI → Ads (MAX)"]
S2 --> S3["GameAnalytics + AppMetrica → IAP"]
S3 --> S4["Notifications → Background"]
end
S4 --> H1
subgraph P4["Giai đoạn 4 · Vào Home"]
H1{"Pipeline xong VÀ đủ
minLoadingSeconds (1.5s)?"} -->|"Có"| H2["SceneLoader.Load(SceneId.Home)
(fade, 90% → 100%)"]
H2 --> H3(["Home sẵn sàng"])
end
2. Chi tiết 4 giai đoạn khởi động
Giai đoạn 1: Bootstrapper (Trước khi Scene đầu tiên load)
Hàm Bootstrapper.Boot() chạy tại thời điểm BeforeSceneLoad. Nhiệm vụ gồm:
- Đọc cấu hình: Tải
CoreConfig.assettừResources/CoreConfig. Nếu thiếu, tự tạo instance tạm thời và báo lỗi trong Console. - Cấu hình thiết bị: Đặt
Application.targetFrameRate = 60, tắtInput.multiTouchEnabled = false. - Tạo Root hạ tầng: Sinh một GameObject tên là
[Core]mang cờDontDestroyOnLoad, gắn componentCoreHost(dùng để host coroutine cho các class C# thuần). - Khởi tạo và Đăng ký Services: Tạo mới toàn bộ đối tượng dịch vụ và đưa vào
ServiceLocatorngay từ đầu:- Hạ tầng:
CoreConfig,CoreHost,UserIdentity,UIOverlay,SceneLoader,DialogService,AudioService,HapticService. - Dịch vụ dữ liệu & SDK:
AnalyticsService(kèm 3 sink),FirebaseService,RemoteConfigService,RealtimeDbService,BIService,AttService,IAdsService (MaxAdsService),IIapService (IapService),NotificationService,AppUpdateService,ReviewService,SearchAdsService.
- Hạ tầng:
- Khởi chạy InitPipeline: Đưa 10 bước cấu hình vào pipeline và kích hoạt.
Giai đoạn 2: InitPipeline - 10 bước có giới hạn thời gian (Time-boxed)
Mỗi bước trong pipeline đều có một hạn mức thời gian (timeout) riêng biệt. Nếu quá thời gian này hoặc phát sinh lỗi, hệ thống sẽ ghi log cảnh báo, hủy coroutine của bước đó và chuyển ngay sang bước kế tiếp.
| Bước | Tên bước | Timeout | Nhiệm vụ cụ thể | Xử lý khi Timeout / Lỗi |
|---|---|---|---|---|
| 1 | ATT | 120s | (iOS) Hiện popup xin quyền App Tracking Transparency. Bắt buộc chạy trước Ads để SDK MAX có thể nhận IDFA. | Bỏ qua nếu đã quyết định hoặc không phải iOS. |
| 2 | Firebase | firebaseTimeout(Mặc định: 8s) |
Chạy FirebaseApp.CheckAndFixDependenciesAsync(), kích hoạt FirebaseAnalyticsSink và đặt user ID cho Crashlytics. |
Ghi log lỗi. Cờ IsAvailable = false, các dịch vụ Firebase khác tự động trở thành no-op (không chạy). |
| 3 | Auth | 5s | Đăng nhập ẩn danh qua FirebaseAuth.SignInAnonymouslyAsync() để Realtime Database có định danh auth.uid. |
Ghi log cảnh báo. Game vẫn tiếp tục, RTDB có thể bị chặn ghi nếu rule yêu cầu auth. |
| 4 | RemoteConfig | remoteConfigTimeout(Mặc định: 4s) |
Nạp danh sách defaults từ CoreConfig và GameEntry, gọi FetchAndActivateAsync(). |
Sử dụng giá trị mặc định local (offline defaults). Nếu fetch xong sau đó, event Updated sẽ kích hoạt. |
| 5 | RealtimeDb+BI | 1s | Khởi tạo đường dẫn RTDB tại /<realtimeDbRoot>/<release|debug> và đẩy telemetry User Profile qua BIService. |
Thao tác đồng bộ, nếu lỗi bỏ qua. |
| 6 | Ads | adsInitTimeout(Mặc định: 6s) |
Khởi tạo AppLovin MAX SDK bằng MaxSdk.InitializeSdk(), tải trước Interstitial, Rewarded, App Open, và hiện Banner (nếu bật). |
Nếu MAX init lâu hơn 6s, game vẫn tiếp tục. MAX sẽ hoàn tất khởi động trong background sau đó. |
| 7 | GA+AppMetrica | 1s | Kích hoạt GameAnalyticsSink (dùng settings trong Resources) và AppMetricaSink (dùng API key từ CoreConfig). |
Bỏ qua sink nào bị lỗi hoặc bị tắt trong CoreConfig. |
| 8 | IAP | 8s | Khởi tạo Unity Gaming Services, kết nối StoreController và nạp danh sách sản phẩm từ IAPProductCatalog.json. |
Cờ IsReady = false. Nút mua hàng sẽ tạm thời không bấm được cho đến khi Store kết nối thành công. |
| 9 | Notifications | 30s | Tạo Notification Channel "default" và gửi yêu cầu cấp quyền thông báo hệ điều hành (Android 13+ / iOS). | Nếu người dùng không bấm hoặc từ chối, tiếp tục vào game. |
| 10 | Background | 1s | Kích hoạt kiểm tra phiên bản mới trên Store (Play In-App Update / iTunes) và lấy Apple Search Ads attribution trong background. | Chạy dạng Fire-and-forget, hoàn toàn không chặn việc load màn hình Home. |
Giai đoạn 3: GameEntry (Composition Root phía Game)
Ngay sau khi scene đầu tiên nạp xong, hàm GameEntry.Init() được kích hoạt thông qua [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]. Vì Core đã boot xong ở BeforeSceneLoad, tại đây bạn hoàn toàn có thể gọi các service của Core:
// File: Assets/_Project/Game/GameEntry.cs
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Init()
{
// Ngăn ngừa khởi tạo 2 lần nếu scene reload
if (ServiceLocator.TryGet(out _)) return;
// 1. Tạo và đăng ký các dịch vụ sống suốt game (Meta)
var wallet = new GameWallet();
ServiceLocator.Register(wallet);
ServiceLocator.Register(new LevelProgress());
// 2. Đăng ký hàm cấp thưởng IAP: Core sẽ gọi hàm này khi có đơn hàng thành công
App.Iap.SetGrantHandler(wallet.GrantProduct);
// 3. Đăng ký giá trị mặc định cho Remote Config của game (khi offline)
App.RemoteConfig.AddDefault(GameKeys.RcWinCoins, "50");
// 4. Tạo Debug panel (chỉ tồn tại trong Editor và bản build Development)
var debug = new GameObject("[DebugPanel]");
Object.DontDestroyOnLoad(debug);
debug.AddComponent();
}
Giai đoạn 4: LoadingScreen chuyển sang Home
Script LoadingScreen.cs trong scene Loading.unity thực hiện một vòng lặp kiểm tra:
- Cập nhật thanh tiến trình giao diện mượt mà từ 0% đến 90% dựa trên
Bootstrapper.Pipeline.Progress. - Chờ cho đến khi cả 2 điều kiện được thỏa mãn:
Bootstrapper.Pipeline.IsDone == true(Tất cả các bước pipeline đã xong hoặc đã timeout).- Thời gian hiển thị đã đạt ít nhất
CoreConfig.minLoadingSeconds(mặc định là 1.5 giây để người chơi kịp nhìn thấy logo thương hiệu và tránh màn hình bị giật).
- Khi thỏa mãn, thanh tiến trình chạy nốt 10% cuối cùng và gọi:
ServiceLocator.Get().Load(SceneId.Home, fade: true);
3. Các tình huống ngoại lệ đặc biệt
Tình huống 1: Người chơi mở game khi MẤT MẠNG (Offline)
Game mobile casual thường xuyên được chơi trên máy bay hoặc nơi sóng yếu. Khi offline:
- Bước Firebase kiểm tra dependencies nhanh chóng nhận diện không có kết nối.
- Bước Auth timeout sau 5s và bỏ qua.
- Bước Remote Config timeout sau 4s, trả về các giá trị offline mặc định trong
CoreConfigvàGameEntry(ví dụ: thưởng thắng level là 50 coins). - Quảng cáo MAX và Store IAP ghi nhận chưa sẵn sàng (
IsReady = false). - Thanh loading vẫn chạy đủ 1.5s và đưa người chơi vào Home để chơi offline bình thường!
Tình huống 2: Lập trình viên bấm Play trực tiếp ở Scene Main
Khi bạn đang sửa gameplay và muốn test ngay, bạn mở scene Main.unity trong Editor và bấm Play. Điều gì xảy ra?
Dù bạn không chạy từ scene Loading, Unity vẫn gọi Bootstrapper.Boot() trước khi scene Main nạp. [Core] vẫn được sinh ra, toàn bộ service vẫn được đăng ký vào ServiceLocator. Tiếp đó GameEntry.Init() vẫn được gọi sau khi Main load xong. Do đó, GameplayController trong scene Main vẫn lấy được GameApp.Wallet và App.Ads mà không bao giờ bị NullReferenceException!
Mở scene Loading.unity, bấm Play và nhìn vào Console. Bạn sẽ thấy các dòng log chuẩn hóa sau xuất hiện:
[Boot] ATT done in 0.00s
[Boot] Firebase done in 0.28s
[Boot] RemoteConfig done in 0.45s
[Boot] Completed, user=... new=True session=1
Hãy thử tắt Wi-Fi máy tính và bấm Play lại để thấy cơ chế timeout của InitPipeline hoạt động như thế nào!