Chương 1: Tổng quan & Kiến trúc SDK

Nắm vững quy tắc phân lớp của gói UPM com.mgw.sdk, sự khác biệt giữa facade tĩnh App.X và ServiceLocator, mô hình Provider hoán đổi độc lập, và luồng khởi động bất đồng bộ gồm tối đa 11 bước (MGW) hoặc 4 bước (SayKit), mỗi bước có giới hạn thời gian (time-boxed).

1. Mục đích & Phạm vi của SDK

Gói com.mgw.sdk được thiết kế như một thư viện dịch vụ độc lập dạng Unity Package Manager (UPM). Toàn bộ mã nguồn nằm trong các Assembly Definition (asmdef) riêng biệt:

  • TapBrain.Core.asmdef: Chứa toàn bộ hạ tầng cốt lõi, luồng khởi động, facade App.X, đăng ký dịch vụ ServiceLocator, giao diện quản lý dialog, chuyển scene và lưu trữ. Assembly này không tham chiếu SDK của provider (AppLovin, Firebase, SayKit, GameAnalytics, AppMetrica). Nó chỉ phụ thuộc các gói Unity (Purchasing, Services Core, iOS support, TextMeshPro, UGUI) và bốn asmdef Google Play plugins (Google.Play.Common, Core, AppUpdate, Review) mà game phải có trong Assets/GooglePlayPlugins/ với cả hai provider; thiếu chúng thì TapBrain.Core không biên dịch được.
  • TapBrain.Sdk.MGW.asmdef: Triển khai hạ tầng cho nhà phát hành MGW (gồm AppLovin MAX, Firebase, Realtime DB, BI và Mobile Notifications). Biên dịch khi không có cờ TB_SDK_SAYKIT.
  • TapBrain.Sdk.SayKit.asmdef: Triển khai hạ tầng cho nhà phát hành SayGames (gọi trực tiếp API của SayKit). Biên dịch khi có cờ TB_SDK_SAYKIT và gói io.saygames.saykit đã được cài.
  • TapBrain.Sdk.GameAnalytics.asmdef & TapBrain.Sdk.AppMetrica.asmdef: Các module phân tích bổ trợ, chỉ dùng với MGW (không bao giờ dùng chung với SayKit). Mỗi module chỉ biên dịch khi bật cờ tương ứng (TB_USE_GA / TB_USE_APPMETRICA) và gói của nó (com.gameanalytics.sdk / io.appmetrica.analytics) đã được cài.
Quy tắc độc lập kiến trúc

Mã nguồn của game bạn viết đặt tại các thư mục mã nguồn thông thường của dự án. Game tham chiếu đến SDK qua các interface và facade công khai (App.Ads, App.Iap, App.Analytics...). Ngược lại, mã nguồn trong SDK không bao giờ được phép tham chiếu ngược lại mã nguồn của game.

2. Mô hình Facade App.X và ServiceLocator

Trong quá khứ, các lập trình viên thường tạo các lớp Singleton có dạng AdManager.Instance hoặc IapController.Instance rải rác khắp nơi. Điều này gây khó khăn khi cần thay thế giải pháp kỹ thuật, kiểm thử đơn vị hoặc quản lý thứ tự giải phóng bộ nhớ.

MGW SDK sử dụng mô hình Service Locator trung tâm kết hợp với Facade tĩnh không trạng thái:

  • ServiceLocator: Nơi lưu giữ danh sách đối tượng triển khai dịch vụ dưới dạng Dictionary<Type, object>. Theo quy ước, chỉ Bootstrapper và hàm Install của module SDK gọi ServiceLocator.Register<T>(); code game chỉ Get / TryGet. Get<T>() ném InvalidOperationException nếu dịch vụ chưa được đăng ký.
  • App.X (Facade): Lớp tĩnh cung cấp các thuộc tính getter trực tiếp (ví dụ: App.Ads => ServiceLocator.Get<IAdsService>()). App.X không lưu trữ dữ liệu trạng thái nào, nó chỉ chuyển tiếp lời gọi đến instance đang đăng ký trong ServiceLocator.

