配置
类型化、可持久化的设置组,带验证与变更事件。
类型化、可持久化的设置组,带变更事件 · 每次写入都验证 · 无操作替代:
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;其Source为Hydrate)。默认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_AudioSettings、config_GraphicsSettings……)。后端在启动期间选定一次:如果那时存档/读档服务处于激活状态,设置就走它;如果它缺失或已被替换为 NullSaveService,配置服务会回退到 Unity 的 PlayerPrefs。在 Play 会话中稍后替换存档服务不会迁移已经路由过的设置——这个选择是启动时的一次性决定。
内置设置组
框架自带三个现成的组,供其自身服务使用:AudioSettings、GraphicsSettings 和 InputSettings。你可以添加自己的组——任何满足"行为与边界情况"中约束的类都能立即配合 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引用,并且需要无参构造函数。底层序列化器 UnityJsonUtility会静默忽略自动属性——你的值会悄无声息地无法持久化。 -
与 Unity 类型的命名冲突。 内置组
CommonGameSystem.Core.AudioSettings与UnityEngine.AudioSettings同名。如果你的文件同时导入两个命名空间,请添加using别名(见上方示例)。 -
OldValue的 null 检查。 在默认设置下,事件携带的OldValue总是非 null。只有启用PublishOnHydrate之后,OldValue才可能为 null(在从磁盘加载的事件中)——那种情况下使用前先检查。 -
重入的
Set会抛出。 从某个组自己的验证器或变更事件处理器内部对该组调用Set<TGroup>会抛出InvalidOperationException。响应变更即可;不要在同一个组自己的通知里把它写回去。 -
你自己的组类型与 IL2CPP。 框架保护其三个内置组不被 IL2CPP 代码裁剪移除。如果你用 IL2CPP 构建,请给自己的组类型加上
[Preserve]特性,并在项目中添加link.xml条目。