Skip to content

Data

Store and retrieve typed data at runtime - numbers, strings, objects - using a simple key-value storage that works in the Inspector.

Jungle.Data gives you two complementary storage systems. The data storage stores arbitrary typed values (int, float, bool, string, Color, Vector3, and more) that any part of your project can read and write. The inventory tracks quantities of identified items with automatic stacking and composable filter rules. Both are fully configurable in the Inspector and integrate with the rest of the Jungle framework.

Assembly: Jungle.Data Dependencies: Jungle.Core, Jungle.Spatial, Jungle.Graphics Namespace: Jungle.Data


DataStorage

A DataStorage is a flat keyed collection of polymorphic entries. Each entry carries one Key and one typed value. You add entries directly through the Inspector via the type picker; each Key is unique and maps to one typed value. Runtime lookup is O(1) per key.

Key

A Key is a ScriptableObject that acts as a named identifier for data storage entries. Create one via Create > Jungle > Data > Key.

Using ScriptableObject keys instead of raw strings avoids typos, enables Inspector drag-and-drop, and lets you rename keys without breaking references.

Entry Types

Each entry type wraps a single value type and binds it to a Key. Entries use the value provider system internally, so each entry's value can be a constant, a reference to another component, or any IValue<T> implementation.

Built-in entry types:

Class Value Type
IntEntry int
FloatEntry float
BoolEntry bool
StringEntry string
ColorEntry Color
Vector3Entry Vector3
BoundsEntry Bounds
GameObjectEntry GameObject

Hosting a DataStorage

DataStorage instances are plain [Serializable] classes. To use one in a scene or project, host it with one of these containers:

Container Type Use Case
DataStorageComponent MonoBehaviour Per-GameObject data (player stats, object state)
DataStorageAsset ScriptableObject Shared project-wide data (global config, level settings)

Create a DataStorageAsset via Create > Jungle > Data > Data Storage Asset.

Inspector
PlayerStats
Data Storage Component? ⇅ ⋮
Entries2+
Entry 1Float Entry
KeyHealth
Value100
Entry 2Int Entry
KeyScore
Value0
A Data Storage Component with two entries. Each entry is a key and its starting value.

Reading and Writing Values

From C#, use the typed API on DataStorage:

// Reading
if (dataStorage.TryGetValue<int>(healthKey, out int health))
    Debug.Log($"Health: {health}");

// Or with a default fallback
float speed = dataStorage.GetValue<float>(speedKey);

// Writing
dataStorage.SetValue(healthKey, 100);

DataStorage Commands

DataStorageCommand is a composable command that batches multiple operations on a single data storage. Add it to any command-driven workflow (event responses, process triggers) and configure a list of IDataStorageOperation entries in the Inspector.

ProcessData Storage Command
Data Storage SourcePlayerStats (Data Storage Component)1
Operations2+2
Operation 1Set Float
KeyHealth
Value To Set100
Operation 2Increment Int
KeyScore
Amount1
Modulo0
One Data Storage Command running two operations on the same storage.
  1. The storage every operation works on.
  2. The operations. They all run together.

Built-in Operations

Operation Description
SetIntAction Writes an int value by key
SetFloatAction Writes a float value by key
SetBoolAction Writes a bool value by key
SetStringAction Writes a string value by key
ToggleBoolAction Flips a bool entry; a missing entry counts as false
IncrementIntAction Adds to an int entry; supports modulo wrapping

Each set operation takes a Key and an IValue<T> source, so the written value can come from a constant, another data storage entry, or any value provider.

IncrementIntAction

Adds an amount to an existing int entry. Useful for counters, cycling through indices, or decrementing resources. Both Amount and Modulo are smart values, so they can be constants or driven dynamically from another data storage entry or value provider.

Key:     scoreKey
Amount:  1          // negative to decrement
Modulo:  0          // > 0 enables wrapping: (current + amount) % modulo