Bảng danh sách thành viên App.X

Facade Kiểu dữ liệu trả về Vai trò & Chức năng chính
App.Ads IAdsService Hiển thị banner, interstitial, rewarded, app open và kiểm tra trạng thái tải sẵn.
App.Iap IIapService Khởi tạo mua gói, khôi phục đơn hàng, kiểm tra sở hữu và xử lý cấp phát vật phẩm.
App.Analytics IAnalyticsService Theo dõi tiến trình màn chơi, chi tiêu tiền tệ, luồng hướng dẫn và sự kiện tùy chỉnh.
App.RemoteConfig IRemoteConfigService Đọc cấu hình từ xa theo kiểu mạnh (Typed Bind) hoặc getter kèm giá trị fallback ngoại tuyến.
App.Dialogs DialogService Mở, đóng và quản lý ngăn xếp các cửa sổ popup (tải prefab từ Resources/Dialogs/<TênKiểu>).
App.Scenes SceneLoader Chuyển Scene an toàn theo Build Index kèm hiệu ứng làm tối màn hình (Fade Out/Fade In).
App.Overlay UIOverlay Hiển thị lớp chặn tương tác (Blocker), thông báo nổi (Toast) và hiệu ứng mờ dần.
App.Audio AudioService Phát nhạc nền (Music), âm thanh hiệu ứng (SFX), quản lý bật/tắt âm thanh toàn cục.
App.Haptic HapticService Kích hoạt rung phản hồi xúc giác trên thiết bị di động (Selection, Light, Medium, Heavy...).
App.Review IReviewService Kích hoạt cửa sổ yêu cầu đánh giá ứng dụng gốc của hệ điều hành (In-App Review).

Các dịch vụ nền tảng chuyên sâu hoặc ít dùng hằng ngày (như FirebaseService, RealtimeDbService, BIService, NotificationService, AttService, AppUpdateService, SearchAdsService) được định hướng gọi thông qua ServiceLocator.Get<T>() hoặc ServiceLocator.TryGet<T>(out var s).

Dịch vụ chỉ có ở MGW

FirebaseService, RealtimeDbService, BIService và NotificationService nằm trong assembly TapBrain.Sdk.MGW nên không tồn tại khi build SayKit. Code game dùng chúng phải bọc trong #if !TB_SDK_SAYKIT (chỉ gọi TryGet là chưa đủ, vì kiểu dữ liệu còn không biên dịch được). AttService, AppUpdateService, SearchAdsService thuộc Core và có ở cả hai provider.

3. Mô hình Provider (MGW vs SayKit)

Mỗi bản build game của bạn chỉ chạy chính xác một Provider tại một thời điểm:

Đặc điểm so sánh Provider: MGW (Mặc định) Provider: SayKit (SayGames)
Hạ tầng Quảng cáo AppLovin MAX (cần điền SDK Key và các Ad Unit IDs). SayKit SDK (quản lý phân phối và tần suất từ server SayGames).
Hạ tầng Phân tích Firebase Analytics, Crashlytics, GameAnalytics, AppMetrica. SayKit Tracking API (SayGames BI trực tiếp).
Cấu hình từ xa Firebase Remote Config (Fetch & Activate). SayKit GameConfig (đồng bộ qua server SayGames).
Module tùy chọn Hỗ trợ bật thêm GameAnalytics (TB_USE_GA) & AppMetrica (TB_USE_APPMETRICA). Nghiêm cấm: GameAnalytics và AppMetrica không bao giờ được biên dịch cùng SayKit.
Xử lý ATT / GDPR SDK Core tự yêu cầu ATT trên iOS thiết bị thật. SayKit tự chịu trách nhiệm hiển thị popup đồng thuận GDPR / ATT.
Cách chuyển đổi Dùng menu Editor TapBrain > SDK > Switch To SayKit... / Switch To MGW... (không sửa defines thủ công). Menu hiện hộp thoại xác nhận, cất stack cũ sang một thư mục lưu trữ nằm ngoài Assets/ (không xóa, chuyển lại sẽ khôi phục) rồi biên dịch lại; lần đầu chuyển sang SayKit cần cài sẵn gói io.saygames.saykit bằng SayKit Version Manager.

