Skip to main content

Variable Store

ConvoVariableStore is a ScriptableObject that stores typed, scoped key-value pairs: a lightweight runtime database for gameplay state that dialogue can read and write. It is the bridge between what happens in a conversation and the rest of your game.

Create via: Right-click in the Project window → Create → ConvoCore → Runtime → Variable Store

One variable store asset can serve your entire project. Create additional stores only if you need strict isolation between unrelated systems (for example, a separate store for a mini-game).


Variable scopes

Every variable has a scope that determines how it is persisted:

ScopePersisted?Where savedDescription
GlobalYesConvoCoreGameSnapshot.GlobalVariablesShared across all conversations. Use for player-wide state: quest progress, relationship values, story flags that span scenes.
ConversationYesInside each ConversationSnapshot.VariablesBelongs to one conversation. Use for per-NPC state: whether the player was rude, which branch they took, how many times they have spoken to this character.
SessionNoMemory onlyReset when the application closes or Play Mode exits. Never written to disk. Use for temporary flags that exist only within a single play session.
note

Think of Global as your save file's top-level entries: "has the player freed the village?". Conversation scope is per-NPC state: "did the player choose the aggressive option with this merchant?". Session scope is for scratch variables: counters, temporary flags, or UI state that is meaningless after a restart.


Variable types

Variables are strongly typed. Supported types:

Enum valueC# typeInspector label
ConvoCoreVariableType.BoolboolBool
ConvoCoreVariableType.IntintInt
ConvoCoreVariableType.FloatfloatFloat
ConvoCoreVariableType.StringstringString

Attempting to read a variable as the wrong type returns the default value for that type (e.g. 0 for Int, false for Bool) rather than throwing. Use TryGet methods to distinguish between "variable not found" and "variable has the zero value".


Writing variables

using WolfstagInteractive.ConvoCore.SaveSystem;
using UnityEngine;

public class QuestSystem : MonoBehaviour
{
[SerializeField] private ConvoVariableStore _store;

public void StartQuest()
{
_store.SetBool("quest_started", true, ConvoVariableScope.Global);
_store.SetInt("quest_step", 1, ConvoVariableScope.Global);
_store.SetString("quest_giver", "Elder Morin", ConvoVariableScope.Global);
}

public void RecordDialogueChoice(string key, string choiceValue)
{
_store.SetString(key, choiceValue, ConvoVariableScope.Conversation);
}

public void SetSessionFlag(string key)
{
_store.SetBool(key, true, ConvoVariableScope.Session);
}

public void AddGold(int amount)
{
_store.TryGetInt("player_gold", out int current);
_store.SetInt("player_gold", current + amount, ConvoVariableScope.Global);
}
}

All Set methods overwrite any existing value for that key. If the key does not exist, a new entry is created at runtime in _sessionEntries. To create entries that are persisted and visible in the inspector, declare them in _persistentEntries (see Inspector declaration).


Reading variables

// TryGet - returns false if the variable does not exist.
// Use this when the variable might not have been set yet.
if (_store.TryGetBool("quest_started", out bool questStarted))
{
Debug.Log($"Quest started: {questStarted}");
}
else
{
Debug.Log("quest_started has not been set.");
}

if (_store.TryGetInt("player_gold", out int gold))
{
Debug.Log($"Player gold: {gold}");
}

if (_store.TryGetFloat("elapsed_time", out float elapsed))
{
Debug.Log($"Elapsed time: {elapsed:F1}s");
}

if (_store.TryGetString("last_choice", out string choice))
{
Debug.Log($"Last choice: {choice}");
}

// Direct access - retrieves the raw ConvoCoreVariable entry.
// Prefer TryGet for gameplay code; use this when you need the full entry metadata.
ConvoCoreVariable variable = _store.GetVariable("player_gold");
int directGold = variable.GetInt();
warning

GetVariable() throws a KeyNotFoundException if the variable does not exist. Always use the TryGet variants in gameplay code unless you have pre-declared the variable and are certain it will be present.


Checking existence

bool exists = _store.HasVariable("quest_started");

if (exists)
{
// Safe to call GetVariable directly
}

Querying by scope or tag

