Configuration

Typed, persistent settings groups with validation and change events.

Typed, persistent settings groups with change events · Validates on every write · No-op replacement: NullConfiguration

What it does

The Configuration service stores game settings — audio volume, graphics quality, input rebinds, UI preferences — as strongly typed groups (plain serializable classes, one per settings area). Every Set validates and caches the new value. When the change is saved to disk and broadcast as an event depends on the mode: Immediate mode does both synchronously inside Set, while Deferred mode (the startup default) batches changes onto a pending queue — nothing is written or published until you call FlushPending<TGroup>() or FlushAllPending(). Batching keeps rapid UI slider drags from causing frame hitches, at the cost of one explicit flush call when you want consumers to react.

Quick example

An options-panel script that reads the current audio settings, updates them from a slider, and reacts to changes published on the Event Bus:

using System;
using CommonGameSystem.Core;
using UnityEngine;
using AudioSettings = CommonGameSystem.Core.AudioSettings;  // Unity has its own AudioSettings type

[DefaultExecutionOrder(100)]
public class AudioPanel : MonoBehaviour
{
    private IConfiguration _cfg;
    private IEventBus _bus;
    private IDisposable _subscription;

    private void Awake()
    {
        // Resolve and cache once
        _cfg = ServiceLocator.Resolve<IConfiguration>();
        _bus = ServiceLocator.Resolve<IEventBus>();

        // Pull the current value (no event fires on plain loads by default)
        var audio = _cfg.Get<AudioSettings>();
        UpdateSliders(audio);

        // Subscribe for future changes
        _subscription = _bus.Subscribe<ConfigurationChanged<AudioSettings>>(OnAudioChanged);
    }

    private void OnDestroy()
    {
        _subscription?.Dispose();
    }

    // Called when the user drags the volume slider
    public void OnMasterVolumeSliderChanged(float value)
    {
        var current = _cfg.Get<AudioSettings>();
        current.masterVolume = value;
        _cfg.Set(current);                        // Deferred (default): queues the change only
        _cfg.FlushPending<AudioSettings>();       // save + publish NOW so consumers react live
        // To batch disk writes during a drag instead, move the FlushPending
        // call to the slider's drag-end or Apply-button handler.
    }

    // Event handler (OldValue is always non-null for UserSet changes)
    private void OnAudioChanged(ConfigurationChanged<AudioSettings> evt)
    {
        if (evt.Source == ConfigurationChangeSource.UserSet)
            UpdateAudioMixer(evt.NewValue);
    }

    private void UpdateSliders(AudioSettings audio) { /* ... */ }
    private void UpdateAudioMixer(AudioSettings audio) { /* ... */ }
}

Full API surface

Core accessors

  • TGroup Get<TGroup>() where TGroup : class, new() — Fetch the current cached group. The first call for each group type loads it from storage; later calls are cache hits. If nothing is stored yet (fresh install, deleted data), you get the group's default — new TGroup(), or the factory registered in DefaultsProviders. Never throws for missing data; always returns a non-null instance.

  • void Set<TGroup>(TGroup value) where TGroup : class, new() — Replace the group and validate it. In Immediate mode this also saves and publishes the change event on the same call. In Deferred mode (the startup default) it only queues the change — saving and publishing both wait for a flush. Throws ArgumentNullException if value is null, and InvalidOperationException if called re-entrantly for the same group from inside a validator or a change-event handler (a guard against infinite set-publish-set loops).

  • void Reset<TGroup>() where TGroup : class, new() — Restore one group to its factory default, delete its saved data immediately (regardless of mode), and publish a change event with Source == UserSet. Any pending deferred Set for this group is discarded.

  • void ResetAll() — Restore every group to its default, delete all saved settings data immediately, and publish one change event per group. All pending deferred entries are discarded first, so a FlushAllPending() right after is a harmless no-op.

Deferred-mode flush (required under the startup default)

  • void FlushPending<TGroup>() where TGroup : class, new() — Save and publish one pending group. No-op in Immediate mode or when nothing is pending for this group. Typical call sites: a slider's drag-end handler, or an options-screen Apply button.
  • void FlushAllPending() — Save and publish all pending groups in a single pass. Typical call sites: options-screen close, scene transition. Also called automatically when the application quits — but on that path it only saves; it does not publish events (see Behavior & edge cases).

Event payload