4. Luồng khởi động (Boot Pipeline)

Quá trình khởi tạo SDK diễn ra hoàn toàn tự động ngay trước khi Scene đầu tiên được tải nhờ thuộc tính [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] trong Bootstrapper.cs:

  1. Tải file Resources/CoreConfig.asset. Nếu thiếu, SDK ghi một dòng lỗi [Boot] Missing Resources/CoreConfig.asset, using defaults... (Debug.LogError, luôn hiện) và dùng một cấu hình mặc định trong bộ nhớ để không gián đoạn game.
  2. Tạo GameObject vĩnh cửu [Core] mang component CoreHost và đánh dấu DontDestroyOnLoad.
  3. Khởi tạo và đăng ký các dịch vụ cơ bản vào ServiceLocator.
  4. Cài đặt Provider và các Extension đã đăng ký.
  5. Đăng ký dịch vụ dự phòng (NullAdsService, DefaultsRemoteConfigService) nếu sau bước 4 chưa có ai đăng ký IAdsService / IRemoteConfigService (không có provider, hoặc Install của provider bị lỗi).
  6. Khởi chạy InitPipeline gồm các bước tuần tự được giới hạn thời gian chạy. Bước 1-5 chạy đồng bộ ngay trong Boot(), nên App.X đã dùng được ngay khi Scene đầu tiên tải; các bước của pipeline chạy tiếp ở nền qua coroutine.

Bảng các bước chạy của InitPipeline

Bảng dưới là thứ tự đầy đủ của provider MGW khi bật cả hai module phân tích. Số thứ tự chỉ để tham chiếu: bước nào không tồn tại (module không biên dịch) thì các bước sau dồn lên. Với SayKit pipeline chỉ có 4 bước: SayKit (sayKitInitTimeout) → RemoteConfig (1s) → IAP → Background; bước ATT bị bỏ qua vì SayKit tự xin đồng thuận. Các bước tắt bằng cờ enableX trong CoreConfig vẫn xuất hiện trong log và kết thúc ngay.

