配置

类型化、可持久化的设置组,带验证与变更事件。

类型化、可持久化的设置组,带变更事件 · 每次写入都验证 · 无操作替代:NullConfiguration

功能概述

配置服务把游戏设置——音量、画质、输入改键、UI 偏好——存储为强类型的(普通的可序列化类,每个设置领域一个)。每次 Set 都会验证并缓存新值。变更何时保存到磁盘并作为事件广播取决于模式:Immediate 模式在 Set 内部同步完成两者;而 Deferred 模式(启动默认)把变更批量放入待定队列——在你调用 FlushPending<TGroup>()FlushAllPending() 之前,什么都不会写入或发布。批处理可以避免 UI 滑块的快速拖动造成帧卡顿,代价是当你希望消费者做出反应时,需要一次显式的 flush 调用。

快速示例

一个选项面板脚本:读取当前音频设置,根据滑块更新它们,并对事件总线上发布的变更做出反应:

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) { /* ... */ }
}

完整 API 一览

核心访问器

  • TGroup Get<TGroup>() where TGroup : class, new() —— 获取当前缓存的组。每个组类型的第一次调用会从存储加载;之后的调用命中缓存。如果尚无存储数据(全新安装、数据被删除),你会得到该组的默认值——new TGroup(),或注册在 DefaultsProviders 中的工厂。数据缺失时绝不抛出;总是返回非 null 实例。

  • void Set<TGroup>(TGroup value) where TGroup : class, new() —— 替换该组并验证它。在 Immediate 模式下,同一次调用还会保存并发布变更事件。在 Deferred 模式(启动默认)下它只是把变更入队——保存发布都要等到 flush。value 为 null 时抛出 ArgumentNullException;从该组的验证器或变更事件处理器内部对同一组重入调用时抛出 InvalidOperationException(这是防止无限 set-publish-set 循环的保护)。

  • void Reset<TGroup>() where TGroup : class, new() —— 把一个组恢复为出厂默认值,立即删除其已保存数据(与模式无关),并发布一个 Source == UserSet 的变更事件。该组任何待定的延迟 Set 都会被丢弃。

  • void ResetAll() —— 把每个组都恢复为默认值,立即删除所有已保存的设置数据,并为每个组发布一个变更事件。所有待定的延迟条目会先被丢弃,因此紧随其后的 FlushAllPending() 是无害的无操作。

Deferred 模式的 flush(启动默认模式下必须调用)

  • void FlushPending<TGroup>() where TGroup : class, new() —— 保存并发布一个待定的组。在 Immediate 模式下、或该组没有待定项时为无操作。典型调用位置:滑块的拖动结束处理器,或选项界面的 Apply 按钮。
  • void FlushAllPending() —— 在一次遍历中保存并发布所有待定的组。典型调用位置:选项界面关闭、场景转换。应用退出时也会自动调用——但在那条路径上它只会保存;不会发布事件(参见"行为与边界情况")。

事件载荷

通过事件总线IEventBus.Subscribe<ConfigurationChanged<TGroup>> 订阅:

  • ConfigurationChanged<TGroup>.NewValue —— 新设置(总是非 null)。
  • ConfigurationChanged<TGroup>.OldValue —— 之前的设置。仅在从磁盘加载的事件中为 null,且仅当你主动启用了这类事件(见下方的 PublishOnHydrate)。
  • ConfigurationChanged<TGroup>.Source —— 变更的来源:
    • UserSet —— 用户发起的变更(滑块、改键对话框、Apply 按钮)。OldValue 总是非 null。正常处理这类事件即可。
    • Hydrate —— 该组刚刚第一次从存储加载。仅在启用 PublishOnHydrate 时发出;OldValue 为 null。框架保留——你的代码不得发布此值。
    • ConfigBackedRebind —— 为输入服务的按键重绑定集成保留;与输入相关的处理器应跳过对该来源的重新应用,以避免反馈循环。框架保留。
    • Unknown —— 兜底值;按 UserSet 处理。

