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, facadeApp.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ó trongAssets/GooglePlayPlugins/với cả hai provider; thiếu chúng thìTapBrain.Corekhô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_SAYKITvà góiio.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.
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ạngDictionary<Type, object>. Theo quy ước, chỉBootstrappervà hàmInstallcủa module SDK gọiServiceLocator.Register<T>(); code game chỉGet/TryGet.Get<T>()némInvalidOperationExceptionnế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.Xkhô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ý trongServiceLocator.
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).
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:
- 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. - Tạo GameObject vĩnh cửu
[Core]mang componentCoreHostvà đánh dấuDontDestroyOnLoad. - Khởi tạo và đăng ký các dịch vụ cơ bản vào
ServiceLocator. - Cài đặt Provider và các Extension đã đăng ký.
- Đă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ặcInstallcủa provider bị lỗi). - Khởi chạy
InitPipelinegồ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 trongBoot(), nênApp.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). |
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
LoadingScreenbị 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/Readymớ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
// 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>();
}
// 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
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ởiServiceLocator.Get<T>()(và mọiApp.X) khi dịch vụXchưa được đăng ký, ví dụ gọi quá sớm trước khiBootstrapper.Boot()chạy, hoặc module cung cấp dịch vụ đó bị lỗi ởInstallvà bị loại. Khắc phục: gọi từStarttrở đi; với dịch vụ chỉ có ở MGW, bọc code trong#if !TB_SDK_SAYKIT(và dùngServiceLocator.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ụcResources/. Khắc phục: chọn menu UnityTapBrain > 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ấuthrew:để biết nguyên nhân.
10. Checklist kiểm tra nhanh
App.X hoặc ServiceLocator.IsReady của IAP hoặc IsFetched của Remote Config trước khi hiển thị giao diện.OnDestroy() để tránh rò rỉ bộ nhớ khi Scene bị hủy.