Subscribe via IEventBus.Subscribe<ConfigurationChanged<TGroup>> on the Event Bus:

  • ConfigurationChanged<TGroup>.NewValue — the new settings (always non-null).
  • ConfigurationChanged<TGroup>.OldValue — the prior settings. Null only for load-from-disk events, and only if you opt in to those (see PublishOnHydrate below).
  • ConfigurationChanged<TGroup>.Source — where the change came from:
    • UserSet — a user-initiated change (slider, keybind dialog, Apply button). OldValue is always non-null. Act on these normally.
    • Hydrate — the group was just loaded from storage for the first time. Only emitted when PublishOnHydrate is enabled; OldValue is null. Framework-reserved — your code must not publish this value.
    • ConfigBackedRebind — reserved for the Input service's key-rebinding integration; input-related handlers should skip re-applying on this source to avoid feedback loops. Framework-reserved.
    • Unknown — fallback; treat like UserSet.

Options (tuning values, set at startup)

  • PersistCoalesceModeImmediate (save + publish synchronously inside Set) or Deferred (batch until a flush). The framework starts the service in Deferred mode.
  • PublishOnHydrate — if true, publish a ConfigurationChanged event when a group is first loaded from storage (its OldValue is null; its Source is Hydrate). Default false: the intended pattern is to pull the current value once in Awake and subscribe for later changes.
  • LoggerWarningThrottleSeconds — limit repeated validation warnings (clamped or sanitized values) to once per N seconds per field, so a slider drag cannot spam the Console. Default 1 second.
  • PlayerPrefsSoftCapBytes — when settings are stored in PlayerPrefs, warn once the payload approaches the ~64 KB PlayerPrefs string limit. Default 60000.
  • SaveServiceConfigGroupKeyPrefix — the save-slot prefix used when settings are stored through the Save/Load service. Default "config_", so the audio group is stored in slot config_AudioSettings.
  • DefaultsProviders / SetDefaultsProvider<TGroup>(Func<TGroup>) — per-group factory functions, for defaults that come from a designer-authored asset (for example, a ScriptableObject wired through the Inspector) instead of new TGroup().

Where settings are stored

By default the service persists through the Save/Load service, as one save slot per group (config_AudioSettings, config_GraphicsSettings, ...). The backend is chosen once, during startup: if the Save/Load service is active at that point, settings go through it; if it is absent or already replaced by NullSaveService, Configuration falls back to Unity's PlayerPrefs. Replacing the save service later in a Play session does not move settings that were already routed — the choice is a startup decision.

Built-in groups

The framework ships three ready-made groups used by its own services: AudioSettings, GraphicsSettings, and InputSettings. You can add your own groups — any class matching the constraints under Behavior & edge cases works with Get/Set immediately, no registration needed.

Turning it off

// In a test setup, or anywhere before consumers resolve it:
ServiceLocator.Replace<IConfiguration>(new NullConfiguration());

With NullConfiguration, every Get returns a fresh default, and every Set/Reset is a no-op — no event published, no disk write. Useful for headless/CI builds, or kiosk setups where player customization should not persist. Consumers keep working unchanged; they simply see default values and no change events.

Behavior & edge cases

  • Main thread only. All methods assert they run on the main thread (the check is stripped from Release builds). Do not call from worker threads; marshal to the main thread first if you need to.

  • Deferred mode is the default. Set alone neither saves nor publishes. You must call FlushPending<TGroup>() or FlushAllPending(), or consumers (for example, the Audio service's mixer) never see the change. A safety net on application quit saves any pending values, but it does not publish events — so relying on it means live consumers never react during play. Flush per change for live feedback, or on drag-end/"Apply" for batched writes.

  • Group types must be plain serializable classes. Mark them [Serializable] and use public fields only: no auto-properties, no UnityEngine.Object references, and they need a parameterless constructor. Unity's JsonUtility (the serializer underneath) silently ignores auto-properties — your values would quietly fail to persist.

  • Name clash with Unity types. The built-in group CommonGameSystem.Core.AudioSettings shares its name with UnityEngine.AudioSettings. If your file imports both namespaces, add a using alias (shown in the example above).

  • OldValue null check. With the default settings, events always carry a non-null OldValue. Only if you enable PublishOnHydrate can OldValue be null (on load-from-disk events) — check it before use in that case.

  • Re-entrant Set throws. Calling Set<TGroup> for a group from inside that group's own validator or change-event handler throws InvalidOperationException. React to changes; don't write the same group back from its own notification.

  • Your own group types and IL2CPP. The framework protects its three built-in groups from IL2CPP code stripping. If you build with IL2CPP, give your own group types a [Preserve] attribute plus a link.xml entry in your project.

  • Event Bus — subscribing to ConfigurationChanged<TGroup> events
  • Save/Load — the default storage backend for settings
  • Audio — a consumer of AudioSettings
  • Input — key rebinding stored through InputSettings
  • Bootstrap — startup order and the persistence backend registration