配置系统 Config
把 Excel / 模板定义的配置表,在生成期烘焙成不可变 Blob,运行期以世界无关的静态表提供无分配查询。
一句话:编辑期是 ScriptableObject + 生成的代码,运行期只有一个 Blob 指针。
全链路
① 配置模板 ConfigTemplate 定义表结构与字段
↓(Editor:代码生成)
② 生成的 C# 类 [CfgAsset] blittable struct,命名空间 {root}.config
↓(Editor:数据编辑器填数据)
③ 配置资产 .asset 每行一个数据条目
↓(Editor:导出)
④ BlobAssetReference<BlobRoot<TRow>>
↓(Bootstrap:注册)
⑤ ConfigRegistry 静态表(框架内部,宿主不可见)
↓(运行期:查询)
⑥ 玩法 / UI 走门面 {ns}.ConfigManager.{Type}.GetByID(id)
核心类
| 类 | 位置 | 职责 |
|---|---|---|
ConfigTemplate | Runtime | 表结构模板(本身也是配置资产,可在数据编辑器里重开再生成) |
{ns}.ConfigManager(生成) | ConfigManager.cs | 宿主唯一查询入口:每类型一个嵌套类,GetByID(id) → 可写 ref |
ConfigRegistry | Runtime | 运行期低层注册表——框架内部机制,宿主禁用 |
ConfigAssetFactory | Editor | 配置资产唯一创建入口(强制发号) |
ConfigExportService | Editor | 把数据烘焙成 Blob 并导出(客户端/服务端可分开) |
创建配置资产(发号服)
创建任何 [CfgAsset] 配置资产,必须调用 ConfigAssetFactory,禁止 ScriptableObject.CreateInstance<T>() + 手填 ID / GUID——手填会绕过发号服,可能跨工程 / 跨机冲突。
// 在编辑器工具 / 菜单里
ConfigAssetFactory.Create<CharacterConfig>("Boss_01");
ConfigAssetFactory.Create(typeof(CharacterConfig), "Boss_01");
ConfigAssetFactory.CreateMany(typeof(CharacterConfig), "Mob", 10);
⚠
ID / GUID 必须由发号服派发;id == 0 视为未分配。复制资产产生的重复 ID 会被 DuplicateIdGuard 在复制后自动重派发,应用层无需记忆「别复制」。运行期查询(门面 ConfigManager)
代码生成时为每个配置类型产出嵌套查询类,静态、无 World 依赖、零字符串键:
// ① 按 id 查一行(零拷贝 ref,未命中抛 KeyNotFoundException)
ref var row = ref ConfigManager.FashionRow.GetByID(1001);
int price = row.Price;
// ② 遍历全表:Count 作循环边界,GetByIndex(i) 按行序取(同样零拷贝)
for (int i = 0; i < ConfigManager.FashionRow.Count; i++)
{
ref var r = ref ConfigManager.FashionRow.GetByIndex(i);
Debug.Log(r.Price);
}
// ③ 缺失兜底:先预判存在性,再取行
if (ConfigManager.FashionRow.Contains(1001))
{
ref var safe = ref ConfigManager.FashionRow.GetByID(1001);
}
✕
blob 行必须
ref 访问(EA0001):GetByID 返回的可写 ref 必须整个放进 ref 局部变量。Cfg{Type} 行含 BlobString / BlobArray,其偏移是自相对的——值拷贝到栈上后寻址错位,开发构包直接抛 InvalidOperationException,正式包静默读垃圾。ref var row = ref ConfigManager.FashionRow.GetByID(1001); // ✅ 正确
var row = ConfigManager.FashionRow.GetByID(1001); // ❌ EA0001 编译错
CfgFashionRow row; row = ConfigManager.FashionRow.GetByID(1001); // ❌ 值拷贝,寻址错位
ref 局部变量的硬约束:不能跨方法传递(传 id,方法内部自查)、不能存进字段 / 数组 / List、不能被 lambda 捕获、不能跨帧缓存。
配置引用字段(Reference)
若某字段要引用另一个配置,必须用 ConfigFieldType.Reference(UI 称「配置引用」)直接选目标类型,而非定义 long 手填 ID。导出时在「取成员」里选要落盘的成员(如 id)。引用关系由框架维护,重派发 / 复制不会悬空。
◈
运行期经生成式静态门面
ConfigManager.{Type}.GetByID(id) 取回(零拷贝 ref)。禁止调用 ConfigRegistry——它是框架低层注册表,不受宿主兼容承诺,且历史上曾与门面同名造成 CS0104。实例基数(Cardinality)
一个类型在工程中能同时存在几个资产实例:
| 基数 | 含义 | 声明方式 |
|---|---|---|
Multiple(默认) | 可存在任意多个 | 不标 unique |
Single | 全工程唯一 | [CfgAsset(name, order, unique: true)] |
// 全工程唯一的音频总览
[CfgAsset("音频/音频总览", 60, unique: true)]
public class AudioCatalogOverview : ConfigBase { }
// 可多个的角色配置(默认,不写 unique)
[CfgAsset("角色/角色配置", 10)]
public class CharacterConfig : ConfigBase { }
Single 在创建期即强制:已存在即抛 InvalidOperationException,CreateMany 直接不支持;导出期还有第二道查重闸门。
编辑器入口
| 菜单 | 用途 |
|---|---|
HYC Framework / Config / Config Window | 配置窗口 |
HYC Framework / Config / Data Editor | 数据编辑器(填表,主力) |
HYC Framework / Config / Validate Config | 配置结构校验 |
常见坑
TRow必须 blittable:字符串选BlobString/FixedString,Vector3→Unity.Mathematics.float3,否则烘焙失败。- 门面取行必须
ref var row = ref ...(EA0001),否则寻址错位。 GetByID未命中抛KeyNotFoundException:宽松语义用Contains(id)预判,不要为绕开去调低层TryGet。- 引导期注册完所有表之后再开始查询(
Materialize缓存会被Register清空)。 - 不需要 World 句柄,UI 层可直接查。
◈
字段类型、导出设置、检查规则详见框架
Documentation~/11-字段类型与导出设置.md;blittable 铁律见 99-Blob-与数据约定.md。