Chương 5: Analytics (App.Analytics)
Hướng dẫn toàn diện về dịch vụ thu thập và phân tích dữ liệu người chơi qua App.Analytics (giao diện IAnalyticsService). Tìm hiểu mô hình Fan-out Hub chuyển phát sự kiện đa luồng, cơ chế tự động đo lường thời gian chơi, bảng ánh xạ chuẩn hoá sang Firebase Analytics, GameAnalytics, AppMetrica và SayKit.
1. Mục đích & Mô hình kiến trúc (Mental Model)
Trong phát triển game di động thương mại, việc chuyển đổi qua lại giữa các nhà phát hành (Publishers) hoặc thay đổi công cụ phân tích (Firebase, GameAnalytics, AppMetrica, SayKit) thường dẫn đến việc phải viết lại toàn bộ mã nguồn theo dõi dữ liệu. MGW SDK giải quyết triệt để vấn đề này thông qua kiến trúc Fan-out Hub và Ngôn ngữ sự kiện thống nhất:
- Một điểm gọi duy nhất (Single Point of Tracking): Mã nguồn game chỉ tương tác với duy nhất Facade
App.Analytics(trả vềIAnalyticsServicethuộc namespaceTapBrain.Core.Services). Lập trình viên không bao giờ gọi trực tiếp SDK của bên thứ ba trong logic game. - Trung tâm điều phối Fan-out Hub: Bên dưới Facade, lớp nội bộ
AnalyticsServicegiữ vai trò trung tâm phân phối (Hub). Khi một sự kiện được kích hoạt, Hub sẽ đóng gói thành cấu trúcTrackEventvà chuyển lần lượt (đồng bộ, trên luồng chính) tới các bộ thu nhận (Sinks) đã sẵn sàng; sink chưa sẵn sàng bị bỏ qua. - Cầu nối tương thích ngữ nghĩa (Tracking Bridge):
- Provider SayKit: Lớp
SayKitAnalyticsSinkhiện thực giao diệnITrackingSink, tiếp nhận trực tiếpTrackEventvà chuyển thẳng tới các hàm gốc nhưSayKit.trackLevelStarted,SayKit.trackSoftIncome... - Provider MGW: Các Sink như
FirebaseAnalyticsSink,GameAnalyticsSink,AppMetricaSinktiếp nhận tên sự kiện generic và từ điển tham số đã được chuẩn hoá bởi lớpTrackEventMapper.ToGeneric().
- Provider SayKit: Lớp
Hệ thống danh mục sự kiện của MGW SDK được chuẩn hoá theo quy ước số màn chơi bắt đầu từ 1 (dành cho người chơi nhìn thấy). Thứ tự khuyến nghị cho một lượt chơi: LevelStarted → (các StageStarted/StageCompleted nếu có) → LevelCompleted hoặc LevelFailed. SDK không ép thứ tự này: nếu thiếu LevelStarted, sự kiện kết thúc vẫn được gửi nhưng thiếu duration_sec, còn sự kiện stage mang level = 0. Hãy gọi LevelStarted cho mỗi lượt chơi, kể cả khi chơi lại (retry).
2. Quy trình tích hợp từng bước
Bước 1: Theo dõi vòng đời màn chơi (Levels & Stages)
Khi bắt đầu một màn chơi mới, gọi App.Analytics.LevelStarted(level, tag). Khi người chơi hoàn thành hoặc thất bại, gọi LevelCompleted hoặc LevelFailed. Trên các sink của Provider MGW, SDK tự tính thời lượng chơi (giây) từ mốc bắt đầu và đính kèm vào sự kiện kết thúc dưới dạng duration_sec.
using TapBrain.Core;
using UnityEngine;
namespace MyGame.Gameplay
{
public class LevelCoordinator : MonoBehaviour
{
public void StartNewLevel(int levelNumber)
{
// Start the level (level numbers start at 1)
App.Analytics.LevelStarted(levelNumber, tag: "campaign");
}
public void FinishLevel(int levelNumber, int earnedScore, bool isVictory)
{
if (isVictory)
{
// On MGW sinks duration_sec is measured from LevelStarted and attached automatically
App.Analytics.LevelCompleted(levelNumber, score: earnedScore, tag: "campaign");
}
else
{
App.Analytics.LevelFailed(levelNumber, score: earnedScore, tag: "campaign");
}
}
}
}
Bước 2: Theo dõi biến động kinh tế ảo (Virtual Currency)
Bất cứ khi nào số dư tiền tệ của người chơi thay đổi (nhận thưởng hoàn thành màn chơi, xem quảng cáo, mua lượt chơi hoặc nâng cấp trang bị), hãy ghi nhận bằng CurrencyIncome hoặc CurrencyOutcome. Phân loại tiền tệ sử dụng enum CurrencyType.Soft (tiền vàng/bạc kiếm được trong game) hoặc CurrencyType.Hard (kim cương/ngọc quý mua bằng tiền thật).
using TapBrain.Core;
using TapBrain.Core.Services;
namespace MyGame.Economy
{
public class BankManager
{
private int _goldBalance = 500;
public void AddReward(int amount, string placement)
{
_goldBalance += amount;
App.Analytics.CurrencyIncome(CurrencyType.Soft, amount, _goldBalance, place: placement);
}
public bool SpendGold(int cost, string itemKey)
{
if (_goldBalance < cost) return false;
_goldBalance -= cost;
App.Analytics.CurrencyOutcome(CurrencyType.Soft, cost, _goldBalance, place: $"shop_{itemKey}");
return true;
}
}
}
Bước 3: Gửi sự kiện tuỳ chỉnh (Custom Event)
Khi cần ghi nhận các hành vi đặc thù của game không nằm trong các hàm dựng sẵn, bạn có hai lựa chọn:
- Khuyên dùng:
App.Analytics.Event(name, param1, param2, extra1, extra2)— Thiết kế theo cấu trúc vị trí cố định (Fixed-slot architecture). Cả 4 slot được gửi đủ lên Firebase, AppMetrica và SayKit. GameAnalytics chỉ nhậnparam1(dưới khoávalue, khi khác 0);param2,extra1,extra2bị bỏ trên GameAnalytics. Trên MGW, slot bằng 0 hoặc rỗng không được gửi. - Linh hoạt:
App.Analytics.LogEvent(eventName, parameters)— Nhận từ điển tự doIReadOnlyDictionary<string, object>. Lưu ý: trên SayKit chỉ tham số số đầu tiên và chuỗi đầu tiên được giữ lại; trên GameAnalytics chỉ tham số có khoá"value"(kiểu số) được trích xuất.LogEventkhông đi qua cầu nốiTrackEventvà không kiểm tra tên sự kiện.
3. Bảng tra cứu toàn bộ API (App.Analytics)
Tất cả các phương thức dưới đây đều thuộc giao diện IAnalyticsService (namespace TapBrain.Core.Services), được truy xuất thông qua Facade tĩnh App.Analytics:
| Chữ ký phương thức | Tham số & Giá trị mặc định | Mô tả chức năng & Quy tắc sử dụng |
|---|---|---|
void LevelStarted(int level, string tag = null) |
level: Thứ tự màn (bắt đầu từ 1)tag: Nhãn phân loại chế độ chơi (tuỳ chọn) |
Ghi nhận bắt đầu màn chơi chính. Lưu lại mốc thời gian thực để đo thời lượng chơi khi kết thúc. |
void LevelCompleted(int level, long score = 0, string tag = null) |
level: Số màn chơiscore: Điểm đạt được (mặc định 0)tag: Nhãn phân loại |
Ghi nhận người chơi vượt qua màn chơi. Trên sink MGW, tự đính kèm duration_sec (nếu đã gọi LevelStarted) và xoá mốc bắt đầu. |
void LevelFailed(int level, long score = 0, string tag = null) |
level: Số màn chơiscore: Điểm số tại lúc thuatag: Nhãn phân loại |
Ghi nhận thất bại trong màn chơi. Trên sink MGW, tự đính kèm duration_sec (nếu đã gọi LevelStarted) và xoá mốc bắt đầu, nên chơi lại cần gọi LevelStarted lần nữa. |
void StageStarted(int number, string tag = null) |
number: Thứ tự vòng/chặng trong level (1, 2, 3...)tag: Nhãn phân loại |
Bắt đầu một chặng con trong level hiện tại. Tự động lấy số level từ lần gọi LevelStarted gần nhất. |
void StageCompleted(int number, long score = 0, string tag = null) |
number: Số chặngscore: Điểm chặngtag: Nhãn phân loại |
Hoàn thành chặng con. Trên sink MGW, duration_sec của chặng chỉ có khi đã gọi StageStarted; số level lấy từ lần LevelStarted gần nhất (0 nếu chưa có). |
void StageFailed(int number, long score = 0, string tag = null) |
number: Số chặngscore: Điểm sốtag: Nhãn phân loại |
Thất bại tại chặng con. Tự động ghi nhận thời lượng chặng và số level cha. |
void ExtraLevelStarted(string tag = null) |
tag: Tên/nhãn màn đặc biệt (bonus, boss, minigame) |
Theo dõi màn chơi phụ hoặc màn thưởng ngoài luồng chính. Không làm tăng bộ đếm level người chơi. |
void ExtraLevelCompleted(long score = 0, string tag = null) |
score: Điểm thưởng đạt đượctag: Nhãn màn đặc biệt |
Ghi nhận hoàn thành màn chơi phụ/màn thưởng. |
void ExtraLevelFailed(long score = 0, string tag = null) |
score: Điểm số khi thuatag: Nhãn màn đặc biệt |
Ghi nhận thất bại tại màn chơi phụ. |
void TutorialStep(string tutorialName, string stepName) |
tutorialName: Tên chuỗi hướng dẫnstepName: Bước cụ thể (ví dụ: "drag_card") |
Theo dõi từng bước của luồng hướng dẫn tân thủ nhằm phân tích tỉ lệ rơi rụng (Drop-off rate). |
void TutorialCompleted() |
Không có tham số | Ghi nhận người chơi đã hoàn thành toàn bộ chuỗi hướng dẫn cơ bản ban đầu. |
void ItemReceived(string item) |
item: Tên hoặc định danh vật phẩm nhận được |
Ghi nhận người chơi sở hữu vật phẩm, trang bị, thẻ bài hoặc skin mới. |
void Screen(string screen) |
screen: Tên màn hình/giao diện mở ra |
Ghi nhận chuyển hướng màn hình (UI Screen View). Ánh xạ thành sự kiện chuẩn screen_view. |
void Click(string screen, string element) |
screen: Màn hình hiện tạielement: Tên nút hoặc đối tượng được tương tác |
Ghi nhận hành vi click chuột hoặc chạm nút UI cụ thể trên màn hình. |
void CurrencyIncome(CurrencyType currency, int amount, int total, string place = null) |
currency: Loại tiền tệ (Soft / Hard)amount: Lượng tiền nhận thêm (> 0)total: Tổng số dư ví sau khi cộngplace: Vị trí/nguồn cấp (tuỳ chọn) |
Ghi nhận nguồn thu tiền tệ ảo. Ánh xạ thành earn_virtual_currency với khoá virtual_currency_name. |
void CurrencyOutcome(CurrencyType currency, int amount, int total, string place = null) |
currency: Loại tiền tệ (Soft / Hard)amount: Lượng tiền tiêu trừ (> 0)total: Tổng số dư ví sau khi trừplace: Lý do/mục chi tiêu |
Ghi nhận chi tiêu tiền tệ ảo. Ánh xạ thành spend_virtual_currency với đầy đủ số dư và nguồn tiêu. |
void Event(string name, long param1 = 0, long param2 = 0, string extra1 = null, string extra2 = null) |
name: Tên sự kiệnparam1: Giá trị số 1 (gán vào khoá value)param2: Giá trị số 2extra1, extra2: Tham số chuỗi bổ sung |
Sự kiện tuỳ chỉnh theo 4 slot cố định của SayKit. Gửi đủ 4 slot lên Firebase, AppMetrica và SayKit; GameAnalytics chỉ nhận param1 (khoá value). Trên MGW, slot bằng 0 hoặc rỗng bị bỏ. Tên rỗng/null bị bỏ kèm cảnh báo. |
void LogEvent(string eventName, IReadOnlyDictionary<string, object> parameters = null) |
eventName: Tên sự kiệnparameters: Từ điển tham số tự do (tuỳ chọn) |
Gửi sự kiện với cấu trúc tham số tự do. Firebase nhận đủ khoá; AppMetrica nhận JSON tự dựng; GameAnalytics chỉ nhận khoá value kiểu số; SayKit chỉ giữ 1 số và 1 chuỗi đầu tiên. Không kiểm tra tên sự kiện (tên rỗng vẫn được chuyển tới sink). |
void SetUserProperty(string name, string value) |
name: Tên thuộc tínhvalue: Giá trị thuộc tính người dùng |
Gán thuộc tính người dùng (ví dụ: cấp VIP). Chỉ Firebase Analytics hỗ trợ; GameAnalytics, AppMetrica và SayKit bỏ qua. Lệnh gọi trước khi Firebase sẵn sàng bị mất (không được đệm): chỉ gọi sau khi FirebaseService.IsAvailable là true hoặc sau khi boot xong. |
4. Bảng ánh xạ sự kiện chi tiết (TrackEventMapper & Generic Events)
Khi chạy trên Provider MGW, các lệnh gọi từ App.Analytics được lớp TrackEventMapper chuyển đổi sang định dạng sự kiện Generic (tương thích GA4 / Firebase) trước khi chuyển phát đến Firebase Analytics, GameAnalytics và AppMetrica. Lưu ý: bảng dưới là tập tham số mà Firebase và AppMetrica nhận; GameAnalytics chỉ nhận tên sự kiện kèm tham số số value (nếu có):
| Lệnh gọi từ App.Analytics | Tên sự kiện Generic | Tham số đính kèm (chỉ tag/place bị bỏ khi rỗng; duration_sec bị bỏ khi thiếu mốc start; slot của Event bị bỏ khi 0/rỗng) |
|---|---|---|
LevelStarted |
level_start |
level (int), tag (string) |
LevelCompleted |
level_complete |
level (int), score (long), duration_sec (int, nếu có mốc start), tag (string) |
LevelFailed |
level_fail |
level (int), score (long), duration_sec (int, nếu có mốc start), tag (string) |
StageStarted |
stage_start |
level (int - tự điền từ level hiện tại), stage (int), tag (string) |
StageCompleted |
stage_complete |
level (int), stage (int), score (long), duration_sec (int, nếu đã gọi StageStarted), tag (string) |
StageFailed |
stage_fail |
level (int), stage (int), score (long), duration_sec (int, nếu đã gọi StageStarted), tag (string) |
ExtraLevelStarted |
extra_level_start |
tag (string) |
ExtraLevelCompleted |
extra_level_complete |
score (long), tag (string) |
ExtraLevelFailed |
extra_level_fail |
score (long), tag (string) |
TutorialStep |
tutorial_step |
tutorial (string), step (string) |
TutorialCompleted |
tutorial_complete |
Không có tham số (tương thích GA4 chuẩn) |
ItemReceived |
item_received |
item (string) |
Screen |
screen_view |
screen_name (string) |
Click |
ui_click |
screen (string), element (string) |
CurrencyIncome |
earn_virtual_currency |
virtual_currency_name ("soft" hoặc "hard"), value (long), total (long), place (string) |
CurrencyOutcome |
spend_virtual_currency |
virtual_currency_name ("soft" hoặc "hard"), value (long), total (long), place (string) |
Event(name, ...) |
name |
value (long - từ param1 nếu ≠ 0), param2 (long nếu ≠ 0), extra1 (string, nếu không rỗng), extra2 (string, nếu không rỗng) |
GameAnalyticsSink gửi mọi sự kiện dưới dạng Design Event với duy nhất tham số số value (nếu có và là số). Vì vậy level_start, level_complete, stage_*, screen_view... đến GameAnalytics chỉ còn tên sự kiện; level, score, stage, tag, duration_sec không được gửi. Riêng earn_virtual_currency/spend_virtual_currency mang value = số lượng tiền (amount), và Event(...) mang value = param1.
Cơ chế tự động đo duration_sec và kế thừa level
Trong AnalyticsService, biến nội bộ _currentLevel ghi nhớ màn chơi đang hoạt động khi LevelStarted được gọi. Mốc thời gian thực _levelStartedAt = Time.realtimeSinceStartup cũng được kích hoạt.
- Khi gọi
StageStarted(number), SDK tự gán tham sốlevel = _currentLevelmà không yêu cầu bạn phải truyền lại. Đồng thời kích hoạt bộ đếm thời gian riêng_stageStartedAtcho chặng. - Khi kết thúc màn chơi qua
LevelCompletedhoặcLevelFailed, SDK tính toán:
Nếu chưa gọiint seconds = Mathf.RoundToInt(Time.realtimeSinceStartup - _levelStartedAt); _levelStartedAt = -1f; // Reset after useLevelStarted(hoặc mốc đã bị xoá bởi lần kết thúc trước đó, ví dụ chơi lại mà không gọiLevelStarted), thời gian là-1và tham sốduration_secbị lược bỏ hoàn toàn. Cơ chế tương tự áp dụng choStageStarted→StageCompleted/StageFailedqua mốc riêng của chặng. duration_secvà việc tự điềnlevelcho sự kiện stage nằm trong bản ánh xạ generic nên chỉ tác động tới các sink MGW (Firebase, AppMetrica; GameAnalytics không nhận tham số này). Sink SayKit nhận lệnh gọi gốc (SayKit.trackLevelCompleted...) và không được SDK truyềnduration_sec.
Sự khác biệt cốt lõi: Event vs LogEvent
| Tiêu chí | App.Analytics.Event(...) |
App.Analytics.LogEvent(...) |
|---|---|---|
| Cấu trúc tham số | Cố định: 2 số nguyên (param1, param2) và 2 chuỗi (extra1, extra2). |
Tự do: Nhận từ điển IReadOnlyDictionary<string, object> tuỳ ý. |
| Hành vi trên SayKit | Đủ 4 slot: Gọi SayKit.trackEvent với overload nhỏ nhất vẫn giữ mọi slot khác 0/rỗng (name, param1, param2, extra1, extra2). |
Bị cắt giảm: Duyệt từ điển theo thứ tự liệt kê, chỉ lấy giá trị số đầu tiên làm param1 (bool thành 0/1, float/double làm tròn thành số nguyên) và chuỗi đầu tiên làm extra1. param2, extra2 luôn trống; các tham số còn lại bị huỷ bỏ! |
| Hành vi trên GameAnalytics | Chỉ param1 (khi khác 0) được ánh xạ vào khoá "value" để GA ghi nhận thành giá trị Design Event. param2, extra1, extra2 bị bỏ. |
Chỉ đọc duy nhất tham số mang khoá "value" nếu là kiểu số (float, double, int, long). Các tham số khác bị bỏ qua. |
| Khuyến nghị sử dụng | Ưu tiên sử dụng cho sự kiện tuỳ biến thông thường: cùng một lệnh gọi chạy trên cả MGW và SayKit (riêng GameAnalytics chỉ nhận param1). |
Chỉ dùng khi làm việc thuần tuý với MGW và bắt buộc phải gửi từ điển tham số phức tạp lên Firebase Analytics. |
Quy tắc kiểu dữ liệu tham số & Giới hạn nền tảng
- Các kiểu dữ liệu được hỗ trợ:
int,long,float,double,bool,string. Kiểu khác được chuyển thành chuỗi bằngToString()trên Firebase và AppMetrica, và bị bỏ qua trên GameAnalytics và SayKit. - Chuyển đổi kiểu Boolean:
FirebaseAnalyticsSinkép boolean thành1L(true) hoặc0L(false);nullthành chuỗi rỗng. - AppMetrica dùng JSON tự dựng:
AppMetricaSinkghép từ điển thành chuỗi JSON rồi gọiAppMetrica.ReportEvent(name, jsonString). Chỉint,long,float,doubleđược ghi dạng số;boolvà mọi kiểu khác thành chuỗi (bool thành"True"/"False"). Chỉ ký tự"trong giá trị được escape: dấu\và xuống dòng không được escape nên có thể tạo JSON không hợp lệ, hãy tránh các ký tự này trong giá trị chuỗi. - Sự kiện không có tên (Nameless Event): Chỉ
Event(name, ...)bị kiểm tra: nếunamelà chuỗi rỗng hoặcnull, hệ thống bỏ qua sự kiện và ghi cảnh báo[Analytics] Custom event without a name ignored.LogEventkhông kiểm tra tên: tên rỗng vẫn được chuyển tới các sink (Firebase sẽ ghi lỗi[Firebase] LogEvent ... failed), nên hãy tự đảm bảo tên không rỗng. - Quy tắc đặt tên và giới hạn của từng nền tảng (độ dài tên, số tham số tối đa của Firebase/GA...) do nhà cung cấp quy định và không được SDK kiểm tra: hãy tra tài liệu của nền tảng tương ứng.
5. Các sự kiện SDK tự động ghi nhận (Tuyệt đối không gửi trùng lặp)
Mã nguồn gameplay của bạn tuyệt đối không được gửi sự kiện doanh thu quảng cáo hoặc đơn hàng IAP thủ công. Toàn bộ các sự kiện này đã được MGW SDK theo dõi tự động từ tầng hạ tầng với độ chính xác tuyệt đối. Việc tự ý bắn thêm sự kiện sẽ khiến dữ liệu trên Dashboard bị nhân đôi (Double Counting), làm sai lệch hoàn toàn chỉ số LTV và ROAS của chiến dịch Marketing.
| Hạng mục | Đơn vị tự động ghi nhận | Sự kiện được gửi lên các nền tảng phân tích |
|---|---|---|
| Doanh thu quảng cáo (Ad Revenue / ILRD) | Tự động kích hoạt bởi MaxAdsService (Provider MGW) ngay khi có dữ liệu doanh thu của lượt hiển thị. Trên Provider SayKit, callback doanh thu của SayKit cũng gọi vào hub nhưng SayKitAnalyticsSink không chuyển tiếp đi đâu cả (SayKit tự ghi nhận quảng cáo của nó). |
|
| Giao dịch In-App Purchase | Tự động kích hoạt bởi IapService một lần cho mỗi giao dịch mới, ngay sau khi biên lai được xác thực (trước khi trao thưởng; giao dịch được giao lại ở lần mở sau không báo lại). Trên Provider SayKit, sink không chuyển tiếp; giao dịch được báo qua SayKitPurchaseTracker (chỉ có khi define SAYKIT_PURCHASING được bật). |
|
6. Cơ chế hoạt động Runtime & Hợp đồng vận hành
Trạng thái sẵn sàng của Sink & Cơ chế loại bỏ sự kiện sớm
Trong suốt chu kỳ khởi động của ứng dụng, các module phân tích cần thời gian để kết nối mạng và giải quyết phụ thuộc:
- Firebase Analytics: Chỉ kích hoạt khi
CoreConfig.enableFirebase = true.IsReadychỉ thànhtruesau khiCheckAndFixDependenciesAsynctrả vềAvailablevàFirebaseApp.DefaultInstancetạo được. Sự kiện (vàSetUserProperty) gọi trước thời điểm đó bị bỏ qua và mất hẳn: hub chỉ chuyển cho sink đã sẵn sàng, không đệm lại. - AppMetrica: Chỉ có khi project bật define
TB_USE_APPMETRICAvà cài packageio.appmetrica.analytics, vàCoreConfig.enableAppMetrica = true. Sink sẵn sàng khiappMetricaApiKeyđã được điền (khác rỗng và khácREPLACE_ME); nếu không, hệ thống ghi cảnh báo và sink bị tắt trong phiên chạy đó. - GameAnalytics: Chỉ có khi bật define
TB_USE_GAvà cài packagecom.gameanalytics.sdk, vàCoreConfig.enableGameAnalytics = true. Khi kích hoạt, sink tạo GameObjectGameAnalyticsnếu cảnh chưa có, rồi khởi tạo SDK. Khoá lấy từ cửa sổ cấu hình GameAnalytics, không phải CoreConfig. - SayKit: Lớp
SayKitAnalyticsSinkluôn trả vềIsReady = truevì bản thân SDK của SayKit đã tích hợp sẵn cơ chế đệm sự kiện trong bộ nhớ (Memory Buffering) trước khi hoàn tất kết nối máy chủ.
So sánh tổng hợp giữa Provider MGW và SayKit
| Đặc điểm hoạt động | Provider MGW (Mặc định) | Provider SayKit (SayGames) |
|---|---|---|
| Các Sink hoạt động | Firebase Analytics, GameAnalytics (tuỳ chọn), AppMetrica (tuỳ chọn). | Duy nhất SayKitAnalyticsSink kết nối tới hệ thống máy chủ phân tích của SayGames. |
| Cơ chế truyền sự kiện | Ánh xạ qua TrackEventMapper.ToGeneric để phân rã thành tên sự kiện chuẩn và Dictionary. |
Truyền trực tiếp đối tượng cấu trúc TrackEvent qua giao diện ITrackingSink. |
| Hỗ trợ thuộc tính người dùng (User Properties) | Chỉ Firebase hỗ trợ qua App.Analytics.SetUserProperty(name, value); GameAnalytics và AppMetrica bỏ qua. |
Không hỗ trợ (lệnh gọi bị bỏ qua an toàn, không gây lỗi). |
| Xử lý sự kiện tự do (LogEvent) | Firebase nhận mọi khoá (bool thành 0/1); AppMetrica nhận JSON tự dựng (số giữ nguyên, kiểu khác thành chuỗi); GameAnalytics chỉ nhận khoá value kiểu số. |
Chỉ giữ lại số đầu tiên và chuỗi đầu tiên, bỏ qua toàn bộ phần còn lại. |
7. Nhật ký Console chuẩn (Expected Log Lines)
Dưới đây là các dòng log xuất ra cửa sổ Unity Console qua lớp CoreLog với định dạng [Tag] Message:
Các dòng mức Info chỉ được in khi CoreConfig.verboseLog = true hoặc khi chạy bản development (Debug.isDebugBuild). Cảnh báo (Warning) và lỗi (Error) luôn được in. Dòng [Analytics] <tên> chỉ cho biết hub đã nhận và ánh xạ sự kiện: nó chỉ in tên sự kiện (không in tham số) và được in trước khi chuyển cho sink, nên không chứng minh sink đã nhận (sink chưa sẵn sàng sẽ bỏ sự kiện).
| Chuỗi Log xuất hiện trên Console | Mức độ | Ý nghĩa & Ngữ cảnh xuất hiện |
|---|---|---|
[Analytics] level_start |
Log Info | Hub đã nhận lệnh LevelStarted và ánh xạ thành sự kiện level_start (chỉ log tên). |
[Analytics] level_complete |
Log Info | Hub đã nhận lệnh LevelCompleted (tham số như duration_sec không xuất hiện trong log). |
[Analytics] earn_virtual_currency |
Log Info | Hub đã nhận lệnh CurrencyIncome (chỉ log tên sự kiện, không log số dư). |
[Analytics] item_upgrade |
Log Info | Hub đã nhận sự kiện tuỳ chỉnh tên item_upgrade (qua Event hoặc LogEvent). |
[Analytics] Custom event without a name ignored |
Warning | Event(name, ...) được gọi với tên rỗng hoặc null: sự kiện bị huỷ (không áp dụng cho LogEvent). |
[Firebase] Ready (Analytics + Crashlytics) |
Log Info | Module Firebase đã khởi tạo thành công, FirebaseAnalyticsSink bắt đầu nhận sự kiện. |
[Firebase] LogEvent level_start failed: ... |
Error | Firebase SDK ném ngoại lệ khi ghi sự kiện; thông điệp lỗi của Firebase đi kèm sau dấu hai chấm. Sự kiện đó không được gửi, các sink khác không bị ảnh hưởng. |
[AppMetrica] API key not set in CoreConfig, AppMetrica disabled |
Warning | AppMetrica (đã bật enableAppMetrica) bị vô hiệu hoá trong phiên chạy vì chưa cấu hình API Key trong CoreConfig. |
8. So sánh Code Đúng vs Code Sai
// 1. WRONG: completing a level without calling LevelStarted first
void OnVictory()
{
// Consequence: no duration_sec on the event (no start time was recorded)
App.Analytics.LevelCompleted(5, score: 100);
}
// 2. WRONG: sending the IAP event manually
void OnPurchaseSuccess(string id)
{
// Dangerous: double counts revenue on the dashboard.
// IapService already reports the purchase to every sink.
App.Analytics.Event("purchase", extra1: id);
}
// 3. WRONG: sending an event from Update on every frame
void Update()
{
// Fires ~60 events per second: floods the sinks and buries real data in noise
App.Analytics.Event("player_x", (long)transform.position.x);
}
// 1. RIGHT: start the level before finishing it
void OnStartGame()
{
App.Analytics.LevelStarted(5, tag: "normal");
}
void OnVictory()
{
// MGW sinks receive duration_sec measured since LevelStarted
App.Analytics.LevelCompleted(5, score: 100, tag: "normal");
}
// 2. RIGHT: only deliver the content, the SDK reports the purchase
void OnPurchaseSuccess(string id)
{
// The SDK reports the purchase to the active sinks (Firebase, GameAnalytics, AppMetrica).
// Game code only unlocks the feature and updates the UI.
UnlockPremiumFeature(id);
}
// 3. RIGHT: track only meaningful, user-triggered actions
public void OnUpgradeButtonClicked()
{
App.Analytics.Click("shop_screen", "btn_upgrade_sword");
}
9. Ví dụ thực hành mẫu (C# Realistic Examples)
Ví dụ 1: Quản lý trọn vẹn luồng màn chơi (LevelFlowManager)
Script mẫu minh hoạ cách tích hợp đầy đủ chu trình: Bắt đầu màn chơi, hoàn thành các chặng con (Stages) và kết thúc màn chơi có thưởng tiền tệ ảo:
using TapBrain.Core;
using TapBrain.Core.Services;
using UnityEngine;
namespace MyGame.Gameplay
{
public class LevelFlowManager : MonoBehaviour
{
private int _currentLevel = 1;
private int _currentStage = 1;
private int _playerCoins = 250;
public void StartCampaignLevel(int level)
{
_currentLevel = level;
_currentStage = 1;
// 1. Track the start of the main level
App.Analytics.LevelStarted(_currentLevel, tag: "dungeon");
// 2. Start the first stage (the SDK fills in the current level)
App.Analytics.StageStarted(_currentStage, tag: "dungeon");
Debug.Log($"[MyGame] Started Level {_currentLevel}, Stage {_currentStage}");
}
public void PassCurrentStage()
{
// Complete the stage (MGW sinks get its duration_sec)
App.Analytics.StageCompleted(_currentStage, score: 50, tag: "dungeon");
_currentStage++;
// Start the next stage
App.Analytics.StageStarted(_currentStage, tag: "dungeon");
}
public void CompleteLevel(bool isSuccess)
{
if (isSuccess)
{
// Complete the last stage
App.Analytics.StageCompleted(_currentStage, score: 100, tag: "dungeon");
// Complete the main level (MGW sinks get duration_sec since LevelStarted)
App.Analytics.LevelCompleted(_currentLevel, score: 1000, tag: "dungeon");
// Reward virtual currency on victory
int rewardCoins = 100;
_playerCoins += rewardCoins;
App.Analytics.CurrencyIncome(CurrencyType.Soft, rewardCoins, _playerCoins, place: "level_victory");
}
else
{
// Fail the current stage, then the level
App.Analytics.StageFailed(_currentStage, score: 10, tag: "dungeon");
App.Analytics.LevelFailed(_currentLevel, score: 150, tag: "dungeon");
}
}
}
}
Ví dụ 2: Mua và nâng cấp trang bị trong Cửa hàng (ShopItemController)
Script mẫu minh hoạ quy trình người chơi mở giao diện cửa hàng, tiêu tiền ảo để mua sắm vật phẩm và gửi sự kiện nâng cấp tuỳ biến:
using TapBrain.Core;
using TapBrain.Core.Services;
using UnityEngine;
namespace MyGame.Shop
{
public class ShopItemController : MonoBehaviour
{
private int _softGems = 1200;
public void OpenShopView()
{
// Track opening the shop screen
App.Analytics.Screen("shop_weapon_view");
}
public void BuyItem(string itemId, int cost, int targetTier)
{
// Track the tap on the buy button
App.Analytics.Click("shop_weapon_view", "buy_button");
if (_softGems < cost)
{
Debug.LogWarning("[MyGame] Not enough gems to purchase!");
return;
}
// 1. Spend the currency and track it
_softGems -= cost;
App.Analytics.CurrencyOutcome(CurrencyType.Soft, cost, _softGems, place: "shop_buy");
// 2. Track the new item
App.Analytics.ItemReceived(itemId);
// 3. Custom event with the four fixed slots (GameAnalytics keeps only param1)
// param1: cost, param2: upgrade tier, extra1: item id, extra2: purchase source
App.Analytics.Event(
name: "item_upgrade",
param1: cost,
param2: targetTier,
extra1: itemId,
extra2: "shop_dialog"
);
Debug.Log($"[MyGame] Successfully purchased {itemId} tier {targetTier}");
}
}
}
10. Các lỗi phổ biến & Cách khắc phục
- Lỗi mất tham số
duration_sec: Xảy ra khi gọiLevelCompletedhoặcLevelFailednhưng trước đó bỏ quênLevelStarted, hoặc khi chơi lại mà không gọi lạiLevelStarted(mốc bị xoá sau lần kết thúc đầu). Hãy gọiLevelStartedcho mỗi lượt chơi. - Dùng
LogEventvới nhiều tham số trên Provider SayKit: SayKit chỉ ghi nhận 1 số và 1 chuỗi đầu tiên từ từ điển tham số. Giải pháp: dùngApp.Analytics.Event(name, p1, p2, e1, e2)để cả 4 slot đến được SayKit (lưu ý GameAnalytics chỉ nhậnp1). - Tự log sự kiện doanh thu quảng cáo hoặc IAP: Làm trùng dữ liệu doanh thu trên các nền tảng phân tích. Hãy để SDK (
MaxAdsService,IapService) tự báo. - Gửi sự kiện quá thường xuyên: Tránh gọi Analytics trong
Update(),FixedUpdate()hoặc mỗi bước di chuyển của nhân vật: mỗi lệnh gọi phát một sự kiện tới mọi sink đang sẵn sàng, làm nhiễu dữ liệu và tốn tài nguyên. Chỉ theo dõi các mốc có ý nghĩa phân tích. Giới hạn số sự kiện/tham số của từng nền tảng do nhà cung cấp quy định, SDK không kiểm tra.
11. Checklist kiểm tra nhanh
LevelStarted đều truyền số màn người chơi nhìn thấy (≥ 1), không bắt đầu từ 0.ad_impression, purchase hoặc ad_revenue.param1; dữ liệu quan trọng nên nằm ở param1 hoặc ở Firebase/AppMetrica.