Chương 6: Remote Config (App.RemoteConfig)

Hướng dẫn toàn diện về dịch vụ cấu hình từ xa qua App.RemoteConfig (giao diện IRemoteConfigService). Tìm hiểu cơ chế liên kết kiểu mạnh Bind<T>, các hàm getter an toàn ngoại tuyến, cơ chế chống tràn số (Range Clamping) và vòng đời nạp dữ liệu tại Boot.

1. Mục đích & Mô hình kiến trúc (Mental Model)

Remote Config cho phép nhà phát triển điều chỉnh các thông số cân bằng game, kích hoạt tính năng mới và điều tiết tần suất quảng cáo từ xa thông qua bảng điều khiển đám mây (Firebase Remote Config hoặc SayKit Config) mà không cần phải phát hành bản cập nhật ứng dụng mới lên App Store hay Google Play.

Trong MGW SDK, dịch vụ được thiết kế xoay quanh nguyên tắc An toàn ngoại tuyến & Luôn đọc được dữ liệu (Always Available & Offline-Safe):

  • Không bao giờ trả về Null: Từ lúc cảnh đầu tiên được nạp (sau khi Bootstrapper chạy), mọi giá trị đều đọc được — trước khi mạng kết nối, khi thiết bị ngoại tuyến hoặc khi Firebase gặp lỗi. Hệ thống trả về giá trị mặc định nội bộ (Local Defaults), và GetString trả "" nếu khoá chưa có mặc định. Truy cập App.RemoteConfig trước khi boot (ví dụ trong static constructor) sẽ ném lỗi “not registered”.
  • Mô hình ràng buộc kiểu mạnh (Typed Binding): Thay vì truy vấn chuỗi dạng GetString("some_key") rải rác dễ sai chính tả, bạn khai báo một lớp C# chứa các trường public. SDK tự lấy tên trường làm khoá, lấy giá trị khởi tạo của trường làm giá trị mặc định (ghi vào bảng mặc định, xem cảnh báo ở mục 4) và tự điền lại (Re-fill) mỗi khi có lần cập nhật.
  • Phân tách tầng trừu tượng (Base Service Architecture): Lớp nội bộ RemoteConfigServiceBase chịu trách nhiệm lưu giữ bảng giá trị mặc định, phân tích cú pháp chuỗi độc lập văn hoá (Culture Invariant) và kiểm soát dải số của kiểu int/float. Tầng Provider (Firebase hoặc SayKit) chỉ cần cài đặt logic truy vấn dữ liệu từ xa.
Triết lý thiết kế Bind<T>

Cơ chế Reflection chỉ chạy duy nhất một lần khi bạn gọi App.RemoteConfig.Bind(target) và chạy lại khi nhận được gói cấu hình mới từ máy chủ (rất hiếm khi xảy ra). Tuyệt đối không có Reflection chạy trong vòng lặp khung hình (Update), đảm bảo hiệu năng tối đa cho gameplay.

2. Hướng dẫn tích hợp từng bước

Bước 1: Khai báo lớp cấu hình kiểu mạnh (Typed Config Class)

Tạo một lớp C# thông thường chứa các trường dữ liệu public với giá trị khởi tạo làm giá trị mặc định. Tên biến chính là tên khoá (Key) trên máy chủ Remote Config:

namespace MyGame.Config
{
    public class MyGameConfig
    {
        // int: key "starting_gold", default 100
        public int starting_gold = 100;

        // float: key "player_move_speed", default 6.5f
        public float player_move_speed = 6.5f;

        // bool: key "double_coin_enabled", default true
        public bool double_coin_enabled = true;

        // string: key "featured_event_name", default "summer_quest"
        public string featured_event_name = "summer_quest";

        // long: key "daily_reset_timestamp", default 86400
        public long daily_reset_timestamp = 86400L;
    }
}

Bước 2: Ràng buộc đối tượng và lắng nghe sự kiện Updated

Tại vị trí khởi động logic trò chơi của bạn, gọi App.RemoteConfig.Bind() và đăng ký lắng nghe sự kiện Updated để cập nhật lại hệ thống khi có thông số mới từ máy chủ:

using TapBrain.Core;
using UnityEngine;
using MyGame.Config;

namespace MyGame.Bootstrap
{
    public class GameInitializer : MonoBehaviour
    {
        private MyGameConfig _config;

