Chương 12: Tra cứu nhanh & Quy chuẩn (Quick Reference & Rules)
Trang tra cứu cô đọng toàn diện: bảng thông số API của toàn bộ dịch vụ App.X, danh mục quy chuẩn kiến trúc bắt buộc cho mã nguồn game kèm ví dụ đúng/sai, bảng thuật ngữ cốt lõi và mục hỏi đáp FAQ chuyên sâu giúp giải quyết tức thì các vướng mắc trong quá trình tích hợp.
1. API Cheat-Sheet (Tra cứu nhanh dịch vụ App.X)
Lớp tĩnh TapBrain.Core.App hoạt động như một Facade trung tâm. Các thuộc tính bên trong chuyển tiếp cuộc gọi tới các dịch vụ được đăng ký trong ServiceLocator.
1.1. App.Ads (IAdsService)
Điều khiển quảng cáo Banner, Interstitial, Rewarded và App Open. Trên MGW, tần suất interstitial được kiểm soát bởi chính sách giãn cách AdsPolicy (đọc từ Remote Config); SayKit giới hạn tần suất ở phía server nên không dùng AdsPolicy.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
IsInitialized |
Property (get) | bool |
True khi SDK quảng cáo (MAX/SayKit) đã khởi tạo xong. Với MGW, luôn false nếu enableAds tắt hoặc MAX SDK key chưa điền (App.Ads vẫn là MaxAdsService, chỉ là chưa khởi tạo). SayKit bỏ qua enableAds. NullAdsService (không có provider) luôn false. |
IsInterstitialReady |
Property (get) | bool |
True khi interstitial đã nạp xong (và chưa mua Xóa quảng cáo trên SayKit). SayKit lưu đệm kết quả này 1 giây. |
IsRewardedReady |
Property (get) | bool |
True khi Rewarded ad unit đã sẵn sàng phục vụ. |
Initialized |
Event | event Action |
Bắn một lần duy nhất khi hệ thống quảng cáo khởi tạo thành công. Đăng ký sau thời điểm đó sẽ không nhận lại sự kiện: kiểm tra IsInitialized trước. |
FullscreenOpened |
Event | event Action<AdFormat> |
Kích hoạt khi quảng cáo toàn màn hình (Interstitial, Rewarded, AppOpen) mở ra. Banner không phát sự kiện này. |
FullscreenClosed |
Event | event Action<AdFormat> |
Kích hoạt khi quảng cáo toàn màn hình đóng lại (Interstitial, Rewarded, AppOpen). Trên MGW, sự kiện này cũng đặt lại mốc thời gian giãn cách của AdsPolicy, kể cả sau khi xem rewarded. |
ShowBanner() |
Method | void ShowBanner() |
Hiển thị banner ở cạnh dưới màn hình. Tự động bỏ qua nếu đã mua gói Xóa quảng cáo. Trên MGW, gọi trước khi MAX khởi tạo xong chỉ ghi nhận ý định; banner tự hiện khi khởi tạo xong nếu CoreConfig.autoShowBanner bật. |
HideBanner() |
Method | void HideBanner() |
Ẩn banner hiện tại. |
TryShowInterstitial |
Method | bool TryShowInterstitial(string placement, Action onClosed = null) |
Trả về false nếu bị từ chối do chưa sẵn sàng, đang mở quảng cáo khác, hoặc (MGW) vi phạm giãn cách. Callback onClosed luôn được gọi đúng một lần: đồng bộ, trước khi hàm trả về false nếu bị bỏ qua. |
ShowRewarded |
Method | void ShowRewarded(string placement, Action<RewardedResult> onResult) |
onResult luôn được gọi đúng một lần (đồng bộ với NotReady nếu chưa sẵn sàng). Kết quả: Rewarded, Skipped, NotReady, Failed; Failed chỉ có trên MGW (SayKit gộp cả lỗi hiển thị vào Skipped). Chỉ trao quà khi kết quả là Rewarded. |
TryShowAppOpen |
Method | bool TryShowAppOpen() |
Chỉ MGW: thử hiển thị quảng cáo App Open; cần ad unit App Open trong CoreConfig, app_open_enabled = true trên Remote Config, MAX đã khởi tạo và chưa mua Xóa quảng cáo. SDK cũng tự gọi khi người chơi quay lại app từ nền. SayKit và NullAdsService luôn trả false. |
ShowMediationDebugger |
Method | void ShowMediationDebugger() |
MGW: mở Mediation Debugger của AppLovin MAX (chỉ khi MAX đã khởi tạo, nếu chưa thì không làm gì). SayKit: mở debugger MAX của SayKit. NullAdsService: chỉ in cảnh báo. |
| Trạng thái Xóa quảng cáo | Lưu ý | — | Không có thuộc tính công khai trên App.Ads: AdsPolicy.IsAdsRemoved là internal và game không gọi được. Game đọc trạng thái qua App.Iap.IsOwned(removeAdsProductId) (id lấy từ CoreConfig.removeAdsProductId, ví dụ Bootstrapper.Config.removeAdsProductId). |
1.2. App.Iap (IIapService)
Quản lý danh mục mua hàng In-App Purchases, kiểm tra quyền sở hữu, phục hồi giao dịch và sổ cái đơn hàng chờ xử lý.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
IsReady |
Property (get) | bool |
True khi đã kết nối cửa hàng ứng dụng và nạp xong danh mục sản phẩm từ IAP Catalog. |
Ready |
Event | event Action |
Kích hoạt khi IAP hoàn tất khởi tạo và danh sách sản phẩm sẵn sàng. Đăng ký sau thời điểm đó sẽ không nhận lại: kiểm tra IsReady trước. |
RemoveAdsGranted |
Event | event Action |
Kích hoạt khi Core cấp quyền Xóa quảng cáo (mua mới hoặc khôi phục) cho sản phẩm CoreConfig.removeAdsProductId. |
SetGrantHandler |
Method | void SetGrantHandler(PurchaseGrantHandler handler) |
Đăng ký đúng một bộ xử lý trao quà (gọi lại sẽ thay thế). Delegate có chữ ký bool Handler(string productId); các đơn đang chờ từ phiên trước được giao ngay khi gọi. Chỉ trả về true khi đã cộng quà và lưu dữ liệu thành công; false, exception hoặc chưa đăng ký handler đều giữ đơn ở trạng thái chờ. |
GetLocalizedPrice |
Method | string GetLocalizedPrice(string productId) |
Lấy chuỗi giá tiền bản địa hóa kèm ký hiệu tiền tệ từ Store (ví dụ: "$0.99" hoặc "22.000 đ"). Trả về chuỗi rỗng nếu store hoặc sản phẩm chưa sẵn sàng. |
IsOwned |
Method | bool IsOwned(string productId) |
True khi store có đơn đã xác nhận cho sản phẩm (non-consumable/subscription); với removeAdsProductId là true khi cờ Xóa quảng cáo đã được cấp. Trả về false khi store chưa sẵn sàng. |
Purchase |
Method | void Purchase(string productId, Action<PurchaseResult> onDone) |
Bắt đầu thanh toán. Kết quả: Success, Cancelled, Failed, Pending, NotReady (đồng bộ nếu IAP chưa sẵn sàng). Pending xảy ra khi store hoãn đơn (ví dụ Ask to Buy) hoặc khi grant handler chưa đăng ký/từ chối. Gọi lại cùng một sản phẩm khi giao dịch đang chạy sẽ bị bỏ qua kèm cảnh báo và callback của lần gọi trùng không bao giờ được gọi. |
Restore |
Method | void Restore(Action<bool> onDone) |
Phục hồi các giao dịch non-consumable đã mua trước đó (chủ yếu phục vụ yêu cầu kiểm duyệt của Apple App Store). Gọi onDone(false) ngay nếu IAP chưa sẵn sàng. Chỉ giữ một callback: gọi lần hai khi lần đầu chưa xong sẽ ghi đè và callback của lần đầu không bao giờ được gọi. |
1.3. App.Analytics (IAnalyticsService)
Trung tâm gửi sự kiện phân tích chuẩn hóa. Trên MGW, sự kiện được chuyển tiếp tới các sink đang hoạt động (Firebase, và GameAnalytics/AppMetrica khi module được biên dịch và bật); trên SayKit, tới SayKitAnalyticsSink (hàm native của SayKit). Sink chưa sẵn sàng sẽ bỏ qua sự kiện.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
LevelStarted |
Method | void LevelStarted(int level, string tag = null) |
Đánh dấu bắt đầu màn chơi chính (chỉ số level bắt đầu từ 1). Core ghi nhận thời điểm bắt đầu. |
LevelCompleted |
Method | void LevelCompleted(int level, long score = 0, string tag = null) |
Ghi nhận người chơi vượt qua level. Trên MGW, duration_sec được tự tính từ LevelStarted (bỏ qua nếu chưa có LevelStarted). |
LevelFailed |
Method | void LevelFailed(int level, long score = 0, string tag = null) |
Ghi nhận thất bại tại level. Trên MGW, duration_sec được tự tính như trên. |
StageStarted |
Method | void StageStarted(int number, string tag = null) |
Theo dõi các chặng nhỏ (stages) bên trong level hiện tại; mang theo level của LevelStarted gần nhất. |
StageCompleted / StageFailed |
Method | void StageCompleted(int number, long score = 0, string tag = null)void StageFailed(int number, long score = 0, string tag = null) |
Kết thúc chặng; trên MGW tự tính duration_sec của chặng. |
ExtraLevelStarted |
Method | void ExtraLevelStarted(string tag = null) |
Màn chơi phụ (Bonus level, Boss rush, Event level), không làm tăng bộ đếm level chính. |
ExtraLevelCompleted / ExtraLevelFailed |
Method | void ExtraLevelCompleted(long score = 0, string tag = null)void ExtraLevelFailed(long score = 0, string tag = null) |
Kết thúc màn chơi phụ. |
TutorialStep / TutorialCompleted |
Method | void TutorialStep(string tutorialName, string stepName)void TutorialCompleted() |
Theo dõi các bước trong hướng dẫn người chơi mới (FTUE). |
ItemReceived / Screen / Click |
Method | void ItemReceived(string item)void Screen(string screen)void Click(string screen, string element) |
Theo dõi vật phẩm nhận được, màn hình đang xem và các nút bấm quan trọng. |
CurrencyIncome / CurrencyOutcome |
Method | void CurrencyIncome(CurrencyType currency, int amount, int total, string place = null)void CurrencyOutcome(CurrencyType currency, int amount, int total, string place = null) |
Ghi nhận biến động kinh tế: loại tiền (CurrencyType.Soft hoặc Hard), lượng thay đổi, số dư sau giao dịch và ngữ cảnh (place: shop, level_end...). |
Event |
Method | void Event(string name, long param1 = 0, long param2 = 0, string extra1 = null, string extra2 = null) |
Sự kiện tùy biến với 4 slot cố định (2 số long, 2 chuỗi). Ưu tiên dùng hàm này: đủ slot trên SayKit, Firebase và AppMetrica. GameAnalytics chỉ nhận một giá trị số value (= param1) và bỏ param2, extra1, extra2. |
LogEvent |
Method | void LogEvent(string eventName, IReadOnlyDictionary<string, object> parameters = null) |
Sự kiện với dictionary tham số tự do: đầy đủ trên Firebase/AppMetrica; GameAnalytics chỉ lấy tham số số tên value; SayKit chỉ giữ tham số số đầu tiên và chuỗi đầu tiên. |
SetUserProperty |
Method | void SetUserProperty(string name, string value) |
Gắn thuộc tính người dùng. Chỉ Firebase nhận (SayKit, GameAnalytics, AppMetrica bỏ qua) và không có bộ đệm: gọi trước khi Firebase sẵn sàng (hoặc khi Firebase bị tắt/thiếu cấu hình) thì giá trị bị bỏ, hãy gọi lại sau khi khởi tạo xong. |
1.4. App.RemoteConfig (IRemoteConfigService)
Đọc cấu hình động từ đám mây, hỗ trợ giá trị mặc định cục bộ và ánh xạ field của một class vào các key thông qua reflection (chỉ chạy khi Bind và khi có cập nhật). Trên SayKit, mỗi key phải có field cùng tên trong SayKitGameConfig mới nhận được giá trị từ server.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
IsFetched |
Property (get) | bool |
True khi lần tải gần nhất từ backend thành công. Trước đó giá trị đọc ra là giá trị mặc định cục bộ. |
Updated |
Event | event Action |
Bắn sau mỗi lần có dữ liệu từ backend (và sau khi các đối tượng đã Bind được cập nhật xong). Đăng ký sau lần tải đầu sẽ không nhận lại: dùng IsFetched. |
AddDefault |
Method | void AddDefault(string key, string value) |
Khai báo giá trị mặc định cho một key trước khi đọc hoặc trước khi tải. |
Fetch |
Method | IEnumerator Fetch() |
Bước boot do SDK chạy (có timeout). Game không gọi hàm này. |
GetString / GetLong / GetDouble / GetBool |
Method | string GetString(string key)long GetLong(string key, long fallback = 0)double GetDouble(string key, double fallback = 0)bool GetBool(string key, bool fallback = false) |
Thứ tự giá trị: giá trị từ backend (nếu có) → giá trị mặc định đã đăng ký (CoreConfig, AddDefault, Bind) → chuỗi rỗng. Tham số fallback chỉ được dùng khi key không có giá trị mặc định hoặc giá trị không parse được (parse theo InvariantCulture; NaN/Infinity bị coi là không parse được). |
Bind<T> |
Method | T Bind<T>(T target) where T : class |
Phương thức khuyên dùng: mỗi field public (không readonly) kiểu int, long, float, double, bool, string của target là một key (tên field = tên key, giá trị khởi tạo = giá trị mặc định), được điền ngay và điền lại mỗi lần có cập nhật. Chỉ nhận kiểu tham chiếu (class), không nhận struct. |
1.5. App.Dialogs (DialogService)
Quản lý ngăn xếp hộp thoại popup trên một Canvas bền vững (sortingOrder = 1000). Tự động đóng tất cả hộp thoại khi chuyển scene.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
IsAnyOpen |
Property (get) | bool |
True nếu có ít nhất một hộp thoại đang hiển thị (bỏ qua các hộp thoại đang trong hiệu ứng đóng). |
Top |
Property (get) | DialogBase |
Hộp thoại nằm ở vị trí cao nhất trên đỉnh ngăn xếp. |
DialogOpened / Closed |
Event | event Action<DialogBase> |
Bắn khi một hộp thoại bắt đầu mở ra hoặc khi đã hoàn tất đóng. |
Show<T> |
Method | T Show<T>(DialogShowMode mode = DialogShowMode.Stack) where T : DialogBase |
Nạp prefab từ Resources/Dialogs/<T>.prefab và mở. Các chế độ: Stack (đè lên trên), ReplaceTop (thay thế đỉnh), ReplaceAll (đóng hết rồi mở). Nếu T đã mở, tái sử dụng instance đó. |
IsOpen<T> |
Method | bool IsOpen<T>() where T : DialogBase |
Kiểm tra xem hộp thoại kiểu T có đang hiển thị hay không. |
CloseAll |
Method | void CloseAll() |
Đóng toàn bộ các hộp thoại đang có trong ngăn xếp. |
CreateQueue |
Method | DialogQueue CreateQueue() |
Tạo một hàng đợi hiển thị popup nối tiếp nhau theo thứ tự ưu tiên (Priority Queue). |
1.6. App.Scenes (SceneLoader)
Điều phối chuyển cảnh an toàn, tích hợp hiệu ứng làm tối màn hình (Fade) và báo cáo tiến độ nạp bất đồng bộ.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
IsLoading |
Property (get) | bool |
True khi đang có một tiến trình chuyển scene đang chạy. |
CurrentIndex |
Property (get) | int |
Build Index của scene đang kích hoạt (Active Scene). |
LoadingSceneIndex |
Constant | const int LoadingSceneIndex = 0 |
Quy ước bất biến: Scene số 0 luôn là scene Loading. |
SceneLoading |
Event | event Action<int> |
Kích hoạt trước khi bắt đầu fade out để đổi scene. |
SceneLoaded |
Event | event Action<int> |
Kích hoạt khi scene mới đã nạp xong và trước khi fade in mở màn hình. |
Load |
Method | void Load(int buildIndex, bool fade = true, Action<float> onProgress = null) |
Yêu cầu chuyển sang scene có chỉ số buildIndex. Nếu đang bận nạp scene khác, lệnh gọi này sẽ bị bỏ qua kèm log cảnh báo. |
1.7. App.Overlay (UIOverlay)
Lớp Canvas phủ trên cùng (sortingOrder = 30000) phục vụ làm tối màn hình, chặn tương tác và hiển thị thông báo nhanh (Toast).
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
FadeOut / FadeIn |
Method | IEnumerator FadeOut() / IEnumerator FadeIn() |
Coroutine làm tối màn hình dần về đen (FadeOut) hoặc làm sáng rõ trở lại (FadeIn). |
ShowBlocker / HideBlocker |
Method | void ShowBlocker() / void HideBlocker() |
Bật/tắt màn hình mờ chặn tương tác người dùng. Có cơ chế đếm tham chiếu (Reference Counting) để hỗ trợ các luồng lồng nhau an toàn. |
Toast |
Method | void Toast(string message, float seconds = 2f) |
Hiển thị thông điệp ngắn nổi lên màn hình trong số giây chỉ định. |
1.8. App.Audio (AudioService)
Quản lý 2 kênh âm thanh độc lập: Nhạc nền lặp lại (Music) và Hiệu ứng một lần (SFX). Tự động lưu trạng thái bật/tắt vào LocalStorage.
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
SoundOn |
Property (get/set) | bool |
Trạng thái bật/tắt hiệu ứng âm thanh SFX. |
MusicOn |
Property (get/set) | bool |
Trạng thái bật/tắt nhạc nền. |
PlaySfx |
Method | void PlaySfx(AudioClip clip, float volume = 1f) |
Phát âm thanh hiệu ứng một lần. Bỏ qua nếu SoundOn == false hoặc clip bị null. |
PlayMusic |
Method | void PlayMusic(AudioClip clip, float volume = 1f) |
Phát nhạc nền lặp lại. Nếu đang phát chính clip đó thì không ngắt quãng. |
StopMusic |
Method | void StopMusic() |
Dừng phát nhạc nền hiện tại. |
SetPausedByAd |
Method | void SetPausedByAd(bool paused) |
Tạm dừng toàn bộ âm thanh trò chơi khi quảng cáo toàn màn hình đang phát (tuân thủ chính sách App Store / Google Play). |
1.9. App.Haptic (HapticService)
Kích hoạt rung phản hồi xúc giác native trên iOS (UIFeedbackGenerator) và Android (Vibrator API 26+).
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
Enabled |
Property (get/set) | bool |
Bật hoặc tắt tính năng rung trên toàn ứng dụng. Tự động lưu trữ cấu hình. |
Play |
Method | void Play(HapticType type) |
Kích hoạt kiểu rung tương ứng: Selection, Light, Medium, Heavy, Success, Warning, Failure. |
1.10. App.Review (IReviewService)
Yêu cầu hộp thoại đánh giá ứng dụng native trong app (In-App Review).
| Thành viên | Loại | Ý nghĩa & Chữ ký hàm | Quy ước & Hành vi đặc biệt |
|---|---|---|---|
RequestReview |
Method | void RequestReview() |
Kích hoạt popup đánh giá native của Google Play hoặc iOS StoreKit. Trên SayKit, lệnh này sẽ mở popup đánh giá chuyên biệt của SayGames (giúp mở khóa chế độ quảng cáo đầy đủ). |
1.11. Dịch vụ hạ tầng mở rộng (ServiceLocator)
Các dịch vụ ít khi gọi hằng ngày được truy cập an toàn qua ServiceLocator.Get<T>() hoặc ServiceLocator.TryGet<T>():
CoreHost: MonoBehaviour duy nhất sống suốt vòng đời ứng dụng. Cung cấpRun(IEnumerator),Stop(Coroutine),Delay(seconds, Action), sự kiệnApplicationPaused,ApplicationQuitting,BackPressed(phím Back trên Android).UserIdentity: Thông tin định danh người chơi:UserId,IsNewUser,InstallDateUtc,FirstVersion,SessionCount,DaysSinceInstall.CoreConfig: Tệp cấu hình chứa toàn bộ các tham số và key của SDK (truy cập quaBootstrapper.Confighoặc ServiceLocator).AttService: Dịch vụ iOS Tracking:Status,IsAuthorized,RequestIfNeeded(),OpenAppSettings().AppUpdateService: Kiểm tra cập nhật ứng dụng mới:UpdateAvailable, sự kiệnChecked(bool),Check(),OpenStorePage(config),CompareVersions(a, b)(static).SearchAdsService: Thu thập thông tin nguồn gốc cài đặt từ Apple Search Ads:Cached(payload hoặc null),FetchOnce().- Các dịch vụ dành riêng cho MGW (bọc trong
#if !TB_SDK_SAYKIT):FirebaseService:IsAvailable,AuthUid.RealtimeDbService:IsReady,DataSet(debug/release),Child(path),SetJson(path, json),GetJson(path, callback).BIService:PushUserProfile(attStatus).NotificationService: Quản lý thông báo cục bộ:IsPermissionGranted, sự kiệnScheduleRequested(bắn khi app bị tạm dừng),Schedule(string title, string body, DateTime fireTimeLocal, string data = "")(chỉ có hiệu lực khi đã được cấp quyền và thời điểm ở tương lai),CancelAll().
2. Quy chuẩn mã nguồn game (Rules Your Game Code Must Follow)
Để đảm bảo mã nguồn trò chơi sạch sẽ, dễ bảo trì, đạt hiệu năng tối ưu và không làm hỏng tính cô lập của SDK, toàn bộ lập trình viên khi tích hợp SDK bắt buộc phải tuân thủ nghiêm ngặt các quy tắc kiến trúc dưới đây.
Quy tắc 1: Mã nguồn game hoàn toàn tách biệt khỏi Package SDK
Lý do: Package SDK được phân phối dưới dạng UPM package tải qua Git Tag hoặc đặt trong thư mục Packages. Mọi chỉnh sửa trực tiếp vào cache package sẽ bị xóa sạch khi Unity giải quyết lại dependencies.
// Adding game data classes directly into Packages/com.mgw.sdk/Runtime/...
namespace TapBrain.Core
{
public class MyPlayerInventory { } // Architecture error: pollutes the SDK package
}
// Game code lives under your own Assets/Scripts/... folder
namespace MyGame.Inventory
{
public class MyPlayerInventory { }
}
Quy tắc 2: Core SDK không bao giờ tham chiếu ngược mã Game
Lý do: SDK được thiết kế dùng chung cho nhiều trò chơi khác nhau. SDK chỉ tương tác với game thông qua các interface, delegate hoặc event (như PurchaseGrantHandler, IReceiptValidator).
// Core code calling a concrete game class directly
// (e.g. IapService reaching into the game's inventory): a serious violation.
// Core may only reach the game through interfaces, events and delegates.
// The game registers a delegate with the IAP service.
// Register once from a long-lived game object (not from a scene that gets destroyed).
public sealed class PlayerInventoryIap
{
private readonly PlayerInventory _inventory;
public PlayerInventoryIap(PlayerInventory inventory)
{
_inventory = inventory;
App.Iap.SetGrantHandler(OnPurchaseGranted);
}
private bool OnPurchaseGranted(string productId)
{
if (productId == "gold_pack_100")
{
_inventory.AddGold(100);
_inventory.SaveData();
return true; // reward delivered AND saved
}
return false; // unknown product: keep the order pending
}
}
Quy tắc 3: Sử dụng Facade App.X và ServiceLocator thay cho Singleton
Lý do: Chống phụ thuộc chặt vào các thể hiện tĩnh (static instances), không cần đặt các prefab "GameMaster" lê thê trong scene, hỗ trợ kiểm thử và thay thế linh hoạt. Chỉ Bootstrapper và hàm Install của module SDK được đăng ký dịch vụ của SDK; game chỉ đọc qua App.X hoặc ServiceLocator.Get<T>()/TryGet<T>().
// WRONG: hunting for the service in the scene
var ads = FindObjectOfType<MyAdsManager>();
ads.ShowInterstitial();
// Also wrong: a hand-written static global access point for an SDK service.
// Use the SDK facade
App.Ads.TryShowInterstitial("level_end", onClosed: () =>
{
GoToNextLevel();
});
Quy tắc 4: Chuyển Scene duy nhất qua App.Scenes.Load
Lý do: App.Scenes.Load tự động xử lý hiệu ứng mờ chuyển cảnh, đóng toàn bộ các dialog đang mở, chặn các lệnh gọi trùng lặp và tương thích với SceneLoader của SDK.
// Loading a scene directly with Unity's SceneManager
UnityEngine.SceneManagement.SceneManager.LoadScene(1);
// Open dialogs are not closed and there is no fade transition
// Always change scenes through App.Scenes
App.Scenes.Load(buildIndex: 1, fade: true, onProgress: p =>
{
loadingSlider.value = p;
});
Quy tắc 5: Cấu hình tập trung trong CoreConfig
Lý do: Tập trung toàn bộ key, thông số quảng cáo và tùy chọn bật tắt module vào một tài sản duy nhất (Resources/CoreConfig.asset), giúp việc kiểm tra của Build Guard diễn ra tức thì và tránh bị rò rỉ key ra code. Ngoại lệ: khóa của SayKit nằm trong Assets/SayKit/SayKitApp.cs và key GameAnalytics nằm trong cửa sổ GameAnalytics Settings.
// Hard-coding an ad unit id in C# and calling the network SDK directly
string adUnit = "ca-app-pub-1234567890/0987654321";
MaxSdk.ShowInterstitial(adUnit);
// Ad units are configured in CoreConfig.asset (MGW); game code only calls the facade
App.Ads.TryShowInterstitial("home_screen");
Quy tắc 6: Mọi Coroutine phải có điều kiện dừng rõ ràng
Lý do: Coroutine không có điểm kết thúc hoặc chạy vòng lặp vô tận mà không có cơ chế thoát sẽ gây rò rỉ bộ nhớ, treo ứng dụng khi đổi scene và tiêu tốn CPU.
IEnumerator PollServerRoutine()
{
while (true) // Dangerous: never ends, keeps running after the object is destroyed
{
yield return new WaitForSeconds(5f);
CheckStatus();
}
}
// Stop condition: ends on success, after the timeout, or when the object is destroyed
IEnumerator PollServerRoutine()
{
float timeout = 30f;
float started = Time.realtimeSinceStartup;
while (!isSuccess && Time.realtimeSinceStartup - started < timeout)
{
if (this == null) yield break;
yield return new WaitForSecondsRealtime(2f);
CheckStatus();
}
}
Quy tắc 7: Tuyệt đối không cấp phát bộ nhớ và tìm kiếm component trong Hot Paths
Lý do: Các hàm thực thi định kỳ như Update, FixedUpdate, LateUpdate (và các handler input) chạy mỗi khung hình. Sử dụng LINQ, tạo mảng/closure mới hoặc gọi GetComponent/FindObjectOfType tại đây sẽ gây GC pressure dẫn đến tụt khung hình (frame drops).
void Update()
{
// Very expensive on mobile: lookups and LINQ allocations every frame
var player = GetComponent<PlayerMotor>();
var enemies = FindObjectsOfType<Enemy>().Where(e => e.IsAlive).ToList();
}
private PlayerMotor _motor;
private readonly List<Enemy> _aliveEnemies = new List<Enemy>();
void Awake()
{
_motor = GetComponent<PlayerMotor>();
}
void Update()
{
// Reuse the cached reference and the preallocated list
_aliveEnemies.Clear();
EnemyManager.GetAliveEnemiesNonAlloc(_aliveEnemies);
}
Quy tắc 8: Phân định vùng lưu trữ LocalStorage với tiền tố game.
Lý do: LocalStorage của SDK là lớp bọc mỏng quanh PlayerPrefs của Unity. Mọi key của SDK có tiền tố core. (như core.sound_on, core.user_id, core.remove_ads); dữ liệu trò chơi dùng tiền tố game. để tách biệt rõ ràng, tránh trùng với key của SDK hoặc của thư viện khác dùng chung PlayerPrefs.
LocalStorage.SetInt("level", 5);
LocalStorage.SetBool("sound_on", false); // Unprefixed keys can collide with other PlayerPrefs users
LocalStorage.SetInt("game.player_level", 5);
LocalStorage.SetInt("game.coins", 1500);
Quy tắc 9: Ngôn ngữ chuẩn trong Code và Log
Lý do: Đảm bảo tính chuẩn hóa quốc tế của codebase, dễ dàng tích hợp các công cụ phân tích tự động (CI/CD, log aggregators, Crashlytics) và chia sẻ kỹ thuật.
- Toàn bộ tên biến, tên hàm, class, chú thích code (comments) và chuỗi log phải viết bằng tiếng Anh.
- Tài liệu hướng dẫn, giải trình kiến trúc và trao đổi với đồng đội viết bằng tiếng Việt có dấu.
Quy tắc 10: Chuyển đổi Provider đồng bộ qua công cụ của SDK
Lý do: Mỗi provider (MGW hoặc SayKit) gắn liền với một bộ thư viện native, package và Scripting Defines riêng. Không thể chuyển đổi bằng cách sửa tay define hoặc manifest.json.
- Provider chỉ được đổi bằng menu TapBrain > SDK > Switch To ... (hoặc nút
Switch To ...trong cửa sổ TapBrain > SDK Settings...). Cửa sổ SDK Settings tự nó chỉ bật/tắt module của provider hiện tại (GameAnalytics/AppMetrica trên MGW, SayKit push trên SayKit). - Mọi lệnh gọi SDK bên thứ ba (
MaxSdk.*,Firebase.*,SayKit.*) phải đi quaApp.X; dịch vụ chỉ có trên MGW phải bọc trong#if !TB_SDK_SAYKIT.
3. Bảng thuật ngữ cốt lõi (Glossary)
| Thuật ngữ | Tên chuyên môn | Định nghĩa & Ý nghĩa trong kiến trúc SDK |
|---|---|---|
| Provider | SDK Provider | Nhà cung cấp hạ tầng SDK toàn diện của bản build: hoặc là MGW (Firebase + AppLovin MAX) hoặc là SayKit (SayGames). Trong một bản build chỉ tồn tại duy nhất 1 provider (hiện thực giao diện ISdkProvider). |
| Module / Extension | SDK Extension Module | Các thành phần bổ sung độc lập hiện thực ISdkModule (ví dụ: GameAnalytics, AppMetrica). Chỉ hoạt động song hành cùng MGW Provider. |
| Facade | Facade Pattern | Mẫu thiết kế cung cấp giao diện tĩnh tiện lợi (App.X) cho các dịch vụ cốt lõi, giúp che giấu độ phức tạp của ServiceLocator phía dưới. |
| Boot Pipeline | InitPipeline | Hệ thống điều phối các bước khởi động tuần tự có giới hạn thời gian (time-boxed) cho các dịch vụ trong SDK trước khi chuyển vào scene chính của game. |
| Grant Handler | PurchaseGrantHandler | Delegate do game cung cấp cho hệ thống IAP để thực hiện việc cộng vật phẩm, mở khóa tính năng và lưu dữ liệu khi một hóa đơn mua hàng được xác thực hợp lệ. |
| Pending Order Ledger | PendingOrderStore | Cơ chế lưu trữ tạm thời các hóa đơn mua hàng đã thanh toán ở Store nhưng chưa được game xác nhận trao thưởng thành công, giúp tự động trao bù ở phiên chơi sau. |
| Tangles | Receipt Obfuscation Tangles | Các file mã nguồn C# chứa các khóa mã hóa bảo mật bị làm mờ (GooglePlayTangle, AppleTangle) do Unity IAP sinh ra để xác thực biên lai cục bộ trên thiết bị mà không cần máy chủ riêng. |
| Build Guard | Build Preprocessor Guard | Cơ chế bảo vệ tự động (CoreConfigTools.OnPreprocessBuild) chạy trước khi đóng gói: chặn mọi bản build khi không có provider hoặc khi cấu hình SayKit sai; chặn Release Build khi CoreConfig hoặc SayKitApp còn key giữ chỗ hay thiếu cấu hình Firebase (Development Build chỉ cảnh báo phần placeholder). |
| Placement | Ad Placement | Chuỗi ký tự định danh ngữ cảnh hoặc vị trí gọi quảng cáo trong game (ví dụ: "level_end", "revive", "shop_free_coins"), được truyền cho MAX/SayKit để báo cáo và (trên SayKit) làm place để server giới hạn tần suất. |
| Sink | Analytics Sink | Thành phần tiếp nhận và chuyển tiếp các sự kiện đo lường từ AnalyticsService tới một mạng phân tích dữ liệu cụ thể (hiện thực IAnalyticsSink). |
4. Câu hỏi thường gặp (Frequently Asked Questions - FAQ)
1. Tôi có thể sử dụng đồng thời cả 2 provider MGW và SayKit trong cùng một bản build không?
Không thể. Hai provider MGW và SayKit là loại trừ lẫn nhau hoàn toàn (mutually exclusive): asmdef của MGW có điều kiện !TB_SDK_SAYKIT còn asmdef của SayKit cần TB_SDK_SAYKIT, nên mỗi bản build chỉ biên dịch một provider. Nếu SayKit đang bật mà dự án còn package của MGW (AppLovin MAX, Firebase, EDM, GameAnalytics, AppMetrica), Build Guard chặn mọi bản build. Thông điệp [Sdk] Two SDK providers compiled in ... là log lỗi lúc chạy (không phải do Build Guard) và provider thứ hai bị bỏ qua; để đổi provider hãy dùng TapBrain > SDK > Switch To ....
2. Tại sao quảng cáo không hiển thị khi tôi test trong Unity Editor?
Quảng cáo thật chỉ được hiển thị và kiểm chứng đầy đủ trên thiết bị Android/iOS. Trong Editor với MGW, App.Ads là MaxAdsService: khi MAX SDK key chưa điền hoặc enableAds tắt, nó không khởi tạo (IsInitialized == false), TryShowInterstitial trả về false (và gọi onClosed ngay) còn ShowRewarded trả NotReady. NullAdsService chỉ được dùng khi không có provider nào được biên dịch. Khi đã có key, hành vi trong Editor phụ thuộc package MAX/SayKit, vì vậy hãy luôn nghiệm thu quảng cáo trên thiết bị thật.
3. Tôi cần điền các mã Ad Unit ID ở đâu?
Với MGW, toàn bộ Ad Unit ID được quản lý trong file CoreConfig.asset. Mở bằng menu TapBrain > Select Core Config, sau đó điền vào các trường Max Sdk Key, Banner Ad Unit, Interstitial Ad Unit, Rewarded Ad Unit (và App Open Ad Unit tùy chọn), phân tách riêng cho Android và iOS. MAX SDK Key còn cần được khai báo trong AppLovin > Integration Manager. Với SayKit, khóa nằm trong Assets/SayKit/SayKitApp.cs, không dùng ad unit của CoreConfig.
4. Làm thế nào để tắt bớt một module (ví dụ không dùng Firebase hoặc GameAnalytics)?
Có 2 cấp độ tắt:
1. Tắt runtime: Bỏ dấu tích tại các trường enableFirebase, enableAds, enableGameAnalytics, enableAppMetrica... trong CoreConfig.asset. Khi tắt, các bước khởi động tương ứng của module đã biên dịch trở thành no-op (chạy rỗng và kết thúc ngay). Với MGW, tắt enableAds chỉ làm MAX không khởi tạo (App.Ads vẫn là MaxAdsService với IsInitialized == false); SayKit bỏ qua enableAds.
2. Gỡ bỏ lúc biên dịch (Compile-time): Mở menu TapBrain > SDK Settings... và bỏ chọn các module tùy chọn như GameAnalytics hoặc AppMetrica. Unity sẽ loại bỏ define và không biên dịch các assembly đó.
5. Điều gì xảy ra khi thiết bị người chơi ở trạng thái Offline (không có mạng)?
Game vẫn vào được màn hình chính. InitPipeline giới hạn thời gian cho từng bước (Firebase, Remote Config, Ads, IAP...); khi hết timeout, pipeline bỏ qua bước đó và đi tiếp (thời gian ở màn hình Loading có thể kéo dài theo tổng các timeout). Remote Config dùng các giá trị mặc định cục bộ; các bước đang chờ có thể hoàn tất ngầm sau đó và bắn sự kiện (Updated, Initialized).
6. Tôi có thể thay đổi các giá trị mặc định của Remote Config không?
Có. Bạn có thể sửa danh sách mặc định trong CoreConfig.asset tại mục Remote Config Defaults, đăng ký trong mã khởi tạo game bằng App.RemoteConfig.AddDefault("my_key", "default_val"), hoặc tiện lợi nhất là khai báo giá trị khởi tạo cho các field public của một class khi dùng App.RemoteConfig.Bind(new MyConfigData()) (Bind không nhận struct). Tham số fallback của GetLong/GetDouble/GetBool chỉ có tác dụng khi key không có giá trị mặc định hoặc giá trị không parse được. Trên SayKit, key còn cần một field cùng tên trong SayKitGameConfig thì mới nhận giá trị từ server.
7. Làm thế nào để kiểm thử mua hàng In-App Purchases mà không tốn tiền thật?
Trong Unity Editor, hệ thống IAP tự động kích hoạt Fake Store của Unity. Khi bạn bấm nút mua, một popup giả lập của Unity sẽ hiện lên cho phép bạn chọn kết quả thành công hoặc thất bại. Trên thiết bị thật, hãy sử dụng tài khoản Sandbox Test (trên iOS App Store Connect) hoặc License Testing (trên Google Play Console).
8. Tại sao khi bấm Play trực tiếp ở scene Home hoặc Main trong Editor thì game mở ngay mà không có màn hình chờ?
Vì component LoadingScreen chỉ nằm ở scene số 0 (Loading scene). Khi bạn bấm Play ở scene khác, Bootstrapper.Boot() vẫn chạy ngầm nhưng scene hiện tại sẽ nạp ngay lập tức. Do đó, các dịch vụ bất đồng bộ (IAP, Remote Config) có thể chưa xong khi Start() của các script trong scene chạy. Để có trải nghiệm sát với thực tế nhất, luôn bấm Play từ scene Loading.
9. App.Ads.TryShowInterstitial xử lý giãn cách (cooldown) như thế nào?
TryShowInterstitial trên MGW tuân theo AdsPolicy (SayKit không dùng AdsPolicy vì tự giới hạn tần suất ở server):
- Nếu chưa đủ inter_first_delay_sec (mặc định 60s) từ lúc Core khởi động: từ chối, trả về false (lý do first_delay chỉ xuất hiện trong log).
- Nếu chưa đủ inter_interval_sec (mặc định 30s) từ lần đóng quảng cáo toàn màn hình gần nhất (kể cả rewarded): từ chối (lý do interval).
- Nếu người chơi đã mua gói Xóa quảng cáo: từ chối (lý do remove_ads).
Trong mọi trường hợp bị từ chối, callback onClosed được gọi đồng bộ, ngay trước khi hàm trả về false, để game không bị nghẽn luồng.
10. Khi nào PurchaseGrantHandler cần trả về true và khi nào trả về false?
Hàm handler nhận vào productId và chỉ được trả về true khi game của bạn đã thực sự cộng vật phẩm/tiền tệ cho người chơi và lưu thành công xuống bộ nhớ. Nếu trả về false (hoặc ném exception), SDK sẽ giữ hóa đơn đó trong sổ cái PendingOrderStore và không xác nhận với Store, giúp giao dịch tự động được gửi lại để trao quà ở lần mở game tiếp theo.
11. Tại sao giao dịch mua IAP của tôi lại ở trạng thái Pending?
Trạng thái Pending xảy ra khi:
1. Store đang chờ phê duyệt thanh toán (ví dụ: giao dịch Ask to Buy của tài khoản trẻ em trên iOS, hoặc thanh toán qua ví điện tử chậm).
2. Game của bạn chưa đăng ký App.Iap.SetGrantHandler nên SDK tạm giữ hóa đơn lại để chờ đăng ký.
3. Handler trả về false vì mã sản phẩm chưa được khai báo xử lý trong switch-case của game.
12. Sự khác biệt giữa App.Analytics.Event và App.Analytics.LogEvent là gì?
App.Analytics.Event(name, param1, param2, extra1, extra2) sử dụng 4 slot cố định (2 số long và 2 chuỗi), đủ dữ liệu trên SayKit, Firebase và AppMetrica (GameAnalytics chỉ nhận một giá trị số value = param1). Ngược lại, LogEvent(name, dictionary) nhận dictionary tham số tùy biến, đầy đủ trên Firebase/AppMetrica; trên SayKit chỉ giữ số đầu tiên và chuỗi đầu tiên, còn GameAnalytics chỉ lấy tham số số tên value.
13. Tại sao Firebase tự động vô hiệu hóa khi chạy mà chưa có file cấu hình google-services.json?
Đây là cơ chế tự bảo vệ của FirebaseService (chỉ có trên MGW): khi không tạo được Firebase app do thiếu hoặc sai file cấu hình, SDK đặt cờ IsAvailable = false và in log lỗi [Firebase] App creation failed, Firebase disabled for this session... (mức Error nhưng bình thường khi chưa có key). Các dịch vụ phụ thuộc Firebase (Auth, Remote Config, RTDB) tự bỏ qua bước kết nối; Remote Config trả về giá trị mặc định cục bộ và RTDB giữ IsReady == false, giúp lập trình viên phát triển logic mà không bị crash.
14. Mã game của tôi có thể tự thêm một bước boot vào InitPipeline được không?
Không nên và không được phép. InitPipeline là thành phần nội bộ (internal) của SDK dành riêng cho việc khởi tạo các thư viện SDK bên thứ ba. Toàn bộ logic khởi tạo của game của bạn nên được đặt trong các script tại scene (ví dụ hàm Start() của scene đầu tiên sau Loading), hoặc đăng ký qua các sự kiện sẵn sàng như App.Iap.Ready và App.RemoteConfig.Updated.
15. Tại sao không được đặt FindObjectOfType hoặc GetComponent bên trong hàm Update?
FindObjectOfType phải duyệt qua các đối tượng trong Scene nên rất tốn CPU khi gọi mỗi khung hình. GetComponent gọi liên tục cũng tiêu tốn CPU không cần thiết (và trong Editor có thể cấp phát khi không tìm thấy component). Hãy tìm và lưu tạm (cache) biến tham chiếu trong các hàm khởi tạo sớm như Awake() hoặc Start().
16. Chuyện gì xảy ra nếu một module SDK gặp lỗi ném ngoại lệ trong quá trình boot?
Bootstrapper bọc các lệnh gọi module (Install, AddBootSteps, AddLateBootSteps) trong TryModuleCall. Nếu một module ném exception, SDK in log lỗi [Boot] SDK module <Name> failed in <phase>, skipped: ... và tiếp tục boot. Lỗi ở Install loại module (hoặc provider) khỏi phiên chạy; lỗi ở AddBootSteps/AddLateBootSteps chỉ làm mất các bước của lần gọi đó. Tiến trình khởi động vẫn tiếp tục để game không bị treo ở màn hình Loading.
17. Gói Xóa quảng cáo (Remove Ads) trong SDK hoạt động như thế nào?
Khi bạn khai báo Product ID trong trường CoreConfig.removeAdsProductId và người chơi mua gói này thành công (hoặc khôi phục), SDK sẽ tự động:
1. Lưu cờ sở hữu vào LocalStorage (core.remove_ads).
2. Bắn sự kiện App.Iap.RemoveAdsGranted.
3. Trên MGW, chặn vĩnh viễn việc hiển thị Banner, Interstitial và App Open Ads; trên SayKit, gọi SayKit.enablePremium() (dừng banner, interstitial và cross-promo).
4. Rewarded Ads (quảng cáo nhận thưởng khi bấm nút) vẫn tiếp tục hoạt động bình thường.
Game đọc trạng thái qua App.Iap.IsOwned(removeAdsProductId); AdsPolicy.IsAdsRemoved là internal nên game không gọi được.
18. Làm sao để lập lịch thông báo cục bộ (Local Notifications)?
Trên stack MGW, lấy dịch vụ thông báo qua ServiceLocator.TryGet<NotificationService>(out var notifications) (bọc trong #if !TB_SDK_SAYKIT, cùng using TapBrain.Sdk.Mgw;). Dịch vụ bắn sự kiện ScheduleRequested mỗi khi người chơi tạm thoát game (pause/minimize): hãy lên lịch tại đó bằng notifications.Schedule("Tên thông báo", "Nội dung nhắc nhở", DateTime.Now.AddHours(4)). Lịch chỉ có hiệu lực khi người chơi đã cấp quyền (IsPermissionGranted) và CoreConfig.enableNotifications bật; SDK hủy mọi thông báo đã lên lịch khi app mở lại.
5. Checklist kiểm tra nhanh quy chuẩn
Assets/Scripts/....App.Ads, App.Iap, App.Analytics, App.Scenes, App.Dialogs...App.Iap.SetGrantHandler và chỉ trả về true khi đã lưu thành công dữ liệu.game. (ví dụ: game.high_score).GetComponent trong các hàm Update hoặc FixedUpdate.