Extensibility

Adding a setting touches one asset and one list. It is in the menu the next time you press Play. Code comes in only when the value has to do something, and even then it is usually one line.

Add a setting

The short way: the editor's Settings Profile page (Tools → WaggleBum → WagSettings → Editor), Add Setting. In the dialog pick a kind, type a key — checked as you type — set a default and, for a slider, a range, press Create. The asset is made next to the profile and appended to it. Done; press Play.

The Add Setting dialog with a key typed and the asset it will create

By hand:

1. Create the asset. Assets → Create → WaggleBum → WagSettings → the type you need:

TypeControl in the menuFields that matter
Float SettingsliderKey, Default, Minimum, Maximum
Int Settingslider with whole numbersKey, Default, Minimum, Maximum
Bool SettingtoggleKey, Default
Enum Settingdropdown of the Options you listKey, Default (an index), Options
String Settingtext fieldKey, Default

The Assets → Create → WaggleBum → WagSettings menu

The key is category.name: the part before the dot is the tab, the part after becomes the label. gameplay.difficulty lands in a Gameplay tab as "Difficulty"; gameplay.cameraShake reads "Camera Shake". A key with no dot lands under General.

The key is the storage key. Changing it after release orphans every player's saved value, so treat it as permanent. Keys may not contain " \ { } , : or control characters.

2. Add it to the SettingsProfile. Drop the asset into the profile's list — the one setup generated in Assets/WagSettings/Resources, or your own. Order in the list is order in the menu. Re-running setup later keeps anything you added by hand; it only replaces the entries it wrote itself.

A SettingsProfile in the Inspector with its list of settings

3. Press Play. In the UI Toolkit menu the row is there: right tab, right control, right label, and Apply / Revert / Reset, search, dirty marking and persistence already work for it.

In the uGUI menu, rows are placed by hand, so add one: duplicate an existing row in the prefab (Sample Scenes/Quick Start/Prefabs/WagSettingsMenu (uGUI).prefab has one of each kind), put it in the right category panel, and set the binder's Key. That is the whole difference between the two frameworks — see Getting Started.

Reading it

From anywhere, once a WagSettingsManager is running:

int difficulty = WagSettings.Get<int>("gameplay.difficulty");                 // the pending value
WagSettings.Definition<int>("gameplay.difficulty").Changed += value => { };   // Set, Revert, Reset

For most gameplay settings that is all the code there is: read it where it matters, or react to the event. The rest of this page is for values that have to drive Unity itself.

The inspector checks the key as you type it

A malformed key otherwise fails only when the setting is first used — often in a different scene, on a different day, a long way from where the mistake was made. The inspector reports it immediately:

Reported asExamples
Cannot be usedErrorempty, a forbidden character, leading or trailing whitespace
Works, but unconventionalWarningno category prefix, Audio.Master casing, an empty part between dots

The Inspector flagging a malformed key on a setting asset

Warnings are advice, not obstacles — a key with no category still works, it just lands under "General".

Seeing the current value

The inspector shows only the authored fields when you are not playing, because the current value is runtime state and is deliberately never saved into the asset — a player's choices belong in their save data, not in a file you ship.

Enter play mode and the inspector shows the live value, whether it has unapplied changes, and buttons to revert or reset it. An asset whose Default still reads 0.8 while the game is plainly playing at 0.25 is working correctly.

Write an applier

An applier turns a value into a Unity effect on a schedule — live as the player drags, or only when they press Apply. It knows about UnityEngine; the core does not. Derive from SettingApplierBase and supply two things: which settings you track, and how to push them.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using WaggleBum.WagSettings;

public sealed class MusicVolumeApplier : SettingApplierBase
{
    [SerializeField] private AudioMixer _mixer = null!;

    private ISettingDefinition<float> _music = null!;

    protected override void ResolveSettings(SettingsRegistry registry, IList<ISettingDefinition> tracked)
    {
        _music = Require<float>(registry, "audio.music");
        tracked.Add(_music);
    }

    public override void ApplyCurrentValues()
    {
        _mixer.SetFloat("MusicVolume", AudioVolume.LinearToDecibels(_music.Value));
    }
}

Put the component under the WagSettings Manager. The manager attaches everything under it when it starts; there is nothing to call. (An applier that has to live elsewhere: WagSettings.Manager.Attach(applier), and detach it yourself.)

The base class handles the two things hand-written appliers get wrong:

  1. Every += has a matching -=. Settings are ScriptableObjects, so they outlive your scene. A subscription you never release keeps the component, its GameObject, and everything they reference alive across scene loads. Attach/Detach and Unity's OnEnable/OnDisable are wired for you.
  2. The persisted value is applied on attach. An applier that only reacts to Changed leaves the game running on whatever was authored in the scene until the player next touches a setting — a bug that hides until someone reloads a scene.

The third rule is still yours: never write to the store from an applier. Appliers read; the registry writes, once, in Apply().

Require throws on a key the registry does not hold, naming it. That is deliberate: a slider that silently drives nothing looks like a broken game rather than a broken setup.

Immediate or on Apply

The Apply Mode field on every applier decides when it acts:

ModeFires onUse for
Immediate (default)every change to a tracked settingcheap changes the player should hear or see as they drag — volume, UI scale
OnApplythe registry committing, via Apply() or Load()anything expensive or disruptive — resolution, quality tier, window mode

OnApply is what stops a resolution setting from thrashing the window while a dropdown is being scrolled. It listens to SettingsRegistry.Committed, which fires after an Apply() that wrote something and after a Load() — both cases where the committed values genuinely changed.

Note that ApplyCurrentValues() re-applies all of an applier's tracked settings, not just the one that changed. For the handful of settings a single applier owns that is cheaper than routing per-setting callbacks, and it keeps the method a straightforward "push the current state into Unity".

Add a new setting type

Only needed for a value the built-in types cannot express.

[CreateAssetMenu(fileName = "New Vector2Setting", menuName = "WaggleBum/WagSettings/Vector2 Setting")]
public sealed class Vector2Setting : TypedSettingAsset<Vector2>
{
}

That is the whole implementation — TypedSettingAsset<T> supplies the key, default, value, change event and dirty tracking. The type must be serializable by Unity, and the store you use must be able to persist it (PlayerPrefsStore handles the primitive types; see Persistence).

For a fixed list of choices, prefer EnumSetting over a new type — it stores an index plus display names and already works with the dropdown UI.

Write a store

Implement ISettingsStore to persist somewhere else — a save file, a backend, a test double.

public sealed class MyStore : ISettingsStore
{
    public bool Has(string key)                  { ... }
    public T    Load<T>(string key, T fallback)  { ... }
    public void Save<T>(string key, T value)     { /* buffer, do not commit */ }
    public void Flush()                          { /* commit the whole batch */ }
    public void Delete(string key)               { /* buffer */ }
}

The one contract that matters: Save and Delete buffer, Flush commits. The registry writes every dirty setting and then flushes exactly once, so a store that commits per-write turns one Apply into forty round trips.

Round-trip test any store you write — set, save, flush, reload, assert — the same shape as the tests in Tests/EditMode.