        private void Awake()
        {
            // 1. Create the config object and bind it: fields are filled now and on every update
            _config = App.RemoteConfig.Bind(new MyGameConfig());

            // 2. Subscribe to updates (a fetch can finish after boot, or fail: see IsFetched)
            App.RemoteConfig.Updated += OnRemoteConfigUpdated;

            ApplyBalanceChanges(_config);
        }

        private void OnDestroy()
        {
            App.RemoteConfig.Updated -= OnRemoteConfigUpdated;
        }

        private void OnRemoteConfigUpdated()
        {
            // Bound fields are already refilled before Updated fires.
            // Updated also fires when the fetch failed: check App.RemoteConfig.IsFetched if it matters.
            Debug.Log($"[MyGame] Remote Config updated, fetched={App.RemoteConfig.IsFetched}");
            ApplyBalanceChanges(_config);
        }

        private void ApplyBalanceChanges(MyGameConfig config)
        {
            // Apply config.starting_gold, config.player_move_speed... to the game systems
        }
    }
}

Bước 3: Sử dụng các hàm Getter phi kiểu (Untyped Getters)

Trong trường hợp bạn chỉ muốn đọc nhanh một giá trị mà không muốn tạo lớp dữ liệu, hãy sử dụng các hàm getter kèm giá trị fallback an toàn:

using TapBrain.Core;
using UnityEngine;

namespace MyGame.Gameplay
{
    public class LiveBalanceHelper
    {
        public static void CheckEventStatus()
        {
            // Read a bool; the fallback is used only if the key has no registered default (or the value cannot be parsed)
            bool isBossActive = App.RemoteConfig.GetBool("special_boss_event", fallback: false);

            // Read a double with a safe fallback
            double rewardMultiplier = App.RemoteConfig.GetDouble("reward_rate_multiplier", fallback: 1.0);

            // Read a string ("" when there is no remote value and no default)
            string bannerNotice = App.RemoteConfig.GetString("live_ops_banner_text");

            Debug.Log($"[MyGame] Boss={isBossActive}, Multiplier={rewardMultiplier}, Notice={bannerNotice}");
        }
    }
}

3. Bảng tra cứu toàn bộ API (IRemoteConfigService)

Giao diện IRemoteConfigService nằm trong namespace TapBrain.Core.Services và được truy cập toàn cục qua App.RemoteConfig:

Thuộc tính / Phương thức Tham số & Kiểu trả về Mô tả chi tiết & Hành vi vận hành
bool IsFetched { get; } Trả về bool Trả về true khi lần fetch của Firebase hoàn tất không lỗi (kể cả khi activated=False vì không có gì mới), hoặc khi SayKit đã khởi tạo xong. Luôn false nếu Firebase không khả dụng hoặc khi dùng DefaultsRemoteConfigService.
event Action Updated; Sự kiện Action Kích hoạt sau khi toàn bộ đối tượng đã Bind được điền lại. Không đảm bảo có dữ liệu mới: Firebase phát Updated cả khi fetch thất bại (khi đó IsFetched = false); SayKit phát mỗi lần có cập nhật. Nếu Firebase không khả dụng thì Updated không bao giờ phát.
void AddDefault(string key, string value) key: Tên khoá
value: Giá trị mặc định (dạng chuỗi)
Thêm hoặc ghi đè một giá trị mặc định trong bảng cục bộ. Với Firebase, bảng này được chụp lại và nạp vào SDK khi bước Boot RemoteConfig bắt đầu: hãy gọi AddDefault trước thời điểm đó. Gọi muộn hơn chỉ có hiệu lực cho khoá chưa nằm trong bản chụp. Không điền lại đối tượng đã Bind và không phát Updated.
string GetString(string key) key: Tên khoá
Trả về string
Trả về giá trị của backend nếu có (với Firebase, mọi giá trị không phải “static”, gồm cả mặc định đã nạp vào Firebase SDK lúc boot). Nếu không, trả về mặc định cục bộ; nếu không có mặc định, trả về chuỗi rỗng "".
long GetLong(string key, long fallback = 0) key: Tên khoá
fallback: Giá trị dự phòng (mặc định 0)
Trả về long
Phân tích chuỗi sang số nguyên 64-bit theo chuẩn CultureInfo.InvariantCulture. Chuỗi lấy theo thứ tự: giá trị backend, mặc định đã đăng ký, rồi mới đến fallback. fallback chỉ được dùng khi khoá không có mặc định, hoặc khi chuỗi không phân tích được (ví dụ "1.5", "abc", ""): khi đó trả fallback, không phải mặc định đã đăng ký.
double GetDouble(string key, double fallback = 0) key: Tên khoá
fallback: Giá trị dự phòng (mặc định 0)
Trả về double
Phân tích sang số thực dấu phẩy động kép. Nếu giá trị chuỗi là NaN hoặc Infinity, hệ thống coi là không hợp lệ và trả về fallback. Cùng quy tắc ưu tiên mặc định/fallback như GetLong.
bool GetBool(string key, bool fallback = false) key: Tên khoá
fallback: Giá trị dự phòng
Trả về bool
Phân tích chuỗi boolean. Nhận "true"/"false" (không phân biệt hoa thường), "1" (true) và "0" (false). Mọi chuỗi khác hoặc chuỗi rỗng trả về fallback.
T Bind<T>(T target) where T : class target: Đối tượng lớp cấu hình
Trả về chính đối tượng target
Quét các trường public, non-readonly, thuộc 6 kiểu được hỗ trợ của target; ghi giá trị khởi tạo của từng trường vào bảng mặc định (ghi đè mặc định cùng khoá, kể cả khoá của SDK). Điền giá trị ngay và điền lại trước mỗi lần phát Updated. target = null ném ArgumentNullException.
IEnumerator Fetch() Trả về IEnumerator Bước Boot do InitPipeline gọi để lấy dữ liệu từ xa; code game không gọi phương thức này.