With modulo: 4, values cycle through 0, 1, 2, 3, 0, 1, ... - useful for rotating through selections or animation states.

Custom Operations

Implement IDataStorageAction to create your own:

[Serializable]
[JungleClassInfo("Multiply Float", "Multiplies a float entry by a factor.", null, "Data")]
public class MultiplyFloatAction : IDataStorageAction
{
    [SerializeField] private Key key;
    [SerializeField] private float factor = 2f;

    public void Execute(DataStorage storage)
    {
        if (!key) return;
        var current = storage.GetValue<float>(key);
        storage.SetValue(key, current * factor);
    }
}

Values From DataStorage

Data storage entries can feed into any system that consumes value providers. The ValueFromDataStorage<T> base class reads a typed value from a data storage by key, exposing it as an IValue<T>.

Built-in implementations:

Class Provides
IntFromDataStorage IIntValue
FloatFromDataStorage IFloatValue
BoolFromDataStorage IBoolValue
StringFromDataStorage IStringValue
ColorFromDataStorage IColorValue
Vector3FromDataStorage IVector3Value
BoundsFromDataStorage IBoundsValue
GameObjectFromDataStorage IGameObjectValue

Each one references an IDataStorageValue (where the data storage lives) and a Key (which entry to read). These are also ISettableValue<T>, so pipelines can write back into the data storage.

DataStorage Value Sources

The IDataStorageValue interface provides the data storage reference itself. Two built-in sources:

Source Description
DataStorageValueFromComponent References a DataStorageComponent on a GameObject
DataStorageValueFromAsset References a DataStorageAsset in the project

Inventory

The inventory system tracks quantities of identified items with automatic stacking and composable filter rules.

Core Types

IInventory - the interface that all inventory implementations satisfy. Exposes entries, filters, store/retrieve methods, and change events.

public interface IInventory
{
    IReadOnlyList<InventoryEntry> Entries { get; }
    IReadOnlyList<InventoryFilter> StoreFilters { get; }
    IReadOnlyList<InventoryFilter> RetrieveFilters { get; }
    int EntryCount { get; }
    bool TryStore(Item item, int quantity = 1);
    bool TryRetrieve(Item item, int quantity = 1);
    int GetQuantity(Item item);
    bool Contains(Item item);
    event Action<InventoryEntry> OnEntryAdded;
    event Action<InventoryEntry> OnEntryRemoved;
    event Action<InventoryEntry> OnEntryChanged;
}

Inventory - a MonoBehaviour that implements IInventory. Add it directly to a GameObject to give that object an inventory. Items with the same Item reference stack automatically.

InventoryEntry - a single (Item Item, int Quantity) pair.

Item - a ScriptableObject that gives items a name, description, and an embedded DataStorage for extensible metadata. Create via Create > Jungle > Storage > Item.

// Item carries metadata through its DataStorage
[CreateAssetMenu(menuName = "Jungle/Storage/Item")]
public class Item : ScriptableObject, IDataStorageProvider
{
    public string Description;
    public DataStorage DataStorage { get; }  // query with any Key
}

Any system can query an Item's data storage using its own keys - the inventory reads maxQuantity, an object pool reads prefab, the UI reads icon - without coupling to each other.

Hosting an Inventory

Container Type Use Case
Inventory MonoBehaviour Per-GameObject inventory (player backpack, chest)
InventoryAsset ScriptableObject Shared inventory (party resources, shop stock)

Inventory is a MonoBehaviour - add it to any GameObject and it is ready to use. For InventoryAsset, call Initialize() manually before first use.

Store and Retrieve

// Store 3 of an item
if (inventory.TryStore(healthPotion, 3))
    Debug.Log("Stored!");

// Retrieve 1
if (inventory.TryRetrieve(healthPotion, 1))
    Debug.Log("Retrieved!");

// Query
int count = inventory.GetQuantity(healthPotion);
bool has = inventory.Contains(healthPotion);

