TapBrain Template

Chương 4: Luồng khởi động (Boot Flow)

⚡ Điểm mấu chốt của 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:

  1. Đọc cấu hình: Tải CoreConfig.asset từ Resources/CoreConfig. Nếu thiếu, tự tạo instance tạm thời và báo lỗi trong Console.
  2. Cấu hình thiết bị: Đặt Application.targetFrameRate = 60, tắt Input.multiTouchEnabled = false.
  3. Tạo Root hạ tầng: Sinh một GameObject tên là [Core] mang cờ DontDestroyOnLoad, gắn component CoreHost (dùng để host coroutine cho các class C# thuần).
  4. 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 ServiceLocator ngay 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.
  5. 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:

  1. 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.
  2. 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).
  3. 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 CoreConfig và 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?

💡 Phép màu của BeforeSceneLoad

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!

🎯 Bài tập quan sát log Console

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!