4. Cơ chế liên kết kiểu mạnh (Typed Bind<T>)

Phương thức Bind<T>(target) là phương thức ưu tiên hàng đầu để quản lý cấu hình trong MGW SDK. Để sử dụng chính xác, bạn cần nắm vững các quy tắc ràng buộc sau:

Các kiểu dữ liệu trường được hỗ trợ

Hệ thống kiểm tra kiểu dữ liệu trường thông qua field.FieldType và chỉ hỗ trợ 6 kiểu dữ liệu cơ bản sau:

Kiểu dữ liệu C# Phương thức nạp giá trị tương ứng Cơ chế xử lý đặc biệt
string GetString(field.Name) Nhận chuỗi trực tiếp từ máy chủ hoặc mặc định.
bool GetBool(field.Name, (bool)field.GetValue(target)) Hỗ trợ cả "true"/"false" lẫn "1"/"0".
int ToIntOrKeep(field.Name, (int)field.GetValue(target)) Kiểm tra dải số: Giữ nguyên giá trị cũ (kèm cảnh báo) nếu vượt dải int. Giá trị không phân tích được (ví dụ "1.5") cũng giữ giá trị cũ nhưng không có cảnh báo.
long GetLong(field.Name, (long)field.GetValue(target)) Phân tích số nguyên 64-bit theo chuẩn Invariant ("100.0" không hợp lệ; giữ giá trị cũ).
float ToFloatOrKeep(field.Name, (float)field.GetValue(target)) Kiểm tra dải số: Giữ nguyên giá trị cũ (kèm cảnh báo) nếu vượt dải float. Giá trị không phân tích được hoặc NaN/Infinity giữ giá trị cũ, không có cảnh báo.
double GetDouble(field.Name, (double)field.GetValue(target)) Bỏ qua các giá trị NaN và Infinity.

Quy tắc quét trường của Reflection

  • Chỉ áp dụng với trường Public Instance: SDK sử dụng cờ BindingFlags.Instance | BindingFlags.Public. Các trường private, protected hoặc internal sẽ bị bỏ qua.
  • Bỏ qua trường Readonly (IsInitOnly): Các trường có từ khoá readonly (ví dụ: public readonly int speed = 10;) sẽ bị bỏ qua và không thể nạp giá trị từ xa.
  • Bỏ qua Property: Các thuộc tính có dạng public int Health { get; set; } không phải là Field nên hệ thống hoàn toàn không quét tới. Bạn bắt buộc phải khai báo dưới dạng Field thông thường: public int health = 100;.
  • Kiểu khác bị bỏ qua im lặng: trường static, enum, Vector2, List<T>... không thuộc 6 kiểu trên sẽ không được nạp.