Bước (#) Tên bước Chủ sở hữu Thời gian tối đa (Timeout) Hành vi khi lỗi hoặc quá giờ (Timeout)
1 ATT Core 120s Chỉ hiện hộp thoại trên thiết bị iOS thật (Editor/Android kết thúc ngay). Nếu SayKit chạy, bước này không được thêm vào pipeline vì SayKit tự quản lý consent.
2 Firebase (hoặc SayKit) MGW / SayKit 8s (firebaseTimeout) / 10s (sayKitInitTimeout) MGW: kiểm tra thư viện phụ thuộc, tạo FirebaseApp.DefaultInstance, bật Analytics + Crashlytics; bỏ qua nếu enableFirebase tắt. Thiếu hoặc sai file config: log lỗi [Firebase] App creation failed, Firebase disabled for this session..., các bước Auth / Remote Config (dùng giá trị mặc định) / RTDB tự bỏ qua. SayKit: gọi SayKit.init rồi đợi SayKit.isInitialized; quá giờ thì SayKit vẫn tiếp tục khởi tạo ở nền.
3 Auth MGW 5s Đăng nhập ẩn danh Firebase Auth (Anonymous Sign-In) để lấy UID cho Realtime Database. Bỏ qua nếu enableFirebase hoặc enableAuth tắt. Lỗi đăng nhập chỉ ghi cảnh báo, không chặn game.
4 RemoteConfig MGW / SayKit 4s (MGW, remoteConfigTimeout) / 1s cố định (SayKit) Tải cấu hình từ xa và kích hoạt. Quá giờ thì game tiếp tục với giá trị mặc định đã đăng ký; lần fetch vẫn có thể hoàn tất ở nền và khi đó Updated được bắn.
5 RealtimeDb+BI MGW 1s (Đồng bộ) Mở nhánh Realtime Database (cần enableRealtimeDb và Firebase khả dụng) và, nếu enableBI bật, đẩy hồ sơ thiết bị người chơi.
6 Ads MGW 6s Khởi tạo AppLovin MAX SDK (bỏ qua nếu enableAds tắt). Nếu chưa cấu hình SDK Key, hệ thống ghi cảnh báo [Ads] MAX SDK key not set in CoreConfig, ads disabled và tiếp tục.
7 - 8 GameAnalytics / AppMetrica Extension (MGW) 1s (Đồng bộ) Mỗi module là một bước riêng, chỉ tồn tại khi module được biên dịch (cờ TB_USE_* + gói đã cài). Kích hoạt sink khi enableGameAnalytics / enableAppMetrica bật; AppMetrica còn cần appMetricaApiKey hợp lệ, nếu không ghi cảnh báo [AppMetrica] API key not set in CoreConfig, AppMetrica disabled.
9 IAP Core 8s Kết nối Unity IAP và tải danh mục sản phẩm từ IAPProductCatalog.json (bỏ qua nếu enableIap tắt). Quá giờ thì IsReady vẫn có thể chuyển thành true muộn hơn và sự kiện Ready được bắn lúc đó.
10 Notifications MGW (Late step) 30s Tạo kênh thông báo Android / xin quyền thông báo (bỏ qua nếu enableNotifications tắt). Chạy sau IAP để hộp thoại xin quyền không làm chậm kết nối cửa hàng.
11 Background Core 1s (Đồng bộ) Khởi chạy ngầm (không chờ kết quả) việc kiểm tra cập nhật ứng dụng (enableAppUpdateCheck) và lấy thông tin phân bổ Apple Search Ads (enableSearchAdsAttribution).
Nguyên tắc: Luôn vào được màn hình chính

Mọi bước trong InitPipeline đều được bọc trong hàm bảo vệ (Guard). Nếu một bước ném ngoại lệ, SDK ghi lỗi [Boot] <step> threw: ... và đi tiếp; nếu một bước vượt quá timeout, SDK ghi cảnh báo [Boot] <step> timed out after Xs, continuing, dừng coroutine của bước đó rồi chuyển sang bước kế tiếp. Nhờ vậy một dịch vụ chậm hoặc hỏng không làm treo màn hình khởi động. Lưu ý: bước quá giờ chỉ bị bỏ chờ, công việc bất đồng bộ bên trong SDK bên thứ ba (fetch Remote Config, kết nối IAP...) vẫn có thể hoàn tất muộn, vì vậy hãy dùng cờ sẵn sàng ở mục 5. Nếu chính Bootstrapper.Boot() bị lỗi trước khi tạo pipeline, LoadingScreen ghi lỗi [Boot] Bootstrapper did not create the init pipeline... và dừng ở màn hình Loading thay vì vào game.

5. Kiểm tra tính sẵn sàng của Dịch vụ (Service Readiness)

Một sai lầm rất phổ biến của các lập trình viên mới là giả định rằng: "Hễ Scene đã load xong thì mọi dịch vụ SDK đều đã sẵn sàng". Điều này hoàn toàn sai:

  • Khi bạn nhấn nút Play trực tiếp tại Scene màn chơi trong Unity Editor để test nhanh, màn hình LoadingScreen bị bỏ qua hoàn toàn. Lúc này các bước mạng có thể vẫn đang chạy ngầm.
  • Khi người chơi mở game lúc mạng chậm hoặc không có mạng, bước Remote Config hoặc IAP có thể bị timeout; công việc bất đồng bộ bên trong vẫn có thể hoàn tất muộn hơn khi có mạng trở lại và khi đó sự kiện Updated / Ready mới được bắn.

Vì vậy, trước khi thao tác với các dịch vụ mạng, bạn phải luôn kiểm tra cờ sẵn sàng hoặc đăng ký lắng nghe sự kiện:

Dịch vụ Cờ kiểm tra trạng thái Sự kiện kích hoạt khi sẵn sàng Hành vi khi chưa sẵn sàng
App.Ads IsInitialized, IsInterstitialReady, IsRewardedReady Initialized Không hiện quảng cáo: TryShowInterstitial trả về false và vẫn gọi onClosed đúng 1 lần; ShowRewarded gọi onResult đúng 1 lần (không phải Rewarded, ví dụ NotReady).
App.Iap IsReady Ready Gọi mua hàng trả về kết quả PurchaseResult.NotReady.
App.RemoteConfig IsFetched Updated Đọc dữ liệu vẫn dùng được và trả về giá trị mặc định đã đăng ký. Updated cũng được bắn khi lần fetch kết thúc thất bại (lúc đó IsFetched vẫn false).

6. Ví dụ Code hoàn chỉnh

Dưới đây là một ví dụ mẫu thể hiện cách khởi tạo một dịch vụ game, đăng ký kiểm tra trạng thái SDK và gọi các facade an toàn:

using UnityEngine;
using TapBrain.Core;

namespace MyGame
{
    // Shows shop data from SDK services without assuming boot has finished.
    public sealed class ShopStatusView : MonoBehaviour
    {
        private void Start()
        {
            // Remote Config is readable at any time: before the fetch it returns the registered defaults.
            ApplyRemoteValues();
            // Updated fires when the fetch finishes (also later than the loading screen), so keep listening.
            App.RemoteConfig.Updated += OnRemoteConfigUpdated;

            // IAP may still be connecting: read prices now only if it is ready, otherwise wait for Ready.
            if (App.Iap.IsReady) UpdateShopStatus();
            else App.Iap.Ready += OnIapReady;
        }

        // Unsubscribe so no SDK event keeps a reference to a destroyed view.
        private void OnDestroy()
        {
            App.RemoteConfig.Updated -= OnRemoteConfigUpdated;
            App.Iap.Ready -= OnIapReady;
        }

        private void OnRemoteConfigUpdated()
        {
            Debug.Log("[MyGame] Remote Config updated, IsFetched=" + App.RemoteConfig.IsFetched);
            ApplyRemoteValues();
        }

        private void OnIapReady()
        {
            Debug.Log("[MyGame] IAP is ready");
            UpdateShopStatus();
        }

        private void ApplyRemoteValues()
        {
            long bonusGold = App.RemoteConfig.GetLong("daily_bonus_gold", fallback: 100);
            Debug.Log("[MyGame] Current bonus gold: " + bonusGold);
        }

        private void UpdateShopStatus()
        {
            string price = App.Iap.GetLocalizedPrice("pack_coins_starter");
            Debug.Log("[MyGame] Starter pack price: " + price);
        }
    }
}

7. So sánh Code Đúng vs Code Sai

Code Sai
// WRONG: assumes IAP is already connected when this scene starts
void Start()
{
    // Pressing Play directly in this scene skips the loading screen, so IAP may not be ready yet.
    var price = App.Iap.GetLocalizedPrice("vip_pack");
    UpdatePriceUI(price); // may be an empty string

    // WRONG: looking services up by hand instead of using App.X / ServiceLocator
    var ads = FindObjectOfType<AdsController>();
}
Code Đúng
// RIGHT: check IsReady, otherwise wait for the Ready event
void Start()
{
    if (App.Iap.IsReady)
    {
        UpdatePriceUI(App.Iap.GetLocalizedPrice("vip_pack"));
    }
    else
    {
        App.Iap.Ready += OnIapReady;
    }
}

private void OnIapReady()
{
    UpdatePriceUI(App.Iap.GetLocalizedPrice("vip_pack"));
}

// Always unsubscribe, otherwise the event keeps calling a destroyed object
private void OnDestroy()
{
    App.Iap.Ready -= OnIapReady;
}

8. Các dòng Console Log kỳ vọng

Với provider MGW đã cấu hình đầy đủ (file Firebase, MAX key, bật GameAnalytics), khi nhấn Play từ Scene mở đầu (Loading Scene) bạn sẽ thấy các log sau theo đúng thứ tự trong Console. Con số thời gian và id chỉ mang tính minh họa:

[IAP] No IReceiptValidator registered: receipts accepted without local validation
[Boot] ATT done in 0.01s
[Firebase] Ready (Analytics + Crashlytics)
[Boot] Firebase done in 0.45s
[Boot] Auth done in 0.20s
[RemoteConfig] Fetched, activated=True
[Boot] RemoteConfig done in 0.50s
[RTDB] Ready at /mygame/debug
[Boot] RealtimeDb+BI done in 0.02s
[Ads] MAX initialized
[Boot] Ads done in 0.60s
[Boot] GameAnalytics done in 0.01s
[IAP] Ready, 2 products
[Boot] IAP done in 1.10s
[Boot] Notifications done in 0.05s
[Boot] Background done in 0.01s
[Boot] Completed, user=17308000001234 new=True session=1
Khi nào các dòng log này hiện

Các dòng thông tin ở trên (CoreLog.Info) chỉ in khi CoreConfig.verboseLog bật hoặc bản build là Development; cảnh báo và lỗi luôn in. Dòng [IAP] No IReceiptValidator... chỉ xuất hiện khi game chưa đăng ký IReceiptValidator. Đoạn /mygame/ trong [RTDB] Ready at ... là giá trị realtimeDbRoot của bạn, còn debug đổi thành release ở bản không phải Development. Dòng [Boot] AppMetrica done ... xuất hiện thêm nếu bật AppMetrica. Với SayKit, log chỉ gồm các bước SayKit, RemoteConfig, IAP, Background rồi Completed.

9. Các lỗi thường gặp & Cách khắc phục

  • Service X is not registered. Was the Bootstrapper created?: Ném ra bởi ServiceLocator.Get<T>() (và mọi App.X) khi dịch vụ X chưa được đăng ký, ví dụ gọi quá sớm trước khi Bootstrapper.Boot() chạy, hoặc module cung cấp dịch vụ đó bị lỗi ở Install và bị loại. Khắc phục: gọi từ Start trở đi; với dịch vụ chỉ có ở MGW, bọc code trong #if !TB_SDK_SAYKIT (và dùng ServiceLocator.TryGet<T>(out var s) bên trong khối đó nếu dịch vụ có thể vắng).
  • [Boot] Missing Resources/CoreConfig.asset, using defaults. Create it via TapBrain > Select Core Config.: Dự án chưa có file cấu hình trong thư mục Resources/. Khắc phục: chọn menu Unity TapBrain > Select Core Config để tạo file và điền thông số (Editor cũng tự tạo file mặc định sau lần biên dịch lại kế tiếp nếu chưa có).
  • [Boot] <step> timed out after Xs, continuing: Do mạng chập chờn hoặc máy chủ phản hồi chậm. Đây là cảnh báo của cơ chế bảo vệ để game không bị đơ, không phải lỗi sập ứng dụng; dịch vụ tương ứng có thể sẵn sàng muộn hơn (xem mục 5).
  • [Boot] <step> threw: ...: Bước đó ném ngoại lệ; SDK bỏ qua bước và chạy tiếp. Đọc nội dung ngoại lệ ngay sau dấu threw: để biết nguyên nhân.

10. Checklist kiểm tra nhanh

Chỉ tương tác qua facade App.X hoặc ServiceLocator.
Kiểm tra IsReady của IAP hoặc IsFetched của Remote Config trước khi hiển thị giao diện.
Luôn hủy đăng ký sự kiện trong OnDestroy() để tránh rò rỉ bộ nhớ khi Scene bị hủy.