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.
- The storage every operation works on.
- 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<T>"]
VFD -->|reads| DS
VFD -->|"IValue<T>"| 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 (
IObservableFloatValuesource) with triggersOnChangeFloatTrigger,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 whenReactor.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 callReactor.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).
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.