Both TryStore and TryRetrieve run through their respective filter lists before modifying data. If any filter rejects the operation, it returns false and the inventory is unchanged.

Events

Subscribe to inventory changes at runtime:

inventory.OnEntryAdded   += entry => Debug.Log($"New item: {entry.Item.name}");
inventory.OnEntryRemoved += entry => Debug.Log($"Removed: {entry.Item.name}");
inventory.OnEntryChanged += entry => Debug.Log($"Changed: {entry.Item.name} x{entry.Quantity}");

Inventory Filters

Filters gate store and retrieve operations with AND logic. Add multiple filters to an inventory to compose rules.

Filter Description
SlotCapacityFilter Limits the total number of distinct entry slots. Set maxSlots to -1 for unlimited.
PerItemCapacityFilter Limits quantity per item type by reading a maxQuantity int from the item's DataStorage (via IDataStorageProvider).

Custom Filters

Extend InventoryFilter:

[Serializable]
[JungleClassInfo("Weight Limit", "Rejects if total weight exceeds a cap.", null, "Inventory")]
public class WeightLimitFilter : InventoryFilter
{
    [SerializeField] private Key weightKey;
    [SerializeField] private float maxWeight = 100f;

    public override bool CanStore(IInventory inventory, Item item, int quantity)
    {
        // Calculate total weight from all entries...
        return totalWeight <= maxWeight;
    }
}

InventoryCommand

InventoryCommand is a composable command that batches multiple operations on a single inventory. It holds a list of IInventoryOperation entries, following the operation-based command pattern used throughout Jungle. Add it to any command-driven workflow and configure operations in the Inspector.

Identity System

IItemProvider - interface for components that carry an Item asset identity. Used to bridge scene GameObjects with inventory entries.

ItemProvider - simple MonoBehaviour that assigns an Item asset to a GameObject. Add to prefabs that need to be tracked by inventory or other identity-aware systems. Add via Add Component > Jungle / Inventory / Item Provider.

Inventory Value Providers

Like data storage, inventories integrate with the value provider system. IInventoryValue extends IValue<IInventory>, so any value provider that resolves to an IInventory can feed into inventory-aware systems.

Source Description
InventoryReference References an InventoryComponent on a GameObject
InventoryValueFromAsset References an InventoryAsset in the project

Component Lookup

FromDataStorageStrategy implements IComponentLookupStrategy to find components by data storage key. It looks for a DataStorageComponent on the target GameObject, then retrieves a Component reference stored under the given key.

This is useful when a system needs to resolve a component reference indirectly - for example, finding a specific collider or renderer stored as metadata on an object.


Data Flow

flowchart TD
    subgraph Storage
        K[Key]
        E[Entry]
        DS[DataStorage]
        K --> E --> DS
    end

    subgraph Containers
        DSC[DataStorageComponent]
        DSA[DataStorageAsset]
        DS --> DSC
        DS --> DSA
    end

    subgraph Commands
        CMD[DataStorageCommand]
        OP[IDataStorageOperation]
        OP --> CMD
        CMD -->|writes| DS
    end

    subgraph "Value Integration"
        VFD["ValueFromDataStorage&lt;T&gt;"]
        VFD -->|reads| DS
        VFD -->|"IValue&lt;T&gt;"| Consumer[Any Consumer]
    end
flowchart TD
    subgraph Inventory
        IT[Item]
        IE[InventoryEntry]
        IINV[IInventory]
        INV[Inventory]
        IA[InventoryAsset]
        IF[InventoryFilter]
        ICMD[InventoryCommand]
        IOP[IInventoryOperation]
        IT --> IE --> IINV
        IF -->|gates| IINV
        INV -->|implements| IINV
        IA -->|implements| IINV
        IOP --> ICMD
        ICMD -->|operates on| IINV
    end

    IT -->|IDataStorageProvider| DS[DataStorage]

Reactive System

