> For the complete documentation index, see [llms.txt](https://malbersanimations.gitbook.io/animal-controller/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://malbersanimations.gitbook.io/animal-controller/scriptable-architecture/mlocal-variables.md).

# MLocal Variables

Last updated AC v1.5.3

## Overview

Local Variables store a set of named variables directly on a Transform. They can be read and written by any other component, compared against other values, and are most often used as the blackboard of the AI Brain component.

Two components ship with the framework, and both implement `ILocalVars`:

| Component                                                    | Add Component menu                                       | Storage                                                                                                                             |
| ------------------------------------------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Local Variables \[BlackBoard]** (`MLocalVars`)             | Malbers ▸ Runtime Vars ▸ Local Variables \[BlackBoard]   | a name-keyed `Dictionary<string, object>` built from a serialized list of `LocalVar` rows                                           |
| **Local Variables 2 \[BlackBoard]** (`MLocalVarsBlackboard`) | Malbers ▸ Runtime Vars ▸ Local Variables 2 \[BlackBoard] | strongly typed `BlackboardVar` entries, addressable by **name or integer ID**, with typed subscriptions and runtime-added variables |

Which one to use: the **Local Variable** Reaction, the **Local MVariable** Condition and the AI Brain's **Check Local Variable** / **Set Local Var** nodes all target `MLocalVars`, so keep the original component on characters driven by those. Reach for **Local Variables 2** when you want compile-time types, lookup by ID, typed C# callbacks, or variables that are created while the game runs.

<figure><img src="https://963537199-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lzhr1XSMzMqNXjRnNlb%2Fuploads%2FlldGxm15vtJvAtVQvmHl%2Fimage.png?alt=media&#x26;token=36f964ec-50d2-4056-b5e2-591b6da541f1" alt="" width="562"><figcaption></figcaption></figure>

## Local Variables \[BlackBoard]

The original component. Its inspector draws exactly one thing: the **Variables** list. Everything else — the runtime dictionary, the pin, the listeners — is built from that list.

On `Start()` the list is copied into a `Dictionary<string, object>` keyed by the variable name. From that moment on, the dictionary is the live value and the serialized rows are only a mirror.

\[Insert Image - MLocalVars inspector showing the Variables list with several rows of different types]

#### Variables

The authored list of variables. Each row is a `LocalVar` drawn on a single line as **Var** (name), **Type** and **Value**, with a foldout arrow that reveals that row's typed UnityEvent.

#### Var (Name)

The name-key used for the dictionary. This is the string every Reaction, Condition, decision and API call looks the variable up by.

{% hint style="info" %}
Names are **case sensitive**, and duplicates are not merged: on `Start()` the second row with an already-used name logs a warning and is skipped entirely, so its value never reaches the dictionary.
{% endhint %}

#### Type

What the row holds. Fourteen types are available: Int, Float, Bool, String, Vector3, Vector2, GameObject, Transform, Material, UnityObject, and the four Scriptable Variable references IntVar, FloatVar, BoolVar and StringVar.

The last four store a reference to a Scriptable Variable asset, so the local variable and the global asset stay in sync; the typed UnityEvent for those rows fires with the asset's `.Value`, not with the asset itself.

#### Value

The starting value, drawn with the editor matching the selected Type. Changing it in the inspector **while in Play Mode** writes straight into the runtime dictionary and fires that row's event, so you can poke values live.

#### On \<Type> Changed

Expanding a row's foldout reveals one typed UnityEvent for that row — `On Int Changed`, `On Float Changed`, `On Bool Changed` and so on. Material and UnityObject rows share the same Object event.

{% hint style="info" %}
These row UnityEvents are **not** invoked by `SetVar()` in a build. Inside `SetVar` the call that mirrors the new value back onto the row and invokes its event is wrapped in an editor-only block, so at runtime `SetVar` updates the dictionary and notifies C# subscribers but leaves the inspector-wired event silent. `Pin_SetValue()` is not wrapped, so it fires the row event everywhere. Use the Pin pair below when you need a UnityEvent to reach another UnityEvent.
{% endhint %}

{% hint style="info" %}
Every setter compares first: if the new value equals the stored one, nothing is written and no event fires.
{% endhint %}

### The Pin mechanism

A UnityEvent can pass at most one argument, so there is no way to wire a single call that says "set variable *Ammo* to 5". The Pin splits that into two calls:

1. **`Pin_Var(string name)`** — remembers that variable as the pinned one.
2. **`Pin_SetValue(value)`** — writes the value into whatever is currently pinned.

Both are public, so in the inspector you add two entries to the same UnityEvent: first `Pin_Var` with the name, then `Pin_SetValue` with the value.

`Pin_SetValue` is overloaded for **int, float, bool, string, Vector2, Vector3, GameObject, Transform, Material** and **Object**, which is what makes the whole set selectable from a UnityEvent dropdown.

\[Insert Image - a UnityEvent with two rows: Pin\_Var("Ammo") followed by Pin\_SetValue(5)]

{% hint style="info" %}
Reading also pins. `GetVar<T>()` calls `Pin_Var()` internally, so any read silently changes which variable a later `Pin_SetValue` will write to. Always pin immediately before you set.
{% endhint %}

{% hint style="info" %}
Pinning a name that is not in the dictionary logs a warning and clears the pin, after which `Pin_SetValue` does nothing at all — silently.
{% endhint %}

**`Var_Set_True(string name)` / `Var_Set_False(string name)`** are the shortcut for bools: one call, one string argument, no pin step needed.

## Local Variables 2 \[BlackBoard]

`MLocalVarsBlackboard` is the newer component. It is marked `[DisallowMultipleComponent]`, so only one can live on a GameObject.

Each entry is a real typed object (`BlackboardVar<T>`) stored by `[SerializeReference]`, not a row of fourteen parallel value fields. That buys strong typing, a typed `Action<T>` per variable, a stable integer ID, and the ability to create and destroy variables at runtime.

Both lookup tables are built in `Awake()` (and rebuilt on every inspector change in Edit Mode). On `OnEnable` the component broadcasts every variable's current value through its UnityEvent, so inspector-wired listeners receive the initial state.

\[Insert Image - MLocalVars 2 inspector showing the variable list with ID, type badge, name and value columns]

#### Variable list

A reorderable list with two column headers, **Variable Name** and **Value**. Each row shows, left to right: the drag handle, a foldout arrow, the variable's **ID**, a coloured **type badge**, the **name** field, the **value** field and a trash button that removes the row.

The **+** button in the list header opens the type menu: Float, Int, Bool, String, Object, then Vector3, Vector2, Color, then a **Var** submenu with IntVar, FloatVar, BoolVar and StringVar (which hold a reference to the matching Scriptable Variable asset).

#### ID

A stable integer handed out by the component from a serialized counter, shown read-only at the left of each row. It is the second way to address a variable: every `Get`, `Set`, `Listen` and `GetVariable` call has an `int id` overload alongside the `string name` one. IDs are cheaper than string hashing in hot code and survive a rename.

{% hint style="info" %}
Only variables with an ID of 0 or higher are registered in the ID table. A `BlackboardVar` constructed in code without going through the component's `Add...` helpers keeps its default ID of `-1` and is reachable by name only.
{% endhint %}

#### Name

The string key. The field is **disabled while in Play Mode** — renaming a live variable from the inspector would orphan every listener keyed to the old name. Rename in code with `RenameVariable()`, which moves the registry entry atomically.

#### Value

Drawn with the editor for that variable's type. In Play Mode the row reads and writes the live value directly.

An **Object** variable draws an extra type dropdown next to its ObjectField. Picking a type there (any type resolvable at runtime, e.g. `Animator` or `AudioClip`) filters what the field will accept.

#### On Changed

Expanding a row reveals that variable's typed UnityEvent, carrying the new value as its argument.

{% hint style="info" %}
Every event — the UnityEvent, the typed `Action<T>` and the untyped `Action<object>` — fires **only when the value actually changes**, compared with the default equality comparer for the type.
{% endhint %}

### Adding variables at runtime

`AddFloat`, `AddInt`, `AddBool`, `AddString` and `AddObject` create a variable, assign it the next free ID, register it in both tables and return the typed object, so you can subscribe to it immediately. `RemoveVariable()` takes a name or an ID. Adding a name that already exists logs a warning and returns `null` instead of overwriting.

{% hint style="info" %}
Lookups are type-strict and silent. `Set("Health", 1)` on a variable declared as **Float** does nothing and returns `false`, because `1` is an `int`. Likewise `Get<T>()` returns `default` for a missing or mistyped name without logging anything — unlike the original component, which warns. Check the return value, or use `HasVariable()` first.
{% endhint %}

{% hint style="info" %}
`ClearVar()` / `SetNull()` only affect **Object** variables; on any other type they do nothing.
{% endhint %}

The component also carries a **Blackboard/Print All Variables** context-menu item that dumps every ID, name, type and current value to the console.

## Where they are used

* **Local Variable Reaction** — under `Malbers/Variables/Local Variable` in any Reaction list: takes one variable row and writes it into the target's `MLocalVars`, doing nothing if the name is not present.
* **Local MVariable Condition** — under `General/Local MVariable` in any Conditions2 list: reads a named variable off a target `MLocalVars` and compares it against an authored value, with a number comparer for Int and Float types.
* **Check Local Variable** (AI Brain decision) — `Variables/Check Local Variable`: compares a variable on **Self**, on the current **Target**, or on the object held by a **Transform Hook** or **GameObject Var**.
* **Set Local Var** (AI Brain task) — `Variables/Set Local Var`: writes a list of variables onto **Self** or **Target**, then reports the task done on the same frame.

{% hint style="info" %}
All four target the original `MLocalVars` component. They do not see variables that live on **Local Variables 2**.
{% endhint %}

## API

```csharp
using MalbersAnimations;
using UnityEngine;

public class LocalVarsApiExample : MonoBehaviour
{
    public MLocalVars vars;              // Local Variables [BlackBoard]
    public MLocalVarsBlackboard board;   // Local Variables 2 [BlackBoard]

    // ── MLocalVars ─────────────────────────────────────────────────────────
    void OriginalComponent()
    {
        // Does the variable exist? (the dictionary is built on Start)
        if (!vars.HasVar("Ammo")) return;

        // Generic get / set – SetVar returns false when the value did not change
        int ammo = vars.GetVar<int>("Ammo");
        vars.SetVar("Ammo", ammo - 1);

        // Typed helpers
        int   i = vars.GetInt("Ammo");
        float f = vars.GetFloat("Stamina");
        bool  b = vars.GetBool("Alerted");
        string s = vars.GetString("State");

        vars.SetInt("Ammo", 30);
        vars.SetFloat("Stamina", 0.5f);
        vars.SetBool("Alerted", true);
        vars.SetString("State", "Patrol");

        // Bool shortcuts – one string argument, UnityEvent friendly
        vars.Var_Set_True("Alerted");
        vars.Var_Set_False("Alerted");

        // Pin: two UnityEvent calls instead of one two-argument call
        vars.Pin_Var("Ammo");
        vars.Pin_SetValue(30);             // int / float / bool / string /
                                           // Vector2 / Vector3 / GameObject /
                                           // Transform / Material / Object

        // Subscribe: the callback receives the NAME of the variable that changed
        vars.Subscribe("Ammo", OnNamedVarChanged);
        vars.Unsubscribe("Ammo", OnNamedVarChanged);
        vars.ClearListeners("Ammo");       // drops every listener on that name

        // Compare an authored LocalVar row against the stored value
        // bool ok = vars.Compare(someLocalVar, ComparerNumber.Greater);
    }

    void OnNamedVarChanged(string varName) { /* re-read the value yourself */ }

    // ── MLocalVarsBlackboard ──────────────────────────────────────────────
    void Blackboard()
    {
        // Existence, by name or by ID
        if (!board.HasVariable("Ammo")) return;
        bool byId = board.HasVariable(12);

        // Get – returns default when missing or mistyped (no warning)
        int   ammo  = board.Get<int>("Ammo");
        float stam  = board.Get<float>(7);
        if (board.TryGet("Ammo", out int safeAmmo)) { /* checked read */ }

        // Typed helpers (name only)
        int    i = board.GetInt("Ammo");
        float  f = board.GetFloat("Stamina");
        bool   b = board.GetBool("Alerted");
        string s = board.GetString("State");

        // Set – by name or by ID; false means "not found or wrong type"
        bool ok = board.Set("Ammo", 29);
        board.Set(7, 0.5f);

        board.SetInt("Ammo", 30);
        board.SetFloat("Stamina", 0.5f);
        board.SetBool("Alerted", true);
        board.SetString("State", "Patrol");
        board.Var_Set_True("Alerted");
        board.Var_Set_False("Alerted");

        board.ClearVar("Weapon");          // Object variables only
        board.SetNull("Weapon");           // same thing

        // Subscribe – typed, so the callback receives the VALUE
        board.Listen<int>("Ammo", OnAmmo);
        board.StopListening<int>("Ammo", OnAmmo);
        board.Listen<int>(12, OnAmmo);     // by ID
        board.Listen("Ammo", OnAnyValue);  // untyped Action<object>

        // Direct access to the variable object
        BlackboardVar raw = board.GetVariable("Ammo");
        BlackInt typed = board.GetVariable<int>("Ammo") as BlackInt;
        var all = board.GetAllVariables();

        // Create and destroy while the game runs
        BlackFloat hp = board.AddFloat("Health", 100f);
        board.AddInt("Kills", 0);
        board.AddBool("Alerted", false);
        board.AddString("State", "Idle");
        board.AddObject("Weapon", null, typeof(Animator));

        board.RenameVariable("Health", "HP");
        board.RemoveVariable("Kills");
        board.RebuildRegistry();           // after any manual rename/add/remove
    }

    void OnAmmo(int value) { }
    void OnAnyValue(object value) { }
}
```