// Get all variables in a specific scope
IEnumerable<ConvoCoreVariable> globals = _store.GetByScope(ConvoVariableScope.Global);
IEnumerable<ConvoCoreVariable> convVars = _store.GetByScope(ConvoVariableScope.Conversation);

// Get all variables that have a specific tag
IEnumerable<ConvoCoreVariable> questVars = _store.GetByTag("quest");
IEnumerable<ConvoCoreVariable> npcVars = _store.GetByTag("npc_state");

// Combine: all global quest variables
var globalQuestVars = _store.GetByScope(ConvoVariableScope.Global)
.Where(v => v.Tags.Contains("quest"));

Tags are defined per-variable in the inspector (see Inspector declaration).


Listening for changes

Subscribe to be notified when a specific variable changes, or when any variable changes:

private void OnEnable()
{
// Listen to a specific key
_store.Listen("player_gold", OnGoldChanged);

// Listen to all changes
_store.OnVariableChanged += OnAnyVariableChanged;
}

private void OnDisable()
{
_store.Unlisten("player_gold", OnGoldChanged);
_store.OnVariableChanged -= OnAnyVariableChanged;
}

private void OnGoldChanged(string key, object oldValue, object newValue)
{
int oldGold = (int)oldValue;
int newGold = (int)newValue;
_goldDisplay.text = newGold.ToString();
}

private void OnAnyVariableChanged(string key, object oldValue, object newValue)
{
Debug.Log($"[VariableStore] {key}: {oldValue}{newValue}");
}
tip

Use Listen / Unlisten for targeted bindings (e.g. a UI element that displays one variable). Use OnVariableChanged for broad listeners like debug overlays or analytics. Always unsubscribe in OnDisable to avoid memory leaks when objects are destroyed.


Declaring variables in the inspector

Variables can be pre-declared in the Variable Store inspector under _persistentEntries. Each entry has:

FieldDescription
KeyThe variable name. Must be unique within the store.
TypeBool, Int, Float, or String.
Default ValueThe authored starting value for a new game.
ScopeGlobal or Conversation. Session-scoped variables cannot be pre-declared.
DescriptionOptional notes for your team. Not used at runtime.
TagsString tags used with GetByTag().
IsReadOnlyPrevents runtime writes. Read attempts work normally; write attempts log a warning and do nothing.

Pre-declared variables appear in the inspector during Play Mode with their current runtime value shown next to the authored default.

warning

The authored defaults in _persistentEntries represent the starting state for a new game. They are not updated by the save system; they are the baseline. At runtime, writes go to _sessionEntries (the in-memory layer). When the save system loads a slot, it restores the saved values on top of the authored defaults. If you exit Play Mode and re-enter without loading a save, values reset to their authored defaults.


Internal storage model

The variable store uses two internal dictionaries:

DictionaryAccessWhen cleared
_persistentEntriesSerialized field; authored in the inspectorOnly when the asset is reimported or manually edited.
_sessionEntries[NonSerialized]; created lazilyEvery time Play Mode exits or the application closes.

When reading a variable, the store checks _sessionEntries first, then falls back to _persistentEntries. When writing, the value always goes into _sessionEntries. This ensures that authored defaults in _persistentEntries are never modified at runtime, even in the editor.

For Advanced Users

The variable store editor tracks a snapshot of authored defaults captured when Unity exits Edit Mode. During Play Mode, any variable whose current runtime value differs from its authored default is highlighted in orange in the inspector. This live diff makes it easy to see at a glance which variables have been touched during a test playthrough, without running a separate debug overlay.

You can also use the editor's scope filter and text filter toolbar to quickly find variables in large stores. The editor repaints at 0.1-second intervals during Play Mode so the live diff stays current without requiring manual inspector focus.


Clearing variables

// Clear all session-layer entries (does not affect authored persistent entries)
_store.ClearSessionVariables();

// Clear all variables of a specific scope from the session layer
_store.ClearByScope(ConvoVariableScope.Conversation);

// Reset a single variable to its authored default (or remove it if not pre-declared)
_store.ResetVariable("quest_step");

These are useful during scene transitions or when starting a new game: clear Conversation-scoped variables between conversations, or clear all session variables on "New Game".