Unity SDK

Remote Config

Tune balance, flags and A/B variants without shipping a build. Strongly-typed reads, automatic field binding via [ConfigurableField], a ScriptableObject base class, and per-platform / per-version / per-environment scoping enforced server-side.

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.json for offline launches.

Quick start

GameBoot.csC#
1using KalmForge;
2using UnityEngine;
3
4public class GameBoot : MonoBehaviour {
5 async void Start() {
6 await KalmForgeClient.Init();
7
8 // Subscribe BEFORE Init so you don't miss the first event.
9 RemoteConfig.OnConfigLoaded += OnReady;
10 await RemoteConfig.Init();
11 }
12
13 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.

exampleC#
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});
4
5Difficulty d = RemoteConfig.GetValue("balance", "difficulty", Difficulty.Normal);
6
7if (RemoteConfig.HasKey("flags", "experimental")) {
8 /* gate the feature */
9}
10
11if (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#

StoryModeLevel.csC#
1using System;
2using UnityEngine;
3
4[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}
12
13// 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:

exampleC#
1string raw = RemoteConfig.GetValue<string>("story_mode", "levels", "[]");
2var list = JsonUtility.FromJson<StoryModeLevelList>("{\"items\":" + raw + "}");
3
4foreach (var lvl in list.items) {
5 Debug.Log($"Level {lvl.level} - {lvl.name} x{lvl.enemy_hp_multiplier}");
6}
Tip
Prefer 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:

exampleC#
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:

examplejson
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}
Note
Primitive arrays (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.

exampleC#
1using KalmForge;
2using UnityEngine;
3
4public class Player : MonoBehaviour {
5 [ConfigurableField("balance", "player.damage")] public float damage = 10f;
6 [ConfigurableField("balance", "player.speed")] public float speed = 5f;
7
8 void Awake() {
9 // Apply immediately if loaded, or on the next OnConfigLoaded.
10 RemoteConfig.Configure(this);
11 }
12
13 void OnDestroy() => RemoteConfig.Unconfigure(this);
14}
Tip
Static fields are supported too - pass the type: 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.

Balance.csC#
1using KalmForge;
2using UnityEngine;
3
4[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 - Development or Production from KalmForgeSettings.

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.

exampleC#
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

NameTypeDescription
OnConfigLoadedevent ActionFires exactly once after Init() resolves (cache or network).
OnConfigUpdatedevent ActionFires whenever a Fetch() merges new schema data.

API reference

RemoteConfig - static API
NameTypeDescription
IsInitializedboolInit() has run.
IsConfigLoadedboolOnConfigLoaded has fired.
EnvironmentKalmForgeSettings.EnvironmentEnvironment used by the next Fetch().
Init(environment?)TaskLoad cache, then fetch from network.
Fetch()TaskRe-fetch and merge. Safe to call repeatedly.
GetValue<T>(section, key, default)TRead or fall back. Honours AB overrides.
TryGetValue<T>(section, key, out value)boolRead without a default.
HasKey(section, key)boolTrue if the key was returned by the last fetch.
Configure(object)voidBind every [ConfigurableField] on the instance now and on every future fetch.
Configure(Type)voidBind static [ConfigurableField] members on the type.
Unconfigure(object)voidStop tracking the instance.
CacheFileNamestring (const)"kalmforge_remote_config.json".

REST endpoint

examplebash
1GET /api/public/sdk/remote-config
2 ?platform=ios
3 &version=72
4 &environment=production
5Headers:
6 X-API-Key: kf_xxx_yyy
7
8Response:
9{
10 "configs": [
11 {
12 "name": "balance",
13 "hash": "abc123",
14 "schema": { "sections": { "player": { "damage": { "type": "float", "value": 12.5 } } } }
15 }
16 ]
17}
Back to DocsKalmForge SDK · v1.0.1