选项(调优值,在启动时设置)

  • PersistCoalesceMode —— Immediate(在 Set 内同步保存并发布)或 Deferred(批量累积直到 flush)。框架以 Deferred 模式启动该服务。
  • PublishOnHydrate —— 若为 true,当组第一次从存储加载时发布一个 ConfigurationChanged 事件(其 OldValue 为 null;其 SourceHydrate)。默认 false:预期的模式是在 Awake拉取一次当前值,然后订阅后续的变更。
  • LoggerWarningThrottleSeconds —— 把重复的验证警告(值被钳制或净化)限制为每字段每 N 秒一次,使滑块拖动无法刷屏 Console。默认 1 秒。
  • PlayerPrefsSoftCapBytes —— 当设置存储于 PlayerPrefs 时,负载接近 PlayerPrefs 约 64 KB 的字符串上限时发出警告。默认 60000。
  • SaveServiceConfigGroupKeyPrefix —— 设置经由存档/读档服务存储时使用的存档槽前缀。默认 "config_",因此音频组存储在槽 config_AudioSettings 中。
  • DefaultsProviders / SetDefaultsProvider<TGroup>(Func<TGroup>) —— 按组的工厂函数,用于默认值来自设计师制作的资源(例如通过 Inspector 接线的 ScriptableObject)而非 new TGroup() 的情况。

设置存储在哪里

默认情况下,该服务通过存档/读档服务持久化,每个组一个存档槽(config_AudioSettingsconfig_GraphicsSettings……)。后端在启动期间选定一次:如果那时存档/读档服务处于激活状态,设置就走它;如果它缺失或已被替换为 NullSaveService,配置服务会回退到 Unity 的 PlayerPrefs。在 Play 会话中稍后替换存档服务不会迁移已经路由过的设置——这个选择是启动时的一次性决定。

内置设置组

框架自带三个现成的组,供其自身服务使用:AudioSettingsGraphicsSettingsInputSettings。你可以添加自己的组——任何满足"行为与边界情况"中约束的类都能立即配合 Get/Set 工作,无需注册。

关闭此服务

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

使用 NullConfiguration 时,每次 Get 都返回一个全新的默认值,每次 Set/Reset 都是无操作——不发布事件,不写磁盘。适用于无头/CI 构建,或不应保留玩家自定义的自助终端(kiosk)场景。消费者无需改动即可继续工作;它们只是看到默认值,并且收不到变更事件。

行为与边界情况

  • 仅限主线程。 所有方法都会断言自己运行在主线程上(该检查在 Release 构建中被剔除)。不要从工作线程调用;如有需要,先调度回主线程。

  • Deferred 模式是默认值。 仅调用 Set 既不保存也不发布。你必须调用 FlushPending<TGroup>()FlushAllPending(),否则消费者(例如音频服务的混音器)永远看不到变更。应用退出时的安全网会保存所有待定值,但它不会发布事件——因此依赖它意味着运行中的消费者在游戏过程中永远不会响应。想要实时反馈就按变更 flush;想要批量写入就在拖动结束/"Apply"时 flush。

  • 组类型必须是普通的可序列化类。 标记 [Serializable],且只使用公共字段:不要自动属性、不要 UnityEngine.Object 引用,并且需要无参构造函数。底层序列化器 Unity JsonUtility 会静默忽略自动属性——你的值会悄无声息地无法持久化。

  • 与 Unity 类型的命名冲突。 内置组 CommonGameSystem.Core.AudioSettingsUnityEngine.AudioSettings 同名。如果你的文件同时导入两个命名空间,请添加 using 别名(见上方示例)。

  • OldValue 的 null 检查。 在默认设置下,事件携带的 OldValue 总是非 null。只有启用 PublishOnHydrate 之后,OldValue 才可能为 null(在从磁盘加载的事件中)——那种情况下使用前先检查。

  • 重入的 Set 会抛出。 从某个组自己的验证器或变更事件处理器内部对该组调用 Set<TGroup> 会抛出 InvalidOperationException。响应变更即可;不要在同一个组自己的通知里把它写回去。

  • 你自己的组类型与 IL2CPP。 框架保护其三个内置组不被 IL2CPP 代码裁剪移除。如果你用 IL2CPP 构建,请给自己的组类型加上 [Preserve] 特性,并在项目中添加 link.xml 条目。

相关页面

  • 事件总线 —— 订阅 ConfigurationChanged<TGroup> 事件
  • 存档/读档 —— 设置的默认存储后端
  • 音频 —— AudioSettings 的一个消费者
  • 输入 —— 通过 InputSettings 存储的按键重绑定
  • 启动引导 —— 启动顺序与持久化后端注册