Overview
Remote Config pulls every config schema your project has authored and caches it locally. Configs are organised as configs → sections → keys. You read a value by (section, key); the SDK fetches per platform, app version major and active environment.
- Reads are synchronous after the initial fetch.
- Schema changes hot-reload - registered fields and ScriptableObjects re-apply automatically.
- Disk-cached at
persistentDataPath/kalmforge_remote_config.jsonfor offline launches.
Quick start
1using KalmForge;2using UnityEngine;34public class GameBoot : MonoBehaviour {5 async void Start() {6 await KalmForgeClient.Init();78 // Subscribe BEFORE Init so you don't miss the first event.9 RemoteConfig.OnConfigLoaded += OnReady;10 await RemoteConfig.Init();11 }1213 void OnReady() {14 float dmg = RemoteConfig.GetValue("balance", "player.damage", 10f);15 bool promo = RemoteConfig.GetValue("flags", "summer_promo", false);16 string url = RemoteConfig.GetValue<string>("links", "support", "");17 }18}Typed reads
GetValue<T>(section, key, defaultValue) coerces the stored value to T. Supported types out of the box: string, bool, int, long, float, double, enum, and arrays of any of the above.
1int wave = RemoteConfig.GetValue("balance", "starting_wave", 1);2bool flag = RemoteConfig.GetValue("flags", "show_intro", true);3float[] tier = RemoteConfig.GetValue<float[]>("economy", "tier_costs", new float[]{1f,5f,20f});45Difficulty d = RemoteConfig.GetValue("balance", "difficulty", Difficulty.Normal);67if (RemoteConfig.HasKey("flags", "experimental")) {8 /* gate the feature */9}1011if (RemoteConfig.TryGetValue<float>("balance", "boss_hp", out var hp)) {12 boss.maxHp = hp;13}Structured arrays
On the dashboard you can declare a key as array<TypeName> - a typed list of objects with a named shape (e.g. array<StoryModeLevel>). The schema ships the structure and a row-per-item value array; the SDK returns it as JSON you deserialise into your own [Serializable] class.
Why use them? Spreadsheet-style editing on the dashboard, type-safe rows in Unity, and the same payload powers per-platform / per-environment overrides and A/B variants without any extra plumbing.
1. Declare the row type in C#
1using System;2using UnityEngine;34[Serializable]5public class StoryModeLevel {6 public int level;7 public string name;8 public float enemy_hp_multiplier;9 public int reward_coins;10 public bool is_boss;11}1213// JsonUtility can't deserialise a top-level array, so wrap it.14[Serializable]15class StoryModeLevelList { public StoryModeLevel[] items; }2. Read the array
GetValue<string> returns the raw JSON for the array. Wrap it as {"items": …} and parse:
1string raw = RemoteConfig.GetValue<string>("story_mode", "levels", "[]");2var list = JsonUtility.FromJson<StoryModeLevelList>("{\"items\":" + raw + "}");34foreach (var lvl in list.items) {5 Debug.Log($"Level {lvl.level} - {lvl.name} x{lvl.enemy_hp_multiplier}");6}Newtonsoft.Json? Then you can deserialise directly: JsonConvert.DeserializeObject<StoryModeLevel[]>(raw) - no wrapper needed.3. Bind to a field automatically
[ConfigurableField] works with arrays of structured types when you declare the field as the matching C# array. The SDK parses the JSON for you on every refresh:
1public class StoryModeData : RemoteConfigScriptableObject {2 [ConfigurableField("story_mode", "levels")]3 public StoryModeLevel[] levels;4}Schema shape on the wire
For reference, the REST response encodes a structured array as the custom type definition plus a value array of objects keyed by field name:
1{2 "type": "array<StoryModeLevel>",3 "value": [4 { "level": 1, "name": "Forest", "enemy_hp_multiplier": 1.0, "reward_coins": 50, "is_boss": false },5 { "level": 2, "name": "Caves", "enemy_hp_multiplier": 1.2, "reward_coins": 75, "is_boss": false },6 { "level": 3, "name": "Dragon", "enemy_hp_multiplier": 2.5, "reward_coins": 500, "is_boss": true }7 ]8}array<int>, array<string>, array<float>, array<bool>) work directly with GetValue<int[]> etc. - no wrapper class needed.[ConfigurableField] auto-binding
Mark fields with [ConfigurableField(section, key)] and register the target - Remote Config writes the live value into the field on every refresh.
1using KalmForge;2using UnityEngine;34public class Player : MonoBehaviour {5 [ConfigurableField("balance", "player.damage")] public float damage = 10f;6 [ConfigurableField("balance", "player.speed")] public float speed = 5f;78 void Awake() {9 // Apply immediately if loaded, or on the next OnConfigLoaded.10 RemoteConfig.Configure(this);11 }1213 void OnDestroy() => RemoteConfig.Unconfigure(this);14}RemoteConfig.Configure(typeof(Tuning)).ScriptableObject base class
Inherit from RemoteConfigScriptableObject to get editor-safe binding for free: every [ConfigurableField] on the asset is overridden at runtime, and the original authored values are restored when the asset is disabled or the editor exits Play mode.
1using KalmForge;2using UnityEngine;34[CreateAssetMenu(menuName = "KalmForge/Balance")]5public class Balance : RemoteConfigScriptableObject {6 [ConfigurableField("balance", "player.damage")] public float damage = 10f;7 [ConfigurableField("balance", "player.speed")] public float speed = 5f;8}Reference the asset from any MonoBehaviour; the live values are automatically present. Call ResetToOriginal() if you ever want to drop back to the authored values.
Environments & version targeting
Every fetch sends three filters server-side:
- platform - derived from
Application.platform(ios,android,webgl,windows, …). - version - the leading integer of
Application.version("72.0.6"→72). - environment -
DevelopmentorProductionfromKalmForgeSettings.
Override the environment at boot if you need to: await RemoteConfig.Init(KalmForgeSettings.Environment.Production);
A/B test overrides
If A/B Tests assigns a player to a variant whose rc_overrides include "section.key", that value automatically wins over the baseline - no extra code on the read site.
1// Variant has rc_overrides: { "store.gem_pack_price": 4.99 }2float price = RemoteConfig.GetValue<float>("store", "gem_pack_price", 0.99f);3// → 4.99 for players in the variant, 0.99 for everyone else.Events
| Name | Type | Description |
|---|---|---|
| OnConfigLoaded | event Action | Fires exactly once after Init() resolves (cache or network). |
| OnConfigUpdated | event Action | Fires whenever a Fetch() merges new schema data. |
API reference
| Name | Type | Description |
|---|---|---|
| IsInitialized | bool | Init() has run. |
| IsConfigLoaded | bool | OnConfigLoaded has fired. |
| Environment | KalmForgeSettings.Environment | Environment used by the next Fetch(). |
| Init(environment?) | Task | Load cache, then fetch from network. |
| Fetch() | Task | Re-fetch and merge. Safe to call repeatedly. |
| GetValue<T>(section, key, default) | T | Read or fall back. Honours AB overrides. |
| TryGetValue<T>(section, key, out value) | bool | Read without a default. |
| HasKey(section, key) | bool | True if the key was returned by the last fetch. |
| Configure(object) | void | Bind every [ConfigurableField] on the instance now and on every future fetch. |
| Configure(Type) | void | Bind static [ConfigurableField] members on the type. |
| Unconfigure(object) | void | Stop tracking the instance. |
| CacheFileName | string (const) | "kalmforge_remote_config.json". |
REST endpoint
1GET /api/public/sdk/remote-config2 ?platform=ios3 &version=724 &environment=production5Headers:6 X-API-Key: kf_xxx_yyy78Response:9{10 "configs": [11 {12 "name": "balance",13 "hash": "abc123",14 "schema": { "sections": { "player": { "damage": { "type": "float", "value": 12.5 } } } }15 }16 ]17}