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.

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.

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

IIdentifiable - interface for components that carry a ScriptableObject identifier. Used to bridge scene objects with inventory entries.

IdentifiableComponent - simple MonoBehaviour that assigns an identifier to a GameObject. Add to prefabs that need to be tracked by inventory or other identity-aware systems.

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
InventoryValue Inline inventory stored directly on the owner
InventoryValueFromComponent References an Inventory component 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 plus its sibling Setter, Measurer, and Visualizer.

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, body) entries when the source's triggers fire.

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

A target watches its source and decides when to fire. A trigger inside the target decides which condition fires. The body is a list of processes that runs when a trigger fires. Targets reuse the same operation/command surface used everywhere else in Jungle — anything that implements the matching IXReactionTarget exposes a SmartX slot that's auto-wired to the observed value.

Typed Targets

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

  • 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.
  • Event target — source is an IEvent; trigger fires when the event raises.
  • Signal target — source is a SignalType; trigger fires when Reactor.Receive(signal) is called.
  • Frame cycle target — source is a per-frame cycle; trigger fires on cycle begin / end.

Body ops marked IXReactionTarget expose a SmartX ReactionInput slot. On first dispatch the slot is auto-wired to the observed value so the live reaction value flows in with no manual setup.

Retrigger / interrupt behaviour is per-entry. Edge re-fires while the body is still running follow ProcessRetriggerStrategy; band exits and reactor shutdown follow ReactionInterruptPolicy (ForceComplete snaps in-flight processes to their final state, Cancel leaves them alone).

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
FloatSumFromKeys float Sum of multiple float entries
FloatMinFromKeys float Minimum of multiple float entries
FloatMaxFromKeys float Maximum of multiple float entries
IntSumFromKeys int Sum of multiple int entries
TrueFlagsCountInt int Count of true values among listed bool keys
IntAtLeastBool bool True when an int entry ≥ threshold
FloatInRangeBool bool True when a float entry is within range
AllFlagsTrueBool bool True when all listed bool keys are true
AnyFlagTrueBool bool True when any listed bool key is true

Signal Dispatch

Signals are pushed into a Reactor via Reactor.Receive(SignalType). 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[Body processes]
    T2 -->|ignores| X[wrong signal]

Setter

A Setter groups value-setting entries under one lifecycle. Each entry assigns values into its own target — a settable value, a data storage, a reference field — and manages its own scheduling. The component starts and stops every entry together. Sibling to Reactor (consumes triggers and dispatches reactions) and Visualizer (drives scene visuals from data).

Add a Setter component via Add Component > Jungle > Data > Setter. For the common "feed one DataStorage with several measurements" case, the dedicated Measurer component flattens that into a single component.

Entry Types

Each setter entry implements ISetterEntry and receives lifecycle calls from the host Setter.

DataStorageSetter

Targets a DataStorage and runs a list of sub-setters against it. Each sub-setter produces one or more keyed values. Sub-setters sharing the same entry write into the same storage.

Field Type Description
target IDataStorageValue The data storage to write into (component or asset reference)
subSetters List<IDataStorageSubSetter> Sub-setters that produce keyed values

Sub-setters implement IDataStorageSubSetter and self-schedule — they can subscribe to events, sample on a frequency, or write once on initialize. The parent DataStorageSetter resolves the storage once on begin and passes it to every sub-setter.

Duplicate key detection runs automatically in the editor: if two sub-setters write to the same Key, a warning appears in the console.

ScheduledOperationEntry

For "run these write-value ops on a schedule" the current pattern is a ScheduledOperationEntry on a Setter:

  • The entry hosts a list of IOperation (commonly WriteFloat, WriteInt, WriteBool, WriteVector3, WriteGameObject, WriteShape, …) and an IExecutionScheduler (every frame, every fixed frame, every N frames, every interval).
  • Each scheduled tick runs the operation list once. Use this to copy values between sources and sinks on whatever cadence you need.

The older standalone FloatValueSetter, IntValueSetter, etc. types are obsolete — they remain for compatibility but should not be used in new content.