Skip to content

Tutorial: Typed Key-Value Storage with DataStorage

Store and look up runtime values by Key — gameplay stats, UI state, save data, anything that lives as named typed entries. The DataStorage system gives you typed access to a shared key-value bag without coupling readers to writers.

By the end of this tutorial you'll have a PlayerStats storage with health, score, and a current weapon, accessed safely from multiple unrelated systems.

Prerequisites: None — this is foundational.


What Is a DataStorage?

Two kinds of host:

  • DataStorageAsset — a ScriptableObject asset. Shared by everything that references it. Useful for "global player stats", project-wide settings.
  • DataStorageComponent — a MonoBehaviour instance. Lives on a specific GameObject. Useful for "per-NPC stats", per-instance data.

Both wrap a DataStorage instance — a typed dictionary keyed by Key ScriptableObjects.

Each entry is stored under a typed Key. Reading a key returns its current value; writing sets it.


1. Create the Storage

Right-click in the Project window → Create → Jungle → Data → Data Storage. Name it PlayerStats.

The asset's Inspector shows its current entries (empty for now). Entries are added at edit time or runtime.


2. Create Keys

Each typed slot needs a Key. Keys are ScriptableObjects — refactor-safe identifiers.

  • Create → Jungle → Data → Key named Health.
  • Repeat for Score and CurrentWeapon.

The Key itself has no type — the type comes from how it's used (whichever typed source/sink references it). The same Key can be reused across multiple storages if you want.


3. Author Initial Values

In the PlayerStats asset, add entries:

Key Type Initial value
Health float 100
Score int 0
CurrentWeapon GameObject (reference to default weapon prefab)

Save the asset.


4. Read a Key

Any "value from key" provider can read from the storage. Common ones:

Source What it reads
FloatFromDataStorage One float entry.
IntFromDataStorage One int entry.
BoolFromDataStorage One bool entry.
GameObjectFromDataStorage One GameObject entry.
ObservableFloatValueFromKey One float entry with change notifications.

Drop one of these into any SmartFloat, SmartInt, etc. field. Set its storage to the PlayerStats asset, its key to the Health Key. The field now reflects the live value.

Conditions can read too — a FloatValueCondition reading Health < 25 becomes "low health".


5. Write a Key

Write via:

  • A Process Component running a Value Command: add Write Float (once) or Set Float Over Time (gradually), and pick Float From Data Storage pointed at the key as its sink. See the Writing Values tutorial.
  • A Measurement Command: each measurement, such as Speed, writes into its own sink. Point the sink at the key.
  • A DataStorageCommand — for one-off ops triggered by other processes (set, increment, clear).
  • Direct code — dataStorage.SetFloat(key, value).

The point is: every reader/writer agrees on the Key. Multiple writers / multiple readers, all decoupled.


6. Local vs Global Storage

Asset (DataStorageAsset):

  • Persists between scene loads.
  • Shared by every GameObject that references it.
  • Edit-time values become runtime starting values.
  • Important: edits to the asset during play mode are preserved into edit mode unless you reset. To reset, add a Process Component that starts with the scene and runs a DataStorageCommand with a Clear operation.

Component (DataStorageComponent):

  • Per-instance.
  • The Inspector shows the live values during play, scoped to that GameObject.
  • Resets automatically when the GameObject is disabled and the scene reloads.

Pick the asset when you want "one game, one stats bag". Pick the component when each prefab instance has its own.


7. Item Providers

An ItemProvider component assigns an Item asset to a GameObject — so inventory operations know which item a scene object represents when storing or retrieving it. Add it to prefabs that participate in inventory systems.


8. Listening for Changes

Wrap the read in an Observable source — ObservableFloatValueFromKey, ObservableIntValueFromKey, etc. — and pass it to:

  • A Reactor target for typed reactive bodies.
  • A UI binder that updates a label on change.
  • A Condition that polls.

The observable fires only on actual change, not every frame.


9. Composite Values from Multiple Keys

To fold several keys into one value, pick a multi-key provider and give it a combiner:

Provider Combiners What you get
Multi-Key Float (MultiKeyFloat) Sum, Min, Max, Average One number from several float keys
Multi-Key Int (MultiKeyInt) Sum, Min, Max One number from several int keys
Multi-Key Bool (MultiKeyBool) All True, Any True, All False, None True AND / OR over several bool keys
Bool Keys As Int (BoolKeysAsInt) Count True, Count False How many flags are set

Drop the provider in any observable slot, list the keys, pick the combiner. The result stays observable, so a reaction on it fires whenever any of those keys changes — no code, and nothing to keep in sync.

Thresholds are not combiners

"Is this at least 100" and "is this within a band" are not derived values; they are triggers. Put a Threshold or Range trigger on the reaction watching the value, or a FloatValueCondition / IntValueCondition when a command needs to gate on it.


10. Common Patterns

Health bar

  • Storage: PlayerStats.
  • Key: Health (float).
  • Writer: A DataStorageCommand with SetFloat ops, fired by damage / heal events.
  • Reader: UI binder → ObservableFloatValueFromKey(PlayerStats, Health) → fill amount.

Inventory count

  • Storage: PlayerStats.
  • Key: Coins (int).
  • Writer: IncrementIntOperation on pickup events.
  • Reader: an IntValueCondition on the Coins key gates the "buy" button.

Save game

  • Storage: any number of DataStorageAssets.
  • Reader: at game start, hydrate UI / world state from each key.
  • Writer: at game end / interval, snapshot the current values into a save file.

Next Steps