Bind ghi đè bảng mặc định, kể cả khoá của SDK

Bind đặt Defaults[tên_trường] = giá trị khởi tạo. Một trường tên inter_interval_sec, inter_first_delay_sec hoặc app_open_enabled sẽ âm thầm thay mặc định của CoreConfig và đổi hành vi AdsPolicy: đừng khai báo trùng các khoá này trong lớp cấu hình của game. Chỉ Bind một lần cho mỗi đối tượng (bind lần hai sẽ coi giá trị đang có, có thể đã là giá trị từ server, là mặc định), và tránh hai lớp có trường cùng tên (chung một khoá). Với Firebase, Bind nên được gọi trước bước Boot RemoteConfig; gọi muộn hơn vẫn đọc được giá trị nhưng mặc định mới không được nạp vào Firebase SDK.

5. Kiểm tra dải số & Cảnh báo biên (giữ giá trị hiện tại)

Trong C#, việc ép kiểu thu hẹp (Narrowing Cast) từ long sang int hoặc từ double sang float có thể gây ra hiện tượng tràn số im lặng (Silent Wrap-around). Ví dụ: số 3,000,000,000 khi ép sang int sẽ biến thành một số âm, hoặc số thực quá lớn sẽ biến thành Infinity, gây lỗi nghiêm trọng cho gameplay.

Để ngăn chặn tình trạng này, RemoteConfigServiceBase áp dụng cơ chế Giữ nguyên giá trị hiện tại kèm cảnh báo (Keep Current & Warn) (không cắt giá trị về biên):

// How the SDK handles an int field safely:
private int ToIntOrKeep(string key, int current)
{
    long value = GetLong(key, current);
    if (value >= int.MinValue && value <= int.MaxValue) return (int)value;
    CoreLog.Warn("RemoteConfig", $"{key}={value} is out of int range, keeping {current}");
    return current;
}

Tương tự đối với kiểu float: nếu giá trị double đọc được nhỏ hơn float.MinValue hoặc lớn hơn float.MaxValue, hệ thống giữ nguyên giá trị hiện tại của trường và ghi cảnh báo (ví dụ minh hoạ):
[RemoteConfig] player_speed=1E+40 is out of float range, keeping 6.5

6. Các khoá mặc định hệ thống & CoreConfig.asset

Trong file cấu hình CoreConfig.asset (tài nguyên Resources/CoreConfig), danh sách remoteConfigDefaults chứa các khoá mặc định cốt lõi điều khiển chính sách hiển thị quảng cáo trong game. Các hằng số này được định nghĩa tại lớp RemoteConfigKeys:

Tên hằng số (RemoteConfigKeys) Khoá Remote Config (Key String) Giá trị mặc định Mục đích sử dụng trong SDK
InterstitialIntervalSeconds inter_interval_sec "30" Khoảng thời gian giãn cách tối thiểu (giây) giữa 2 lần hiển thị quảng cáo toàn màn hình (Interstitial). Dùng bởi AdsPolicy; tính từ lần đóng quảng cáo toàn màn hình gần nhất (cả interstitial và rewarded). Chỉ có hiệu lực trên Provider MGW; SayKit giới hạn tần suất phía server.
InterstitialFirstDelaySeconds inter_first_delay_sec "60" Thời gian chờ tối thiểu (giây) tính từ lúc khởi động phiên trước khi cho phép quảng cáo Interstitial đầu tiên. Chỉ có hiệu lực trên Provider MGW.
AppOpenEnabled app_open_enabled "false" Bật/tắt quảng cáo App Open khi người chơi mở lại ứng dụng từ chế độ chạy ngầm (Background). Chỉ có hiệu lực trên Provider MGW.

7. Vòng đời nạp dữ liệu tại Boot & Xử lý bất đồng bộ