The reactive system observes data storage values, events, signals, and frame cycles at runtime and dispatches responses automatically. It is built on the Reactor component. To write values instead of reacting to them, use a Process Component running a Value Command (see Writing Values below). Magpile adds Visualizer, which drives scene visuals from data.

Reactor

A Reactor hosts a list of IReactorTarget items. Each target watches one source — a typed observable value, an event, a signal received via Reactor.Receive(), or a frame cycle — and runs its own list of (trigger, process) entries when the source's triggers fire.

Reactor
└─ Targets (IReactorTarget list)
   ├─ Float target ─── source: IObservableFloatValue
   │   └─ Entries (trigger + process)
   │       ├─ Threshold trigger (Above 100) → [IProcess]
   │       └─ Range trigger (0..100)        → [IProcess]
   └─ Signal target  ── source: signal Key (pushed via Receive)
       └─ Entries → [IProcess]

A target watches its source and decides when to fire. A trigger inside the target decides which condition fires. The process is what runs when a trigger fires; use a Process List to run several. Targets reuse the same operation/command surface used everywhere else in Jungle. Each run carries what fired it: the value (read with Context Value) and, for GameObject, Exclusive and cycle reactions, the object (read with Context).

Typed Targets

Each scalar type has its own target shape that exposes the live value to its process:

  • Float target (IObservableFloatValue source) with triggers OnChangeFloatTrigger, ThresholdFloatTrigger (band: Above / Below / AbsAbove / AbsBelow, with hysteresis), RangeFloatTrigger (band: [min, max], with hysteresis).
  • Bool / Int / Double / String / Vector3 / GameObject targets with type-matched trigger shapes. The int target also has IntTierTrigger: an ascending threshold list that fires with the tier index (0 below the first threshold, N at or above the Nth), so a fill meter is one reaction whose process is a GameObject Command targeting the set with an Activate First N operation (ActivateFirstN: first N objects of the set active, the rest off).
  • Event target — source is an IEvent; trigger fires when the event raises.
  • Signal target — source is a signal Key; trigger fires when Reactor.Receive(signal) is called. A signal can carry a value (Context Value in the run) or an object (the run's Context): set Value on Invoke Signal, or call Reactor.Receive(signal, RunValue.Of(x), obj).
  • Active Between Signals target — an On signal and an Off signal plus a set of objects; the objects stay active from one signal to the other. No process.
  • Frame cycle target — source is a per-frame cycle; reactions fire on cycle begin / end. Besides the Process, GameObject and Bool reactions, two state-visual reactions need no process: Active While keeps a set of objects active while any target is inside the cycle (invert for the opposite), and Spawn While spawns one item per target from a creator, keeps it on the target, and reclaims it when the target leaves.

Operations that implement their type's reaction-target interface expose a ReactionInput slot of the matching smart value (SmartFloat for IFloatReactionTarget, and so on). When you add one inside a reaction, the editor sets that slot to Context Value; it stays an ordinary, visible field you can point elsewhere. Nothing rewrites the process at runtime: the value travels with the run and is readable on every frame of it.

Retrigger / interrupt behaviour is per-entry. Edge re-fires while the process is still running follow ProcessRetriggerStrategy; band exits and reactor shutdown follow ReactionInterruptPolicy (ForceComplete snaps in-flight processes to their final state, Cancel leaves them to finish, Pause freezes them on a band exit and resumes them on the next fire).

Observable Values

Observable values are computed from data storage or other sources and notify when they change. Built-in implementations include:

Value Type Source
ObservableBoolValueFromKey bool Data storage bool entry
ObservableIntValueFromKey int Data storage int entry
ObservableFloatValueFromKey float Data storage float entry
ObservableGameObjectValueFromKey GameObject Data storage GameObject entry
MultiKeyFloat float Several float keys folded into one value by a combiner
MultiKeyInt int Several int keys folded into one value by a combiner
MultiKeyBool bool Several bool keys folded into one value by a combiner
BoolKeysAsInt int Several bool keys counted into an int
NotBool bool Inverts another observable bool

Combining Several Keys

MultiKeyFloat, MultiKeyInt and MultiKeyBool each take a list of Keys and a combiner that decides how the per-key values fold into one. Pick the combiner in the Inspector; the value stays observable, so a reaction on it fires whenever any of its keys changes.

Value Combiner Result
MultiKeyFloat SumFloatCombiner (default) Total of every key
MinFloatCombiner / MaxFloatCombiner Lowest / highest
AverageFloatCombiner Mean
MultiKeyInt SumIntCombiner (default) Total
MinIntCombiner / MaxIntCombiner Lowest / highest
MultiKeyBool AllTrueBoolCombiner (default) AND
AnyTrueBoolCombiner OR
AllFalseBoolCombiner / NoneTrueBoolCombiner The negated forms
BoolKeysAsInt CountTrueIntCombiner (default) How many flags are true
CountFalseIntCombiner How many are false

A key with no stored value is skipped rather than counted as zero, and an empty list returns the combiner's identity (0 for Sum, false for All True).

Thresholds and ranges are not combiners: "is this int at least 100" or "is this float within a band" belongs on the reaction watching the value, as a ThresholdIntTrigger / RangeFloatTrigger, or in a FloatValueCondition / IntValueCondition when a command needs to gate on it.

Signal Dispatch

Signals are pushed into a Reactor via Reactor.Receive(Key). The Reactor fans the signal out to every signal-receiving target in its list. For per-GameObject pub/sub (no Reactor required), use LocalSignalTransmitter — anything that holds a reference to the transmitter can register listeners or raise signals on it.

flowchart LR
    A[Caller] -->|"Receive(signal)"| R[Reactor]
    R --> T1[Signal target A]
    R --> T2[Signal target B]
    T1 -->|matches| P1[Processes]
    T2 -->|ignores| X[wrong signal]

Writing Values

To write values into sinks, add a Process Component and pick Value Command as its process. The command holds a list of operations and runs them together. Each operation names its own Source and its own Sink: a settable value, a value asset, a Data Storage key, a component field. The Process Component decides when the writes happen, through its execution frame. Sibling to Reactor (reacts to triggers) and Visualizer (drives scene visuals from data).

Inspector
Process Component? ⇅ ⋮
Process
Start & Stop
ProcessValue Command
Operations1+
Operation 1Write Float
SourceFloat Value From Asset
SinkFloat From Data Storage
A Process Component running a Value Command: one Write Float, from an asset into a Data Storage key.

Value Operations

Operation What it does
Write Float, Write Int, Write Long, Write Bool, Write String, Write Vector3, Write GameObject Reads the source once and writes it into the sink. Finishes at once.
Set Float Over Time, Set Int Over Time, Set Double Over Time, Set Vector3 Over Time, Set Vector2 Over Time, Set Rotation Over Time Drives the sink through the pipeline: lerp, ease, oscillate, spring, or follow a source through modifiers.
Write GameObject While Active Writes a GameObject into the sink when it starts, and clears it when the host stops, if the sink still holds that object.
Add To Container While Active Adds objects to a container while it runs, and removes them when it stops.

To write into a Data Storage, pick Float From Data Storage (or the matching type) as the sink and set its storage and key. A Value Command also works inside a Reactor's process or any other process slot, so it can write when a reaction fires.

Data Storage Command

For operations on the entries of one storage, use a Data Storage Command. It picks the storage once and runs Set Float, Set Int, Toggle Bool, Increment Float, Increment Int, Accumulate Float, Clear, and more against it by key.

Measurements

Measurements are commands too. Pick Measurement Command as the process, set its target, and add measurements such as Speed, Position, or Distance Traveled. Each measurement writes into its own sink until the host stops the command. Joint Measurement Command does the same for a joint. Octoputs adds Attachable Object Measurement Command and Attachment Point Measurement Command for attach counts and hold times.

See the Writing Values tutorial for a walkthrough.