Chương 11: Kiểm tra & Gỡ lỗi (Verification & Troubleshooting)
Hướng dẫn toàn diện về quy trình kiểm chứng quá trình tích hợp SDK trong Unity Editor và trên thiết bị thực tế: nhận biết chính xác chuỗi log khởi động theo từng stack, phân biệt log cảnh báo dự kiến khi chưa có key với các lỗi nghiêm trọng, tra cứu nhanh triệu chứng - nguyên nhân - cách khắc phục, tuân thủ chốt chặn Build Guard và xử lý sự cố biên dịch.
1. Mục đích & Nguyên tắc kiểm thử
Một thay đổi mã nguồn trong dự án chỉ được coi là hoàn tất khi đã vượt qua các bước kiểm chứng hành vi thực thi thực tế (runtime behavior). Việc dự án biên dịch thành công (0 compile errors) chưa bao giờ là đủ, bởi các assembly bị giới hạn bởi Scripting Defines có thể bị bỏ qua âm thầm khi biên dịch.
1.1. Luôn bấm Play từ Loading Scene (Build Index 0)
Trong vòng đời khởi động, Bootstrapper.Boot() được đăng ký thực thi tự động đồng bộ tại thời điểm RuntimeInitializeLoadType.BeforeSceneLoad. Điều này đảm bảo toàn bộ hệ thống dịch vụ App.X được khởi tạo trước khi bất kỳ scene nào được nạp.
- Bấm Play tại scene Loading (Build Index 0): Component
LoadingScreensẽ kích hoạt quy trình chờ tiến trình khởi độngBootstrapper.Pipeline.IsDonehoàn tất kèm thời gian tối thiểuCoreConfig.minLoadingSeconds, sau đó chuyển cảnh sang scene kế tiếp (mặc định là Home với index 1). Đây là quy trình phản ánh chân thực nhất trải nghiệm người dùng thực tế. - Bấm Play trực tiếp tại scene Home hoặc Main: SDK vẫn khởi tạo
[Core]thành công, nhưng scene của bạn sẽ mở ngay lập tức mà không có màn hình chờ. Tại thời điểm các script trong scene gọiStart(), các bước trong pipeline bất đồng bộ (như Firebase, Remote Config, MAX Ads, IAP) có thể vẫn đang chạy ngầm trong background và chưa sẵn sàng!
Vì vậy, code gameplay tuyệt đối không được giả định rằng khi scene nạp xong thì toàn bộ dịch vụ đã sẵn sàng. Hãy luôn kiểm tra thuộc tính trạng thái (IsReady, IsInitialized, IsFetched) hoặc lắng nghe các sự kiện (Ready, Initialized, Updated).
// In a game screen script
void Start()
{
// Dangerous: when Play starts directly from this scene, IAP and Ads may not be ready yet.
// IsOwned() cannot see store purchases before IAP is ready, and on MGW ShowBanner() before
// the ads SDK is initialized only records the intent (nothing is shown yet).
buyButton.interactable = !App.Iap.IsOwned("vip_pass");
App.Ads.ShowBanner();
}
// In a game screen script
void OnEnable()
{
UpdateButtonState();
App.Iap.Ready += UpdateButtonState;
App.Ads.Initialized += OnAdsInitialized;
// Initialized fires only once: handle the case where ads are already up.
if (App.Ads.IsInitialized) OnAdsInitialized();
}
void OnDisable()
{
// These services live for the whole app: always unsubscribe, otherwise a destroyed
// screen stays subscribed and its handler throws MissingReferenceException later.
App.Iap.Ready -= UpdateButtonState;
App.Ads.Initialized -= OnAdsInitialized;
}
void UpdateButtonState()
{
if (App.Iap.IsReady) buyButton.interactable = !App.Iap.IsOwned("vip_pass");
}
void OnAdsInitialized() => App.Ads.ShowBanner();
1.2. Kiểm thử trên thiết bị thật (Device Verification)
Nhiều tính năng native không thể giả lập trọn vẹn trong Unity Editor:
- iOS App Tracking Transparency (ATT): Hộp thoại cấp quyền của hệ điều hành chỉ xuất hiện trên thiết bị iOS vật lý (iOS 14.5+). Ngoài iOS (kể cả trong Editor khi build target không phải iOS), bước ATT kết thúc ngay và
AttService.Statustrả vềNOT_SUPPORTED. - Quảng cáo mediation thực tế (AppLovin MAX / SayKit): Chỉ kiểm chứng được hành vi thật (banner, video interstitial, callback trả thưởng) trên thiết bị. Trong Editor, với MGW chưa điền MAX key thì
App.Ads.IsInitializedluônfalse,TryShowInterstitialtrả vềfalsevàShowRewardedtrảNotReady; khi đã có key, hành vi trong Editor do package MAX/SayKit quyết định nên không dùng làm bằng chứng nghiệm thu. - Xác thực biên lai hóa đơn (IAP In-App Purchase): Editor dùng Fake Store của Unity IAP. Nếu game chưa đăng ký
IReceiptValidatorthì SDK nhận biên lai mà không xác thực cục bộ; kiểm tra luồng thanh toán và xác thực chữ ký Tangle phải chạy bản build Sandbox trên iOS TestFlight hoặc Google Play Internal Testing. - Rung phản hồi (HapticService): Rung qua
Vibrator(Android) vàUIFeedbackGenerator(iOS) chỉ hoạt động trên thiết bị di động thật.
2. Trình tự Boot Log chuẩn (Expected Boot Sequence)
Hầu hết thông điệp log của SDK được ghi qua lớp CoreLog theo định dạng [Tag] message; một vài thông điệp lỗi (như [Boot] Missing Resources/CoreConfig.asset, [Boot] No SDK provider compiled in, [Boot] SDK module ... failed, [Sdk] Two SDK providers ...) dùng Debug.LogError trực tiếp nhưng cùng định dạng. Khi bật Play mode từ scene Loading với console được dọn sạch, hệ sinh thái khởi động sẽ in ra chuỗi log có cấu trúc rõ ràng.
CoreLog.Info (các dòng [Boot] ... done in ..., [IAP] Ready, [Ads] MAX initialized...) chỉ in khi CoreConfig.verboseLog bật hoặc khi chạy development build (trong Editor luôn in). Cảnh báo và lỗi luôn in, kể cả bản release. Các dòng log dưới đây là minh họa: thông điệp và thứ tự là thật, con số thời gian/số sản phẩm thì không.
2.1. Chuỗi Log khởi động cho MGW Stack
Đối với cấu hình tiêu chuẩn MGW (Firebase + AppLovin MAX + IAP), thứ tự thực thi của các bước boot và thông điệp chuẩn xác tại console như sau:
[IAP] No IReceiptValidator registered: receipts accepted without local validation (only if the game registered no validator)
[Boot] ATT done in 0.00s
[Firebase] App creation failed, Firebase disabled for this session. Add the per-game config (Assets/google-services.json, Assets/GoogleService-Info.plist; see Documentation~/KEYS.md in com.mgw.sdk): ... (dự kiến nếu chưa thêm file cấu hình Firebase)
[Boot] Firebase done in 0.05s
[Boot] Auth done in 0.00s
[Boot] RemoteConfig done in 0.00s
[Boot] RealtimeDb+BI done in 0.00s
[Ads] MAX SDK key not set in CoreConfig, ads disabled (dự kiến nếu key còn để REPLACE_ME)
[Boot] Ads done in 0.00s
[AppMetrica] API key not set in CoreConfig, AppMetrica disabled (dự kiến nếu bật module nhưng chưa điền key)
[Boot] AppMetrica done in 0.00s
[Boot] GameAnalytics done in 0.00s (thứ tự giữa AppMetrica và GameAnalytics không cố định)
[IAP] Unity Gaming Services init failed (project not linked?), continuing without it: ... (dự kiến nếu chưa liên kết Unity Cloud Project)
[IAP] Ready, 2 products
[Boot] IAP done in 0.42s
[Boot] Notifications done in 0.00s
[Boot] Background done in 0.00s
[Boot] Completed, user=17300000001234 new=True session=1
2.2. Chuỗi Log khởi động cho SayKit Stack
Khi dự án được cấu hình chuyển sang stack SayKit (nhà phát hành SayGames), quy trình khởi động gọn hơn vì SayKit tự quản lý consent và tích hợp mediation riêng:
[IAP] No IReceiptValidator registered: receipts accepted without local validation (only if the game registered no validator)
[SayKit] App key is still a placeholder in SayKitApp.cs; SayKit will not serve real ads (dự kiến nếu chưa điền key thật)
[Boot] SayKit done in 0.12s
[RemoteConfig] SayKit game config ready (14 fields)
[Boot] RemoteConfig done in 0.00s
[IAP] Ready, 2 products
[Boot] IAP done in 0.35s
[Boot] Background done in 0.00s
[Boot] Completed, user=17300000005678 new=False session=2
Lưu ý: Trên stack SayKit, không có bước ATT (do SayKit sở hữu màn hình GDPR/ATT riêng), không có bước Notifications cục bộ của MGW và không có các bước Firebase/Auth/Ads/GameAnalytics/AppMetrica (quảng cáo được khởi tạo trong bước SayKit). Nếu SayKit không khởi tạo kịp trong CoreConfig.sayKitInitTimeout (mặc định 10 giây), bạn sẽ thấy thêm [Boot] SayKit timed out after 10s, continuing (cảnh báo, game vẫn vào được).
2.3. Quy chuẩn định dạng thời gian và xử lý Timeout
Mỗi bước trong pipeline khởi động InitPipeline được giới hạn thời gian chạy (time-boxed) bằng một ngưỡng timeout riêng. Một số ngưỡng cấu hình được trong CoreConfig, số còn lại cố định trong code (bảng bên dưới):
- Nếu bước hoàn thành kịp thời hạn: Log in ra theo cấu trúc chính xác:
[Boot] <Tên_Bước> done in X.XXs(ví dụ:[Boot] IAP done in 0.38s). - Nếu bước chạy quá thời gian giới hạn: Coroutine của bước đó bị dừng lại, console in cảnh báo:
[Boot] <Tên_Bước> timed out after Xs, continuing. Tiến trình khởi động không bị treo mà tiếp tục chuyển sang bước kế tiếp để đảm bảo người chơi luôn vào được màn hình chính của game. - Nếu bước ném ngoại lệ (Exception): Lỗi được chặn lại bởi cơ chế Guard an toàn: console in lỗi
[Boot] <Tên_Bước> threw: <Exception>, bước bị coi là kết thúc (vẫn in tiếp dòngdone in ...) và pipeline tiếp tục chạy.
| Bước | Timeout |
|---|---|
ATT | 120 giây (cố định) |
Firebase | firebaseTimeout (mặc định 8 giây) |
Auth | 5 giây (cố định) |
RemoteConfig (MGW) | remoteConfigTimeout (mặc định 4 giây) |
RealtimeDb+BI | 1 giây (bước đồng bộ) |
Ads (MGW) | adsInitTimeout (mặc định 6 giây) |
SayKit | sayKitInitTimeout (mặc định 10 giây) |
RemoteConfig (SayKit) | 1 giây (cố định) |
GameAnalytics / AppMetrica | 1 giây (bước đồng bộ) |
IAP | 8 giây (cố định) |
Notifications (MGW) | 30 giây (cố định) |
Background | 1 giây (bước đồng bộ) |
3. Các Log bình thường khi dự án chưa điền Key
3.1. Dự án không có Key là trạng thái hợp lệ
Trong giai đoạn phát triển ban đầu hoặc khi lập trình viên chỉ cần kiểm tra logic gameplay/meta, dự án hoàn toàn có thể hoạt động mà chưa cần gắn các key dịch vụ thật (Firebase, MAX key, AppMetrica, IAP tangles). SDK được thiết kế phòng vệ chặt chẽ:
[Boot] Completed, user=... và chuyển cảnh mượt mà vào scene game. Các tính năng chưa cấu hình sẽ tự động lùi về chế độ an toàn (fallback) mà không làm sập ứng dụng.
3.2. Bảng đối chiếu Log dự kiến vs Lỗi thực sự
Bảng dưới đây giúp bạn phân biệt rõ ràng giữa các log cảnh báo bình thường khi chưa có key và những lỗi kỹ thuật thật sự cần khắc phục:
| Dòng Log xuất hiện | Phân loại | Nguyên nhân & Ý nghĩa | Hành động cần xử lý |
|---|---|---|---|
[Firebase] App creation failed, Firebase disabled for this session... |
Dự kiến (Bình thường) | Chỉ MGW. Log mức Error (hiện màu đỏ) nhưng là bình thường khi chưa có file Assets/google-services.json (Android) hoặc Assets/GoogleService-Info.plist (iOS). |
Bỏ qua trong giai đoạn dev: Auth, Remote Config (dùng giá trị mặc định) và RTDB tự bỏ qua. Trước khi phát hành, tải file cấu hình từ Firebase Console đặt vào thư mục Assets/. |
[Ads] MAX SDK key not set in CoreConfig, ads disabled |
Dự kiến (Bình thường) | Chỉ MGW. Trường CoreConfig.maxSdkKey (nền tảng hiện tại) đang chứa REPLACE_ME hoặc trống; App.Ads.IsInitialized sẽ ở lại false. |
Điền SDK Key lấy từ tài khoản AppLovin MAX vào CoreConfig.asset (và trong AppLovin Integration Manager). |
[AppMetrica] API key not set in CoreConfig, AppMetrica disabled |
Dự kiến (Bình thường) | Chỉ MGW, khi module AppMetrica được biên dịch (TB_USE_APPMETRICA + package) và enableAppMetrica bật nhưng appMetricaApiKey chưa điền. |
Điền key AppMetrica, hoặc tắt enableAppMetrica trong CoreConfig, hoặc bỏ chọn AppMetrica trong TapBrain > SDK Settings... |
[IAP] Unity Gaming Services init failed (project not linked?), continuing without it: ... |
Dự kiến (Bình thường) | Dự án chưa liên kết với Unity Cloud Project ID trong Project Settings > Services. Log mức cảnh báo. | Không ảnh hưởng tới việc test mua hàng giả lập trong Editor (store vẫn Ready). Liên kết Cloud ID khi chuẩn bị build thật. |
InAppPurchasing: IStoreService.Connect called without a callback defined... |
Thường vô hại (log bên thứ ba) | Log nội bộ của package Unity IAP, không do SDK in; nội dung có thể thay đổi theo phiên bản package. | Có thể bỏ qua nếu IAP vẫn đạt [IAP] Ready. |
GameAnalytics: Unsupported platform ... WindowsEditor |
Thường vô hại (log bên thứ ba) | Log của package GameAnalytics khi chạy trên Editor; không do SDK in và nội dung có thể thay đổi theo phiên bản. | Kiểm chứng việc gửi sự kiện GameAnalytics trên thiết bị Android/iOS. |
[SayKit] App key is still a placeholder in SayKitApp.cs; SayKit will not serve real ads |
Dự kiến (Bình thường) | Chỉ SayKit. Key của nền tảng đang chạy (APP_KEY_ANDROID hoặc APP_KEY_IOS) trong SayKitApp.cs còn trống hoặc bắt đầu bằng <. Log mức cảnh báo. |
Điền mã định danh ứng dụng do SayGames cung cấp trước khi build release (Build Guard chặn Release Build nếu còn placeholder). |
[Boot] No SDK provider compiled in: ads disabled, Remote Config uses defaults... |
LỖI THẬT SỰ (Fatal) | Không có assembly provider nào được biên dịch (define và package không khớp). Khi đó App.Ads là NullAdsService và Remote Config chỉ có giá trị mặc định. |
Mở menu TapBrain > SDK Settings... kiểm tra lại stack provider. Build Guard chặn mọi bản build ở trạng thái này. |
[Boot] Bootstrapper did not create the init pipeline ... Loading stopped. |
LỖI THẬT SỰ (Fatal) | Bootstrapper.Boot() gặp exception trước khi tạo pipeline, nên LoadingScreen dừng hẳn. |
Cuộn lên đầu Console để tìm dòng Exception đầu tiên và sửa lỗi ngay tại đó. Scene Loading sẽ đứng yên nếu lỗi này xảy ra. |
[Boot] SDK module X failed in <phase>, skipped: ... |
LỖI THẬT SỰ (Severe) | Một module SDK ném exception trong Install, AddBootSteps hoặc AddLateBootSteps. Lỗi ở Install loại module (hoặc provider) khỏi phiên chạy; lỗi ở hai pha còn lại chỉ làm mất các bước của lần gọi đó. |
Đọc inner exception để xác định module bên thứ ba nào bị lỗi cấu hình hoặc thiếu thư viện liên kết. |
[Boot] <step> threw: ... |
LỖI THẬT SỰ (Severe) | Lỗi logic bên trong một bước boot khiến coroutine của bước bị văng ra; pipeline vẫn tiếp tục. | Sửa ngoại lệ bên trong hàm xử lý của dịch vụ sở hữu bước đó. |
4. Bảng tra cứu Triệu chứng -> Nguyên nhân -> Giải pháp
4.1. Sự cố Khởi động và Hệ thống Core
| Triệu chứng / Dòng Log | Nguyên nhân cốt lõi | Cách khắc phục chuẩn xác |
|---|---|---|
[Boot] Missing Resources/CoreConfig.asset, using defaults. Create it via TapBrain > Select Core Config. |
Không tìm thấy tệp cấu hình CoreConfig.asset trong bất kỳ thư mục Resources/ nào của dự án (Boot dùng bản mặc định tạm). |
Bấm menu TapBrain > Select Core Config. SDK tạo file mặc định tại Assets/Resources/CoreConfig.asset (nếu chưa có file nào) và ping tới file đó; sau đó điền key vào asset. |
[CoreConfig] More than one Resources/CoreConfig.asset; keep one (...) |
Dự án có nhiều file CoreConfig.asset ở các thư mục Resources khác nhau (chỉ cảnh báo trong Editor). |
Xóa các bản copy thừa, chỉ giữ đúng 1 file CoreConfig.asset trong một thư mục Resources/. |
Service X is not registered. Was the Bootstrapper created? (InvalidOperationException) |
Gọi ServiceLocator.Get<T>() hoặc App.X khi Core chưa được khởi tạo: ngoài Play mode, trong static constructor/[RuntimeInitializeOnLoadMethod] chạy trước BeforeSceneLoad, hoặc với kiểu chưa từng được đăng ký. |
1. Không gọi dịch vụ trước khi Boot chạy (hãy dùng Awake/Start hoặc sự kiện sẵn sàng).2. Dùng ServiceLocator.TryGet<T>() cho dịch vụ tùy chọn.3. Dịch vụ chỉ có trên MGW (như FirebaseService) không tồn tại khi build SayKit và gây lỗi biên dịch chứ không phải lỗi này: xem mục 6.2. |
[Storage] Corrupted json at '<key>': ... |
Chuỗi JSON lưu trong LocalStorage (PlayerPrefs) bị lỗi cú pháp hoặc mô hình class dữ liệu đã bị thay đổi không tương thích. |
Xóa key bị hỏng bằng LocalStorage.Delete(key) hoặc sửa phần đọc để có giá trị fallback an toàn. |
Callback sau khi đổi scene ném MissingReferenceException |
Hàm callback của quảng cáo hoặc hộp thoại trả lời về một MonoBehaviour đã bị Destroy do chuyển đổi scene. |
Luôn bọc dòng đầu tiên của hàm callback bằng lệnh bảo vệ: if (this == null) return;, và hủy đăng ký các sự kiện của service (-=) trong OnDisable/OnDestroy. |
4.2. Sự cố Quảng cáo (App.Ads)
| Triệu chứng / Dòng Log | Nguyên nhân cốt lõi | Cách khắc phục chuẩn xác |
|---|---|---|
[Ads] Inter skipped at <placement>: interval |
Chỉ MGW (log Info, cần verbose). Bị chặn bởi AdsPolicy: chưa đủ số giây inter_interval_sec (mặc định 30s) kể từ lần đóng quảng cáo toàn màn hình (interstitial, rewarded hoặc app open) gần nhất. |
Hành vi đúng thiết kế để chống spam quảng cáo. Nếu cần thay đổi thời gian chờ, cập nhật inter_interval_sec trên Remote Config. onClosed vẫn được gọi ngay. |
[Ads] Inter skipped at <placement>: first_delay |
Chỉ MGW. Phiên chơi chưa đạt mốc inter_first_delay_sec (mặc định 60s) tính từ lúc Core khởi động. |
Đảm bảo không kỳ vọng interstitial ngay khi vừa vào game; onClosed vẫn được gọi ngay. |
[Ads] Inter skipped at <placement>: remove_ads |
Chỉ MGW. Người chơi đã mua gói Xóa quảng cáo (sản phẩm trùng CoreConfig.removeAdsProductId). |
Đúng thiết kế. Gói Xóa quảng cáo chặn Banner, Interstitial và App Open; Rewarded vẫn phục vụ khi người chơi chủ động bấm xem. |
[Ads] Inter skipped at <placement>: showing hoặc not_ready |
Chỉ MGW. Một quảng cáo toàn màn hình khác đang hiển thị, hoặc interstitial tiếp theo chưa nạp xong từ network. | Gọi App.Ads.IsInterstitialReady trước khi mở popup mời xem, và luôn truyền callback onClosed vào TryShowInterstitial để tiếp tục luồng game khi quảng cáo bị bỏ qua (callback được gọi đồng bộ trước khi hàm trả về false). |
App.Ads.IsInitialized luôn false; ShowRewarded trả ngay RewardedResult.NotReady |
Với MGW: enableAds tắt hoặc MAX SDK key còn REPLACE_ME (bước Ads chạy rỗng, App.Ads vẫn là MaxAdsService nhưng chưa khởi tạo); hoặc video rewarded chưa nạp xong/đang có quảng cáo khác hiển thị. SayKit bỏ qua enableAds. NullAdsService chỉ được dùng khi không có provider nào đăng ký IAdsService. |
Ẩn hoặc làm mờ nút xem video nếu App.Ads.IsRewardedReady == false; chỉ trao thưởng khi kết quả là Rewarded. |
[Ads] Inter display failed: ... hoặc [Ads] Rewarded display failed: ... |
Chỉ MGW. Mạng quảng cáo native trả mã lỗi hiển thị. Với rewarded, callback nhận RewardedResult.Failed. |
SDK nạp lại quảng cáo ngay; nếu việc nạp thất bại, nó thử lại theo exponential backoff (tối đa 64 giây). Game cần khôi phục lại màn hình gameplay bình thường. |
[Ads] No ads provider compiled in, mediation debugger unavailable |
Gọi App.Ads.ShowMediationDebugger() khi App.Ads là NullAdsService (không có provider nào đăng ký dịch vụ quảng cáo). |
Kiểm tra provider (TapBrain > SDK Settings...). Khi provider có mặt: MGW chỉ mở debugger sau khi MAX đã khởi tạo (chưa khởi tạo thì lệnh không làm gì), SayKit mở debugger MAX của SayKit. |
4.3. Sự cố In-App Purchase (App.Iap)
| Triệu chứng / Dòng Log | Nguyên nhân cốt lõi | Cách khắc phục chuẩn xác |
|---|---|---|
[IAP] <id> tx=... kept pending (no handler or handler refused) |
Game chưa đăng ký xử lý trao quà qua App.Iap.SetGrantHandler(...), hoặc delegate trả về false/ném exception (không nhận diện được product ID hoặc lưu dữ liệu thất bại). Callback của Purchase nhận PurchaseResult.Pending. |
1. Đảm bảo gọi App.Iap.SetGrantHandler sớm ngay khi khởi tạo hệ thống kho đồ.2. Handler chỉ trả về true khi đã cộng vật phẩm và lưu thành công. Nếu trả về false, đơn hàng được giữ lại và giao lại khi SetGrantHandler được gọi ở lần mở game sau. |
[IAP] Invalid receipt for <id> tx=..., not granted |
IReceiptValidator của game từ chối biên lai: dữ liệu Tangle không khớp với Bundle Identifier hoặc khóa giấy phép Google Play / App Store. |
Chạy lại Services > In-App Purchasing > Receipt Validation Obfuscator... để tạo lại các lớp Tangle khớp với ứng dụng hiện tại. |
[IAP] Purchase of <productId> already in progress |
Người chơi bấm liên tục nút mua khi một giao dịch của cùng sản phẩm đang được xử lý. Lần gọi trùng bị bỏ qua và callback của lần gọi trùng không bao giờ được gọi. | Vô hiệu hóa nút mua ngay khi bắt đầu giao dịch và bật lại trong callback của lần gọi đã được chấp nhận. Nếu dùng App.Overlay.ShowBlocker() (đếm tham chiếu), đừng gọi cho lần mua trùng: HideBlocker() sẽ không bao giờ được gọi bù và màn hình bị chặn vĩnh viễn. |
[IAP] Products fetch failed: ... |
Không thể tải danh sách sản phẩm từ catalog (thường do Resources/IAPProductCatalog.json bị lỗi hoặc thiết bị offline). |
Kiểm tra danh mục trong Services > In-App Purchasing > IAP Catalog.... App.Iap.IsReady sẽ giữ giá trị false và Purchase trả NotReady. |
Gọi Restore hai lần liên tiếp, callback đầu không bao giờ chạy |
IapService chỉ giữ một callback Restore: lần gọi thứ hai ghi đè callback của lần trước đang chờ. Khi IAP chưa sẵn sàng, Restore gọi ngay onDone(false). |
Khóa nút Restore cho đến khi callback chạy xong. |
4.4. Sự cố Remote Config & Firebase
| Triệu chứng / Dòng Log | Nguyên nhân cốt lõi | Cách khắc phục chuẩn xác |
|---|---|---|
[RemoteConfig] <key>=<v> is out of int range, keeping <current> (hoặc float range) |
Giá trị số trên dashboard Remote Config vượt quá giới hạn dung lượng của kiểu dữ liệu int (-2,147,483,648 đến 2,147,483,647) hoặc float. |
SDK chủ động giữ lại giá trị hiện tại của field để tránh lỗi tràn số (overflow). Hãy điều chỉnh lại giá trị trên Console hoặc đổi kiểu field trong class đã Bind sang long hoặc double. |
[Boot] Auth timed out after 5s, continuing hoặc [Firebase] Anonymous sign-in failed: ... restricted to administrators |
Dự án sử dụng Firebase thật nhưng tính năng Đăng nhập Ẩn danh (Anonymous Authentication) chưa được bật trong cài đặt. | Truy cập Firebase Console > Authentication > Sign-in method và kích hoạt nhà cung cấp Anonymous. |
[RTDB] Set <path> failed: ... (Internal task faulted) |
Quy tắc bảo mật (Security Rules) của Realtime Database từ chối quyền ghi, hoặc database chưa được khởi tạo trên Firebase. | Tạo database trên Firebase Console và cấu hình rules cho phép ghi khi đã xác thực: ".write": "auth != null". |
4.5. Sự cố Dialog & Scene Loader
| Triệu chứng / Dòng Log | Nguyên nhân cốt lõi | Cách khắc phục chuẩn xác |
|---|---|---|
[Dialog] Missing prefab Resources/Dialogs/X with component X |
Không tìm thấy Prefab của hộp thoại trong thư mục Resources/Dialogs/, đặt sai tên file hoặc prefab chưa được gắn component tương ứng. |
Tạo prefab tại đúng đường dẫn Resources/Dialogs/<Tên_Script>.prefab (trong bất kỳ thư mục Resources nào, ví dụ Assets/Resources/Dialogs/) và đảm bảo script component được gắn ngay ở GameObject gốc của prefab. Hộp thoại khi đó mới mở được bằng App.Dialogs.Show<T>(). |
[Dialog] X already open, reusing it |
Gọi App.Dialogs.Show<T>() khi hộp thoại kiểu T đó đã đang hiển thị trên màn hình. |
Đây là cơ chế tự bảo vệ của SDK để tránh nhân bản nhiều hộp thoại cùng loại. SDK sẽ trả về chính instance đang mở thay vì tạo mới. |
[Scene] Ignored load <buildIndex>: another load is running |
Gọi App.Scenes.Load(...) khi một tiến trình chuyển scene trước đó vẫn đang trong quá trình fade out / load bất đồng bộ. |
SDK bỏ qua lệnh gọi thừa để tránh làm hỏng trạng thái chuyển cảnh. Kiểm tra cờ App.Scenes.IsLoading trước khi yêu cầu chuyển scene. |
[Scene] Scene index <scene> is not in Build Settings |
Chỉ số Build Index truyền vào không tồn tại trong danh sách Scenes In Build của dự án. | Mở File > Build Settings... và kéo thả các scene cần thiết vào danh sách theo đúng thứ tự (Scene index 0 bắt buộc là Loading). |
5. Chốt chặn Build Guard (CoreConfigTools)
Một trong những sự cố tồi tệ nhất khi vận hành game là việc xuất bản bản build release ra chợ ứng dụng nhưng vẫn mang theo các ad unit test, thiếu cấu hình Firebase hoặc chưa đổi định danh ứng dụng. Để ngăn chặn hoàn toàn rủi ro này, SDK tích hợp hệ thống kiểm tra tự động trước khi đóng gói: Build Guard (triển khai qua IPreprocessBuildWithReport trong CoreConfigTools.cs).
5.1. Cơ chế hoạt động: Development vs Release
Hành vi của Build Guard được phân tách rõ ràng tùy thuộc vào chế độ đóng gói:
- Bản build thử nghiệm (Development Build): Với danh sách placeholder (
[CoreConfig] Placeholders left), Build Guard chỉ in cảnh báo (Debug.LogWarning) để không cản trở quá trình debug nội bộ. Lưu ý các lỗi cứng ở bảng dưới (không có provider,SayKitAppsai, còn package MGW trên SayKit) vẫn chặn cả Development Build. - Bản build phát hành (Release Build): Nếu phát hiện bất kỳ trường placeholder nào chưa được điền đầy đủ, Build Guard sẽ ném ra
BuildFailedExceptionvà chặn đứng quá trình đóng gói ngay lập tức. Chế độ Development được xác định theo tùy chọn của chính bản build đó (kể cả build qua script/CI), không phải ô tích trong Build Settings.
5.2. Toàn bộ điều kiện kiểm tra và thông báo lỗi
Dưới đây là danh sách toàn bộ các điều kiện kiểm tra nghiêm ngặt của Build Guard:
| Thông báo ngoại lệ (BuildFailedException) | Phạm vi áp dụng | Ý nghĩa và Cách sửa |
|---|---|---|
[SdkSettings] No SDK provider compiled for provider <Provider>. Fix it in TapBrain > SDK Settings. |
Tất cả các bản build | Hệ thống không tìm thấy lớp hiện thực ISdkProvider nào phù hợp với các scripting define hiện tại. Vào menu TapBrain > SDK Settings... để thiết lập lại. |
[SdkSettings] Found N SayKitApp classes, expected 1 (Assets/SayKit/SayKitApp.cs)... |
Mọi bản build SayKit | Bị trùng lặp script SayKitApp. Cần xóa file Assets/SayKitExtensions/SayKitApp.cs và chỉ giữ lại duy nhất file tại Assets/SayKit/SayKitApp.cs. |
[SdkSettings] SayKitApp has a [RuntimeInitializeOnLoadMethod] init: the SDK already calls SayKit.init... |
Mọi bản build SayKit | File SayKitApp.cs chứa phương thức tự khởi tạo. SDK đã tự động quản lý gọi SayKit.init trong pipeline, cần xóa bỏ attribute này để tránh khởi tạo SayKit 2 lần. |
[SdkSettings] <package> is still installed; SayKit requires removing it... |
Mọi bản build SayKit | Các package của MGW (như AppLovin MAX, Firebase, External Dependency Manager, GameAnalytics, AppMetrica) vẫn còn trong manifest.json. Cần sử dụng menu chuyển đổi stack để gỡ bỏ chúng. |
[CoreConfig] Placeholders left: ... (Development builds only warn.) |
Chỉ chặn trên Release builds | Danh sách liệt kê từng trường chưa hoàn thiện cấu hình (xem bảng bên dưới). |
Chi tiết từng mục có thể bị liệt kê trong thông báo [CoreConfig] Placeholders left::
Missing Resources/CoreConfig.asset: Chưa tạo file cấu hình trong Resources.Assets/google-services.json (Firebase Android config): (MGW) build Android khi bậtenableFirebasenhưng thiếu file cấu hình.Assets/GoogleService-Info.plist (Firebase iOS config): (MGW) build iOS khi bậtenableFirebasenhưng thiếu file cấu hình.MAX SDK key (Android)/(iOS): (MGW) khi bậtenableAds, SDK Key của AppLovin MAX chưa được điền. Cả hai nền tảng đều được kiểm tra, bất kể nền tảng đang build.Banner ad unit,Interstitial ad unit,Rewarded ad unit(Android / iOS): (MGW) Ad Unit ID chưa được cấu hình.AppMetrica API key (Android)/(iOS): khi module AppMetrica được biên dịch (TB_USE_APPMETRICA) vàenableAppMetricabật nhưng chưa điền API Key.SayKitApp.APP_KEY_ANDROID/SayKitApp.APP_KEY_IOS/SayKitApp.APP_NAME_IOS/SayKitApp.APP_BUNDLE_IOS: (SayKit) các hằng số trongSayKitApp.cscòn chứa dấu<ở đầu hoặc để trống.iOS App Store id:CoreConfig.iosAppStoreIdchưa điền. Kiểm tra cho cả hai provider và mọi nền tảng build, kể cả build Android.Bundle id is still the template id: Application Identifier trong PlayerSettings vẫn bắt đầu bằngcom.tapbrain.template. Hãy đặt định danh gói chính thức.
5.3. Kiểm tra chủ động qua Editor Menu
Bạn không cần phải thực hiện một tiến trình Build đầy đủ tốn nhiều thời gian chỉ để kiểm tra các thông số. Hãy sử dụng menu có sẵn của SDK:
TapBrain > Validate Core Config
Menu này chạy phần kiểm tra placeholder của Build Guard (cùng danh sách mục ở trên) cho nền tảng đang kích hoạt trong Build Settings. Nó không chạy các kiểm tra cứng (có provider, SayKitApp, package MGW còn sót trên SayKit): những kiểm tra đó chỉ chạy khi build.
- Nếu mọi thứ đã chuẩn chỉnh: Console in
[CoreConfig] OK, no placeholders left. - Nếu còn thiếu sót: Console in cảnh báo vàng
[CoreConfig] Placeholders left:kèm danh sách từng mục cần bổ sung.
6. Sự cố Biên dịch & Scripting Defines
6.1. Nguyên tắc quản lý Defines
SDK sử dụng các Scripting Define Symbols để điều khiển việc biên dịch các assembly:
- Không có define: Sử dụng MGW Provider mặc định (assembly
TapBrain.Sdk.MGWcó điều kiện ràng buộc là!TB_SDK_SAYKIT). TB_SDK_SAYKIT: Kích hoạt stack SayKit, assembly của MGW sẽ tự động bị loại khỏi quá trình biên dịch.TB_USE_GA: Biên dịch module tích hợp GameAnalytics (chỉ hoạt động với MGW).TB_USE_APPMETRICA: Biên dịch module tích hợp AppMetrica (chỉ hoạt động với MGW).
6.2. Các lỗi biên dịch thường gặp và cách sửa
| Lỗi biên dịch | Nguyên nhân | Giải pháp xử lý |
|---|---|---|
Lỗi không tìm thấy type của MGW sau khi chuyển sang SayKit (ví dụ The type or namespace name 'FirebaseService' could not be found) |
Mã nguồn của game tham chiếu trực tiếp (kể cả dòng using TapBrain.Sdk.Mgw;) đến các lớp chỉ có trong assembly MGW (như FirebaseService, RealtimeDbService, NotificationService) mà không có chỉ thị tiền xử lý. |
Bọc vùng mã nguồn đó (cả using) trong khối #if !TB_SDK_SAYKIT và lấy dịch vụ qua ServiceLocator.TryGet<T>(). |
Console in [Sdk] Two SDK providers compiled in (MGW, SayKit). Keep one... (log lỗi lúc chạy, không phải lỗi biên dịch) |
Cả 2 assembly provider cùng được biên dịch, tức cấu hình define đã bị phá hỏng (các asmdef vốn loại trừ nhau theo TB_SDK_SAYKIT). Provider thứ hai bị bỏ qua; Build Guard không bắt trường hợp này. |
Dùng menu TapBrain > SDK > Switch To ... để chuẩn hóa lại môi trường về một nhà cung cấp duy nhất; không sửa define bằng tay. |
| Một asmdef trong game không thể nhìn thấy type của bên thứ ba | Các file Assembly Definition (asmdef) không thể tham chiếu trực tiếp tới Assembly-CSharp hoặc Assembly-CSharp-firstpass. |
Tạo một asmdef phụ trợ bọc lấy thư mục plugin đó, hoặc để mã nguồn game nằm trong thư mục thông thường không có asmdef. |
| Lỗi trùng lặp file AAR / Duplicate class khi build Android (stack MGW) | Xung đột thư viện native giữa các SDK bên thứ ba do External Dependency Manager (EDM) chưa giải quyết đồng bộ. | Thực hiện: Assets > External Dependency Manager > Android Resolver > Force Resolve. Không sửa tay dependency trong các file Gradle mẫu ở Assets/Plugins/Android/ (EDM ghi đè chúng). Trên SayKit, các file Gradle mẫu do SayKit tự sinh khi build. |
7. Cách đọc Unity Stack Trace cho các Frame thuộc SDK
Khi một ngoại lệ xảy ra trong Unity Console, việc phân biệt frame thuộc mã nguồn game của bạn hay thuộc SDK sẽ giúp khoanh vùng nguyên nhân nhanh chóng:
ArgumentNullException: Value cannot be null.
Parameter name: key
at System.Collections.Generic.Dictionary`2[TKey,TValue].FindEntry (TKey key) [0x00008] in <...>:0
at TapBrain.Core.Services.IapService.Purchase (System.String productId, System.Action`1[T] onDone) [0x00010] in Packages/com.mgw.sdk/Runtime/Core/Services/IAP/IapService.cs:NN
at MyGame.UI.ShopScreen.OnBuyClicked () [0x00012] in Assets/Scripts/UI/ShopScreen.cs:45
at UnityEngine.Events.InvokableCall.Invoke () [0x00000] in <...>:0
Stack trace luôn liệt kê frame đang chạy ở trên cùng, rồi tới nơi gọi nó. Ví dụ trên (minh họa, số dòng không thật) là một productId null bị truyền từ UI vào SDK: frame đầu thuộc .NET, frame kế là SDK, frame tiếp theo là mã game đã gọi.
Quy tắc phân tích:
- Namespace
TapBrain.Core.*: Nằm trong gói packagecom.mgw.sdk. Đây là các dịch vụ hạ tầng cốt lõi (Bootstrap, InitPipeline, ServiceLocator, IapService, SceneLoader, DialogService...). Nếu lỗi xuất phát từ đây, hãy kiểm tra xem bạn đã truyền tham số hợp lệ vào API hay chưa (ví dụ: Product ID có null/rỗng không, callback có bị null không). - Namespace
TapBrain.Sdk.Mgw.*,TapBrain.Sdk.SayGames.*hoặcTapBrain.Sdk.Analytics.*(GameAnalytics/AppMetrica): Các lớp adapter kết nối với SDK bên thứ ba (Firebase, MAX, SayKit, GameAnalytics, AppMetrica). Lỗi ở đây thường liên quan tới trạng thái kết nối mạng, khởi tạo native hoặc thiếu key. - Frame thuộc mã nguồn game của bạn (ví dụ
MyGame.*): Điểm kích hoạt lệnh gọi (nằm bên dưới frame SDK trong stack). Hãy kiểm tra các đối tượng UI, biến tham chiếu hoặc delegate được truyền vào SDK.
8. Checklist nghiệm thu từng tính năng trước khi Release
Trước khi xuất bản bản build chính thức lên cửa hàng ứng dụng, hãy hoàn thành bảng kiểm tra nghiệm thu từng tính năng dưới đây:
inter_interval_sec; Rewarded video chỉ trao thưởng khi nhận kết quả RewardedResult.Rewarded; Callback onClosed luôn được kích hoạt kể cả khi quảng cáo bị skip.App.Iap.SetGrantHandler; Handler trả về true sau khi đã lưu dữ liệu thành công; Sản phẩm Xóa quảng cáo (nếu có) vô hiệu hóa banner và interstitial; Test phục hồi giao dịch (Restore Purchases) hoạt động chuẩn xác trên iOS.LevelStarted, LevelCompleted, LevelFailed); Không phát sinh cảnh báo sự kiện bị bỏ qua do thiếu tên; Định danh người chơi UserId đồng nhất giữa các hệ thống backend.App.RemoteConfig.Bind để tự động cập nhật dữ liệu; Không có cảnh báo giá trị số bị vượt ngưỡng dung lượng (out of range).Resources/Dialogs/; Toàn bộ scene game được nạp qua App.Scenes.Load(...); Chuyển scene đóng tất cả các hộp thoại đang mở; Scene index 0 trong Build Settings là scene Loading.TapBrain > Validate Core Config đạt kết quả [CoreConfig] OK, no placeholders left; Không còn giá trị REPLACE_ME trong CoreConfig; Đã thay đổi Bundle Identifier sang định danh chính thức của studio.