Trong chu trình khởi động của trò chơi (Loading Scene), bước RemoteConfig được InitPipeline điều phối với thời gian chờ giới hạn tối đa bởi CoreConfig.remoteConfigTimeout (mặc định là 4.0 giây):

  1. Đăng ký giá trị mặc định: Khi bước bắt đầu, SDK chụp bảng mặc định gồm CoreConfig.remoteConfigDefaults, các giá trị đã đăng ký qua AddDefault() và các đối tượng đã Bind(), rồi nạp vào Firebase Remote Config SDK trên thiết bị qua SetDefaultsAsync() (không phải lên máy chủ). Mặc định đăng ký sau thời điểm này không được nạp thêm. Nếu Firebase không khả dụng (enableFirebase = false hoặc khởi tạo lỗi), bước kết thúc ngay: IsFetched luôn false và Updated không bao giờ phát.
  2. Fetch và Activate: Kích hoạt hàm FetchAndActivateAsync() của Firebase Remote Config.
  3. Hết hạn chờ (Timeout handling): Nếu sau 4 giây (giá trị remoteConfigTimeout, chỉ áp dụng cho Provider MGW) mà mạng internet chậm hoặc không phản hồi, InitPipeline sẽ ngắt coroutine bước này để chuyển sang bước tiếp theo, xuất log:
    [Boot] RemoteConfig timed out after 4s, continuing
    Trò chơi vẫn tiếp tục chuyển vào Scene chính bình thường và sử dụng giá trị mặc định nội bộ.
  4. Hoàn tất ngầm (Background Completion): Tác vụ của Firebase vẫn tiếp tục chạy ngầm sau khi bước bị ngắt. Khi nó kết thúc, dù thành công hay thất bại, theo thứ tự:
    • Gán IsFetched = (fetch hoàn tất không lỗi).
    • Ghi log: [RemoteConfig] Fetched, activated=True (Info) hoặc [RemoteConfig] Fetch failed: ... (Warning).
    • Điền lại toàn bộ đối tượng đã Bind.
    • Phát sự kiện Updated trên Main Thread. Khi fetch thất bại, Updated vẫn phát với IsFetched = false và dữ liệu vẫn là mặc định.

8. So sánh Provider MGW vs SayKit

Tiêu chí so sánh Provider MGW (Mặc định) Provider SayKit (SayGames)
Backend triển khai FirebaseRemoteConfigService (sử dụng Firebase Remote Config SDK). SayKitRemoteConfigService (sử dụng SayKit GameConfig nội bộ).
Quy tắc khai báo khoá Khoá tự do trên Firebase Console; SDK tự động ánh xạ với tên trường trong Bind<T>. Bắt buộc: Mọi khoá phải được khai báo thành trường public trong file SayKitGameConfig.cs (tại đường dẫn Assets/SayKitExtensions/GameConfig/SayKitGameConfig.cs). Nếu thiếu trường này, SayKit sẽ không nạp khoá đó từ server.
Thời điểm nạp dữ liệu Nạp tại bước Boot số 4 qua lệnh gọi FetchAndActivateAsync. Nạp trong lúc SayKit khởi tạo (bước Boot 2', timeout sayKitInitTimeout); bước RemoteConfig (3') có timeout cố định 1 giây và chỉ đánh dấu đã fetch nếu SayKit đã khởi tạo xong. Mỗi lần SayKitConfig.remoteConfigUpdated kích hoạt, Updated được phát.
Trạng thái khi ngoại tuyến Trả về mặc định từ CoreConfig, AddDefault và Bind. Nếu Firebase không khả dụng thì chỉ dùng các mặc định này và không có Updated. Trước khi SayKit khởi tạo xong (IsFetched = false): chỉ dùng mặc định của CoreConfig/AddDefault/Bind. Sau đó, mọi khoá có trường trong SayKitGameConfig lấy giá trị từ trường đó (giá trị server hoặc giá trị khởi tạo của trường), đè lên mặc định Bind/AddDefault; khoá không có trường vẫn dùng mặc định phía SDK. Hãy giữ giá trị khởi tạo của trường SayKitGameConfig trùng mặc định Bind.
Dự phòng khi không có Provider Nếu không có Provider nào được biên dịch, (hoặc Provider không cung cấp dịch vụ này) SDK đăng ký DefaultsRemoteConfigService — chỉ phục vụ các giá trị mặc định cục bộ, IsFetched luôn luôn là false và Updated không bao giờ phát.

9. Nhật ký Console chuẩn (Expected Log Lines)

Các thông điệp ghi nhận từ hệ thống Remote Config giúp lập trình viên xác định trạng thái kết nối và phát hiện sớm các thông số bất thường (số liệu trong ví dụ chỉ mang tính minh hoạ):

Khi nào dòng Info xuất hiện

Các dòng mức Info (Fetched, SayKit game config ready, [Boot] ... done in) 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.

