From ecf2309e1159b597b4b062fe051a67a3bc010bfd Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:01:10 +0800 Subject: [PATCH] =?UTF-8?q?docs(game):=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=86=85=E5=AE=B9=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 介绍面向静态游戏内容的 AI-First 配表方案 - 说明配置系统管理怪物、物品、技能、任务等静态内容数据 - 描述 YAML 作为配置源文件和 JSON Schema 作为结构描述的支持 - 展示推荐的目录结构和 Schema 示例 - 提供完整的接入模板包括 csproj 配置、启动引导和运行时读取 - 详述运行时校验行为和跨表引用机制 - 说明开发期热重载功能和 VS Code 工具集成 - 列出当前限制和独立 Config Studio 评估结论 --- docs/zh-CN/game/config-system.md | 229 ++++++++++++++++++++++++++++++- 1 file changed, 224 insertions(+), 5 deletions(-) diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index aa1dee07..8fdc20ea 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -82,6 +82,228 @@ dropItems: - slime_gel ``` +## 推荐接入模板 + +如果你准备在一个真实游戏项目里首次接入这套配置系统,建议直接采用下面这套目录与启动模板,而不是零散拼装。 + +### 目录模板 + +```text +GameProject/ +├─ GameProject.csproj +├─ Config/ +│ ├─ GameConfigBootstrap.cs +│ └─ GameConfigRuntime.cs +├─ config/ +│ ├─ monster/ +│ │ ├─ slime.yaml +│ │ └─ goblin.yaml +│ └─ item/ +│ └─ potion.yaml +└─ schemas/ + ├─ monster.schema.json + └─ item.schema.json +``` + +推荐约定如下: + +- `schemas/` 放所有 `*.schema.json`,由 Source Generator 自动拾取 +- `config/` 放运行时加载的 YAML 数据,一对象一文件 +- `Config/` 放你自己的接入代码,例如启动注册、热重载句柄和对外读取入口 + +### `csproj` 模板 + +如果你在仓库内直接用项目引用,最小模板可以写成下面这样: + +```xml + + + net10.0 + disable + enable + + + + + + + + + + + +``` + +这段配置的作用: + +- `GFramework.Game` 提供运行时 `YamlConfigLoader`、`ConfigRegistry` 和只读表实现 +- 三个 `ProjectReference(... OutputItemType="Analyzer")` 把生成器接进当前消费者项目 +- `GeWuYou.GFramework.SourceGenerators.targets` 自动把 `schemas/**/*.schema.json` 加入 `AdditionalFiles` + +如果你使用打包后的 NuGet,而不是仓库内项目引用,原则保持不变: + +- 运行时项目需要引用 `GeWuYou.GFramework.Game` +- 生成器项目需要引用 `GeWuYou.GFramework.SourceGenerators` +- schema 目录默认仍然是 `schemas/` + +如果你的 schema 不放在默认目录,可以在项目文件里覆盖: + +```xml + + GameSchemas + +``` + +### 启动引导模板 + +推荐把配置系统的初始化收敛到一个单独入口,避免把 `YamlConfigLoader` 注册逻辑散落到多个启动脚本中: + +```csharp +using GFramework.Core.Abstractions.Events; +using GFramework.Game.Abstractions.Config; +using GFramework.Game.Config; +using GFramework.Game.Config.Generated; + +namespace GameProject.Config; + +/// +/// 负责初始化游戏内容配置运行时入口。 +/// +public sealed class GameConfigBootstrap : IDisposable +{ + private readonly ConfigRegistry _registry = new(); + private IUnRegister? _hotReload; + + /// + /// 获取当前游戏进程共享的配置注册表。 + /// + public IConfigRegistry Registry => _registry; + + /// + /// 从指定配置根目录加载所有已注册配置表。 + /// + /// 配置根目录。 + /// 是否启用开发期热重载。 + public async Task InitializeAsync(string configRootPath, bool enableHotReload = false) + { + var loader = new YamlConfigLoader(configRootPath) + .RegisterMonsterTable() + .RegisterItemTable(); + + await loader.LoadAsync(_registry); + + if (enableHotReload) + { + _hotReload = loader.EnableHotReload( + _registry, + onTableReloaded: tableName => Console.WriteLine($"Reloaded config table: {tableName}"), + onTableReloadFailed: static (_, exception) => + { + var diagnostic = (exception as ConfigLoadException)?.Diagnostic; + Console.WriteLine($"Config reload failed: {diagnostic?.FailureKind}"); + }); + } + } + + /// + /// 停止开发期热重载并释放相关资源。 + /// + public void Dispose() + { + _hotReload?.UnRegister(); + } +} +``` + +这段模板刻意遵循几个约定: + +- 优先使用生成器产出的 `Register*Table()`,避免手写表名、路径和 key selector +- 由一个长生命周期对象持有 `ConfigRegistry` +- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏 + +### 运行时读取模板 + +推荐不要在业务代码里直接散落字符串表名查询,而是统一依赖生成的强类型入口: + +```csharp +using GFramework.Game.Config.Generated; + +namespace GameProject.Config; + +/// +/// 封装游戏内容配置读取入口。 +/// +public sealed class GameConfigRuntime +{ + private readonly IConfigRegistry _registry; + + /// + /// 使用已初始化的配置注册表创建读取入口。 + /// + /// 配置注册表。 + public GameConfigRuntime(IConfigRegistry registry) + { + _registry = registry ?? throw new ArgumentNullException(nameof(registry)); + } + + /// + /// 获取指定怪物配置。 + /// + /// 怪物主键。 + /// 强类型怪物配置。 + public MonsterConfig GetMonster(int monsterId) + { + return _registry.GetMonsterTable().Get(monsterId); + } + + /// + /// 获取怪物配置表。 + /// + /// 生成的强类型表包装。 + public MonsterTable GetMonsterTable() + { + return _registry.GetMonsterTable(); + } +} +``` + +这样做的收益: + +- 配置系统对业务层暴露的是强类型表,而不是 `"monster"` 这类 magic string +- 后续如果你要复用配置域、schema 路径或引用元数据,可以继续依赖 `MonsterConfigBindings.Metadata` 和 + `MonsterConfigBindings.References` +- 如果未来把配置初始化接入 `Architecture` 或 `Module`,迁移成本也更低 + +### 热重载模板 + +如果你希望把开发期热重载显式收敛为一个可选能力,建议把失败诊断一起写进模板,而不是只打印异常文本: + +```csharp +var hotReload = loader.EnableHotReload( + registry, + onTableReloaded: tableName => Console.WriteLine($"Reloaded: {tableName}"), + onTableReloadFailed: (tableName, exception) => + { + var diagnostic = (exception as ConfigLoadException)?.Diagnostic; + Console.WriteLine($"Reload failed: {tableName}"); + Console.WriteLine($"Failure kind: {diagnostic?.FailureKind}"); + Console.WriteLine($"Yaml path: {diagnostic?.YamlPath}"); + Console.WriteLine($"Display path: {diagnostic?.DisplayPath}"); + }); +``` + +建议只在开发期启用这项能力: + +- 生产环境默认更适合静态加载和固定生命周期 +- 热重载失败时应优先依赖 `ConfigLoadException.Diagnostic` 做稳定日志或 UI 提示 +- 如果你的项目已经有统一日志系统,建议在这里把诊断字段转成结构化日志,而不是拼接一整段字符串 + ## 运行时接入 当你希望加载后的配置在运行时以只读表形式暴露时,优先使用生成器产出的注册与访问辅助: @@ -220,14 +442,11 @@ catch (ConfigLoadException exception) ```csharp using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; +using GFramework.Game.Config.Generated; var registry = new ConfigRegistry(); var loader = new YamlConfigLoader("config-root") - .RegisterTable( - "monster", - "monster", - "schemas/monster.schema.json", - static config => config.Id); + .RegisterMonsterTable(); await loader.LoadAsync(registry);