Chuỗi Log xuất hiện trên Console Mức độ Ý nghĩa & Ngữ cảnh xuất hiện
[RemoteConfig] Fetched, activated=True Log Info Tải và kích hoạt thành công gói cấu hình mới từ máy chủ Firebase.
[RemoteConfig] Fetch failed: ... Warning Fetch của Firebase thất bại (thông điệp lỗi đi kèm). IsFetched = false nhưng Updated vẫn được phát.
[RemoteConfig] starting_gold=3000000000 is out of int range, keeping 100 Warning Giá trị số trên máy chủ vượt quá giới hạn int.MaxValue; hệ thống giữ nguyên giá trị mặc định để tránh tràn số.
[RemoteConfig] player_speed=1E+40 is out of float range, keeping 6.5 Warning Giá trị số trên máy chủ vượt quá giới hạn float.MaxValue; hệ thống giữ nguyên giá trị float an toàn.
[RemoteConfig] SayKit game config ready (18 fields) Log Info (Trên Provider SayKit) SayKit đã khởi tạo lần đầu; số trong ngoặc là số trường public của SayKitGameConfig (ví dụ 18). Chỉ in một lần.
[Boot] RemoteConfig done in 0.45s Log Info Bước nạp Remote Config trong tiến trình Boot hoàn tất nhanh chóng trước thời hạn timeout.
[Boot] RemoteConfig timed out after 4s, continuing Warning Bước vượt remoteConfigTimeout (4s mặc định) tại màn hình Loading; ứng dụng tiếp tục bằng giá trị mặc định. Fetch có thể kết thúc sau đó và phát Updated.

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

Code Sai
// 1. WRONG: property or readonly field in a Bind class
public class BadConfig
{
    // Properties are not supported -> ignored completely!
    public int GoldReward { get; set; } = 50;

    // Readonly field -> the SDK cannot write the remote value!
    public readonly float Speed = 5.0f;
}

// 2. WRONG: reading the value once in Start and never refreshing it
public class BadWeapon : MonoBehaviour
{
    private float _damage;

    void Start()
    {
        // If the fetch has not finished yet, _damage keeps the default forever
        // and misses the server value that arrives later.
        _damage = (float)App.RemoteConfig.GetDouble("weapon_dmg", 10.0);
    }
}
Code Đúng
// 1. RIGHT: plain public fields with an initial value as the default
public class GoodConfig
{
    // Public instance field -> detected and filled automatically
    public int gold_reward = 50;

    // Public float field -> out-of-range remote values are rejected with a warning
    public float speed = 5.0f;
}

// 2. RIGHT: read again on Updated (or use a bound object, which refills itself)
public class GoodWeapon : MonoBehaviour
{
    void Start()
    {
        App.RemoteConfig.Updated += RefreshWeaponStats;
        RefreshWeaponStats();
    }

    void OnDestroy()
    {
        App.RemoteConfig.Updated -= RefreshWeaponStats;
    }

    private void RefreshWeaponStats()
    {
        float damage = (float)App.RemoteConfig.GetDouble("weapon_dmg", 10.0);
        ApplyDamage(damage);
    }
}

11. Ví dụ thực hành mẫu (C# Realistic Examples)

Ví dụ 1: Quản lý thông số cân bằng qua MyGameConfig (Typed Binding)

Minh hoạ cách giữ các biến cân bằng game trong một lớp cấu hình được Bind, tự nhận giá trị mới từ xa mà không cần singleton hay biến static:

using TapBrain.Core;
using UnityEngine;

namespace MyGame.Gameplay
{
    // 1. The balance values: public fields, the initial value is the offline default
    public class MyGameConfig
    {
        public int level_retry_cost = 50;
        public float base_enemy_speed = 3.2f;
        public bool endless_mode_unlocked = false;
        public string season_theme_color = "#FF9900";
    }

    // 2. A component that owns the bound config and reacts to updates
    public class BalanceManager : MonoBehaviour
    {
        private MyGameConfig _config;

        private void Awake()
        {
            // Bind once; the fields refill in place on every update
            _config = App.RemoteConfig.Bind(new MyGameConfig());
            App.RemoteConfig.Updated += OnConfigRefreshed;
            NotifyGameplaySubsystems();
        }

        private void OnDestroy()
        {
            App.RemoteConfig.Updated -= OnConfigRefreshed;
        }

        private void OnConfigRefreshed()
        {
            Debug.Log($"[MyGame] Balance refreshed: retryCost={_config.level_retry_cost}, enemySpeed={_config.base_enemy_speed}");
            NotifyGameplaySubsystems();
        }

        private void NotifyGameplaySubsystems()
        {
            // Push _config to the enemy spawners, retry button UI...
        }
    }
}

Ví dụ 2: Điều phối sự kiện giới hạn thời gian (LiveOpsEventController)

Minh hoạ việc kiểm tra trạng thái bật/tắt sự kiện mùa lễ hội (Special Holiday Event) bằng các hàm getter phi kiểu kết hợp kiểm tra IsFetched:

using TapBrain.Core;
using UnityEngine;

namespace MyGame.Events
{
    public class LiveOpsEventController : MonoBehaviour
    {
        [SerializeField] private GameObject holidayBannerObject;

        private void Start()
        {
            // Register defaults for the event keys (keys already known to Firebase at boot keep their boot-time default)
            App.RemoteConfig.AddDefault("holiday_event_active", "false");
            App.RemoteConfig.AddDefault("holiday_event_title", "Winter Carnival");

            // Listen for cloud updates
            App.RemoteConfig.Updated += EvaluateEventState;

            // Evaluate the current state (fetched values or defaults)
            EvaluateEventState();
        }

        private void OnDestroy()
        {
            App.RemoteConfig.Updated -= EvaluateEventState;
        }

        public void EvaluateEventState()
        {
            bool isActive = App.RemoteConfig.GetBool("holiday_event_active", fallback: false);
            string eventTitle = App.RemoteConfig.GetString("holiday_event_title");

            if (holidayBannerObject != null)
            {
                holidayBannerObject.SetActive(isActive);
            }

            Debug.Log($"[MyGame] Holiday Event: Active={isActive}, Title='{eventTitle}', IsFetched={App.RemoteConfig.IsFetched}");
        }
    }
}

12. Các lỗi phổ biến & Cách khắc phục

  • Dùng Property thay vì Field trong class Bind: Khai báo public int Coin { get; set; } sẽ khiến biến không nhận được giá trị từ Remote Config do cơ chế Reflection chỉ quét Field. Cách sửa: Đổi thành public int coin = 10;.
  • Đặt từ khoá readonly cho trường cấu hình: Cờ IsInitOnly sẽ khiến SDK bỏ qua trường này. Cách sửa: Xoá bỏ từ khoá readonly để cho phép hệ thống cập nhật giá trị mới.
  • Quên thêm trường vào SayKitGameConfig.cs khi dùng Provider SayKit: Trên SayKit, nếu khoá không tồn tại dưới dạng một trường trong lớp SayKitGameConfig, máy chủ SayKit sẽ không truyền giá trị về máy khách. Cách sửa: Khai báo biến trùng tên trong Assets/SayKitExtensions/GameConfig/SayKitGameConfig.cs.
  • Coi Updated là “đã có dữ liệu mới từ server”: Firebase cũng phát Updated khi fetch thất bại. Cách sửa: kiểm tra App.RemoteConfig.IsFetched khi cần phân biệt.
  • Khai báo trường trùng khoá của SDK (inter_interval_sec, inter_first_delay_sec, app_open_enabled) trong lớp Bind: Bind ghi đè mặc định của CoreConfig và đổi hành vi giãn cách quảng cáo. Cách sửa: chỉ đặt khoá riêng của game trong lớp Bind.
  • Lưu trữ biến cục bộ không đăng ký Updated: Gán giá trị vào một biến private trong hàm Awake() nhưng không lắng nghe sự kiện Updated khiến hệ thống không bao giờ nhận được dữ liệu mới sau khi bước Boot hoàn tất nạp ngầm.

13. Checklist kiểm tra nhanh

Đảm bảo toàn bộ các trường cần nạp là trường public, không chứa từ khoá readonly và không khai báo dạng Property (get/set).
Các lớp phụ thuộc thông số cân bằng từ xa đều lắng nghe sự kiện App.RemoteConfig.Updated để tự động làm mới dữ liệu.
Nếu dự án chuyển sang Provider SayKit, đảm bảo mọi tên biến trong class cấu hình đã được khai báo tương ứng trong SayKitGameConfig.cs.