mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-02 11:44:28 +08:00
docs(game): 添加游戏模块完整文档
- 创建了 GFramework.Game 模块的全面文档 - 包含架构模块系统、资产管理、存储系统和序列化系统详解 - 提供了 AbstractModule、AbstractAssetCatalogUtility 等核心组件使用示例 - 添加了分层存储、加密存储和缓存存储的实现方案 - 集成了 JSON 序列化、自定义转换器和版本化数据管理 - 提供了完整的游戏数据管理系统和自动保存系统实现 - 修改了 VitePress 配置文件
This commit is contained in:
parent
91f0375461
commit
b87e511334
12
AGENTS.md
12
AGENTS.md
@ -200,6 +200,17 @@ bash scripts/validate-csharp-naming.sh
|
||||
- The main documentation site lives under `docs/`, with Chinese content under `docs/zh-CN/`.
|
||||
- Keep code samples, package names, and command examples aligned with the current repository state.
|
||||
- Prefer documenting behavior and design intent, not only API surface.
|
||||
- When a feature is added, removed, renamed, or substantially refactored, contributors MUST update or create the
|
||||
corresponding user-facing integration documentation in `docs/zh-CN/` in the same change.
|
||||
- For integration-oriented features such as the AI-First config system, documentation MUST cover:
|
||||
- project directory layout and file conventions
|
||||
- required project or package wiring
|
||||
- minimal working usage example
|
||||
- migration or compatibility notes when behavior changes
|
||||
- If an existing documentation page no longer reflects the current implementation, fixing the code without fixing the
|
||||
documentation is considered incomplete work.
|
||||
- Do not rely on “the code is self-explanatory” for framework features that consumers need to adopt; write the
|
||||
adoption path down so future users do not need to rediscover it from source.
|
||||
|
||||
### Documentation Preview
|
||||
|
||||
@ -218,3 +229,4 @@ Before considering work complete, confirm:
|
||||
- Relevant tests were added or updated
|
||||
- Sensitive or unsafe behavior was not introduced
|
||||
- User-facing documentation is updated when needed
|
||||
- Feature adoption docs under `docs/zh-CN/` were added or updated when functionality was added, removed, or refactored
|
||||
|
||||
@ -9,6 +9,8 @@ namespace GFramework.Game.Tests.Config;
|
||||
[TestFixture]
|
||||
public class YamlConfigLoaderTests
|
||||
{
|
||||
private string _rootPath = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
|
||||
/// </summary>
|
||||
@ -31,8 +33,6 @@ public class YamlConfigLoaderTests
|
||||
}
|
||||
}
|
||||
|
||||
private string _rootPath = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 验证加载器能够扫描 YAML 文件并将结果写入注册表。
|
||||
/// </summary>
|
||||
@ -155,6 +155,182 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用 schema 校验后,缺失必填字段会在反序列化前被拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Required_Property_Is_Missing_According_To_Schema()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
hp: 10
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"hp": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain("name"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用 schema 校验后,类型不匹配的标量字段会被拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Property_Type_Does_Not_Match_Schema()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
hp: high
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"hp": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain("hp"));
|
||||
Assert.That(exception!.Message, Does.Contain("integer"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Unknown_Property_Is_Present_In_Schema_Bound_Mode()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
attackPower: 2
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"hp": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain("attackPower"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组字段的元素类型会按 schema 校验。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Array_Item_Type_Does_Not_Match_Schema()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 1
|
||||
- potion
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>(
|
||||
"monster",
|
||||
"monster",
|
||||
"schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Message, Does.Contain("dropRates"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建测试用配置文件。
|
||||
/// </summary>
|
||||
@ -172,6 +348,16 @@ public class YamlConfigLoaderTests
|
||||
File.WriteAllText(fullPath, content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建测试用 schema 文件。
|
||||
/// </summary>
|
||||
/// <param name="relativePath">相对根目录的文件路径。</param>
|
||||
/// <param name="content">文件内容。</param>
|
||||
private void CreateSchemaFile(string relativePath, string content)
|
||||
{
|
||||
CreateConfigFile(relativePath, content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于 YAML 加载测试的最小怪物配置类型。
|
||||
/// </summary>
|
||||
@ -193,6 +379,27 @@ public class YamlConfigLoaderTests
|
||||
public int Hp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于数组 schema 校验测试的最小怪物配置类型。
|
||||
/// </summary>
|
||||
private sealed class MonsterConfigIntegerArrayStub
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置掉落率列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> DropRates { get; set; } = Array.Empty<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证注册表一致性的现有配置类型。
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using System.IO;
|
||||
using GFramework.Game.Abstractions.Config;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
@ -16,6 +14,9 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace.";
|
||||
private const string RelativePathCannotBeNullOrWhiteSpaceMessage = "Relative path cannot be null or whitespace.";
|
||||
|
||||
private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage =
|
||||
"Schema relative path cannot be null or whitespace.";
|
||||
|
||||
private readonly IDeserializer _deserializer;
|
||||
private readonly List<IYamlTableRegistration> _registrations = new();
|
||||
private readonly string _rootPath;
|
||||
@ -86,6 +87,41 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer = null)
|
||||
where TKey : notnull
|
||||
{
|
||||
return RegisterTableCore(tableName, relativePath, null, keySelector, comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个带 schema 校验的 YAML 配置表定义。
|
||||
/// 该重载会在 YAML 反序列化之前使用指定 schema 拒绝未知字段、缺失必填字段和基础类型错误,
|
||||
/// 以避免错误配置以默认值形式悄悄进入运行时。
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">配置主键类型。</typeparam>
|
||||
/// <typeparam name="TValue">配置值类型。</typeparam>
|
||||
/// <param name="tableName">配置表名称。</param>
|
||||
/// <param name="relativePath">相对配置根目录的子目录。</param>
|
||||
/// <param name="schemaRelativePath">相对配置根目录的 schema 文件路径。</param>
|
||||
/// <param name="keySelector">配置项主键提取器。</param>
|
||||
/// <param name="comparer">可选主键比较器。</param>
|
||||
/// <returns>当前加载器实例,以便链式注册。</returns>
|
||||
public YamlConfigLoader RegisterTable<TKey, TValue>(
|
||||
string tableName,
|
||||
string relativePath,
|
||||
string schemaRelativePath,
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer = null)
|
||||
where TKey : notnull
|
||||
{
|
||||
return RegisterTableCore(tableName, relativePath, schemaRelativePath, keySelector, comparer);
|
||||
}
|
||||
|
||||
private YamlConfigLoader RegisterTableCore<TKey, TValue>(
|
||||
string tableName,
|
||||
string relativePath,
|
||||
string? schemaRelativePath,
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tableName))
|
||||
{
|
||||
@ -99,7 +135,20 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
|
||||
ArgumentNullException.ThrowIfNull(keySelector);
|
||||
|
||||
_registrations.Add(new YamlTableRegistration<TKey, TValue>(tableName, relativePath, keySelector, comparer));
|
||||
if (schemaRelativePath != null && string.IsNullOrWhiteSpace(schemaRelativePath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
SchemaRelativePathCannotBeNullOrWhiteSpaceMessage,
|
||||
nameof(schemaRelativePath));
|
||||
}
|
||||
|
||||
_registrations.Add(
|
||||
new YamlTableRegistration<TKey, TValue>(
|
||||
tableName,
|
||||
relativePath,
|
||||
schemaRelativePath,
|
||||
keySelector,
|
||||
comparer));
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -172,16 +221,19 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
/// </summary>
|
||||
/// <param name="name">配置表名称。</param>
|
||||
/// <param name="relativePath">相对配置根目录的子目录。</param>
|
||||
/// <param name="schemaRelativePath">相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。</param>
|
||||
/// <param name="keySelector">配置项主键提取器。</param>
|
||||
/// <param name="comparer">可选主键比较器。</param>
|
||||
public YamlTableRegistration(
|
||||
string name,
|
||||
string relativePath,
|
||||
string? schemaRelativePath,
|
||||
Func<TValue, TKey> keySelector,
|
||||
IEqualityComparer<TKey>? comparer)
|
||||
{
|
||||
Name = name;
|
||||
RelativePath = relativePath;
|
||||
SchemaRelativePath = schemaRelativePath;
|
||||
_keySelector = keySelector;
|
||||
_comparer = comparer;
|
||||
}
|
||||
@ -196,6 +248,11 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
/// </summary>
|
||||
public string RelativePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时返回空。
|
||||
/// </summary>
|
||||
public string? SchemaRelativePath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string name, IConfigTable table)> LoadAsync(
|
||||
string rootPath,
|
||||
@ -209,6 +266,13 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
$"Config directory '{directoryPath}' was not found for table '{Name}'.");
|
||||
}
|
||||
|
||||
YamlConfigSchema? schema = null;
|
||||
if (!string.IsNullOrEmpty(SchemaRelativePath))
|
||||
{
|
||||
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
|
||||
schema = await YamlConfigSchemaValidator.LoadAsync(schemaPath, cancellationToken);
|
||||
}
|
||||
|
||||
var values = new List<TValue>();
|
||||
var files = Directory
|
||||
.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
|
||||
@ -234,6 +298,12 @@ public sealed class YamlConfigLoader : IConfigLoader
|
||||
exception);
|
||||
}
|
||||
|
||||
if (schema != null)
|
||||
{
|
||||
// 先按 schema 拒绝结构问题,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
|
||||
YamlConfigSchemaValidator.Validate(schema, file, yaml);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var value = deserializer.Deserialize<TValue>(yaml);
|
||||
|
||||
441
GFramework.Game/Config/YamlConfigSchemaValidator.cs
Normal file
441
GFramework.Game/Config/YamlConfigSchemaValidator.cs
Normal file
@ -0,0 +1,441 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。
|
||||
/// 该校验器与当前配置生成器支持的 schema 子集保持一致,
|
||||
/// 以便在配置进入运行时注册表之前就拒绝缺失字段、未知字段和基础类型错误。
|
||||
/// </summary>
|
||||
internal static class YamlConfigSchemaValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 从磁盘加载并解析一个 JSON Schema 文件。
|
||||
/// </summary>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>解析后的 schema 模型。</returns>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="schemaPath" /> 为空时抛出。</exception>
|
||||
/// <exception cref="FileNotFoundException">当 schema 文件不存在时抛出。</exception>
|
||||
/// <exception cref="InvalidOperationException">当 schema 内容不符合当前运行时支持的子集时抛出。</exception>
|
||||
internal static async Task<YamlConfigSchema> LoadAsync(
|
||||
string schemaPath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schemaPath))
|
||||
{
|
||||
throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
|
||||
}
|
||||
|
||||
if (!File.Exists(schemaPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Schema file '{schemaPath}' was not found.", schemaPath);
|
||||
}
|
||||
|
||||
string schemaText;
|
||||
try
|
||||
{
|
||||
schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to read schema file '{schemaPath}'.", exception);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(schemaText);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var typeElement) ||
|
||||
!string.Equals(typeElement.GetString(), "object", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Schema file '{schemaPath}' must declare a root object schema.");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("properties", out var propertiesElement) ||
|
||||
propertiesElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Schema file '{schemaPath}' must declare an object-valued 'properties' section.");
|
||||
}
|
||||
|
||||
var requiredProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (root.TryGetProperty("required", out var requiredElement) &&
|
||||
requiredElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in requiredElement.EnumerateArray())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (item.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyName = item.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(propertyName))
|
||||
{
|
||||
requiredProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var properties = new Dictionary<string, YamlConfigSchemaProperty>(StringComparer.Ordinal);
|
||||
foreach (var property in propertiesElement.EnumerateObject())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
properties.Add(property.Name, ParseProperty(schemaPath, property));
|
||||
}
|
||||
|
||||
return new YamlConfigSchema(schemaPath, properties, requiredProperties);
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Schema file '{schemaPath}' contains invalid JSON.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用已解析的 schema 校验 YAML 文本。
|
||||
/// </summary>
|
||||
/// <param name="schema">已解析的 schema 模型。</param>
|
||||
/// <param name="yamlPath">YAML 文件路径,仅用于诊断信息。</param>
|
||||
/// <param name="yamlText">YAML 文本内容。</param>
|
||||
/// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
|
||||
/// <exception cref="InvalidOperationException">当 YAML 内容与 schema 不匹配时抛出。</exception>
|
||||
internal static void Validate(
|
||||
YamlConfigSchema schema,
|
||||
string yamlPath,
|
||||
string yamlText)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
ArgumentNullException.ThrowIfNull(yamlPath);
|
||||
ArgumentNullException.ThrowIfNull(yamlText);
|
||||
|
||||
YamlStream yamlStream = new();
|
||||
try
|
||||
{
|
||||
using var reader = new StringReader(yamlText);
|
||||
yamlStream.Load(reader);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' could not be parsed as YAML before schema validation.",
|
||||
exception);
|
||||
}
|
||||
|
||||
if (yamlStream.Documents.Count != 1 ||
|
||||
yamlStream.Documents[0].RootNode is not YamlMappingNode rootMapping)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' must contain a single root mapping object.");
|
||||
}
|
||||
|
||||
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in rootMapping.Children)
|
||||
{
|
||||
if (entry.Key is not YamlScalarNode keyNode ||
|
||||
string.IsNullOrWhiteSpace(keyNode.Value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' contains a non-scalar or empty top-level property name.");
|
||||
}
|
||||
|
||||
var propertyName = keyNode.Value;
|
||||
if (!seenProperties.Add(propertyName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' contains duplicate property '{propertyName}'.");
|
||||
}
|
||||
|
||||
if (!schema.Properties.TryGetValue(propertyName, out var property))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' contains unknown property '{propertyName}' that is not declared in schema '{schema.SchemaPath}'.");
|
||||
}
|
||||
|
||||
ValidateNode(yamlPath, propertyName, entry.Value, property);
|
||||
}
|
||||
|
||||
foreach (var requiredProperty in schema.RequiredProperties)
|
||||
{
|
||||
if (!seenProperties.Contains(requiredProperty))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Config file '{yamlPath}' is missing required property '{requiredProperty}' defined by schema '{schema.SchemaPath}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static YamlConfigSchemaProperty ParseProperty(string schemaPath, JsonProperty property)
|
||||
{
|
||||
if (!property.Value.TryGetProperty("type", out var typeElement) ||
|
||||
typeElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property '{property.Name}' in schema file '{schemaPath}' must declare a string 'type'.");
|
||||
}
|
||||
|
||||
var typeName = typeElement.GetString() ?? string.Empty;
|
||||
var propertyType = typeName switch
|
||||
{
|
||||
"integer" => YamlConfigSchemaPropertyType.Integer,
|
||||
"number" => YamlConfigSchemaPropertyType.Number,
|
||||
"boolean" => YamlConfigSchemaPropertyType.Boolean,
|
||||
"string" => YamlConfigSchemaPropertyType.String,
|
||||
"array" => YamlConfigSchemaPropertyType.Array,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Property '{property.Name}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.")
|
||||
};
|
||||
|
||||
if (propertyType != YamlConfigSchemaPropertyType.Array)
|
||||
{
|
||||
return new YamlConfigSchemaProperty(property.Name, propertyType, null);
|
||||
}
|
||||
|
||||
if (!property.Value.TryGetProperty("items", out var itemsElement) ||
|
||||
itemsElement.ValueKind != JsonValueKind.Object ||
|
||||
!itemsElement.TryGetProperty("type", out var itemTypeElement) ||
|
||||
itemTypeElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Array property '{property.Name}' in schema file '{schemaPath}' must declare an item type.");
|
||||
}
|
||||
|
||||
var itemTypeName = itemTypeElement.GetString() ?? string.Empty;
|
||||
var itemType = itemTypeName switch
|
||||
{
|
||||
"integer" => YamlConfigSchemaPropertyType.Integer,
|
||||
"number" => YamlConfigSchemaPropertyType.Number,
|
||||
"boolean" => YamlConfigSchemaPropertyType.Boolean,
|
||||
"string" => YamlConfigSchemaPropertyType.String,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Array property '{property.Name}' in schema file '{schemaPath}' uses unsupported item type '{itemTypeName}'.")
|
||||
};
|
||||
|
||||
return new YamlConfigSchemaProperty(property.Name, propertyType, itemType);
|
||||
}
|
||||
|
||||
private static void ValidateNode(
|
||||
string yamlPath,
|
||||
string propertyName,
|
||||
YamlNode node,
|
||||
YamlConfigSchemaProperty property)
|
||||
{
|
||||
if (property.PropertyType == YamlConfigSchemaPropertyType.Array)
|
||||
{
|
||||
if (node is not YamlSequenceNode sequenceNode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property '{propertyName}' in config file '{yamlPath}' must be an array.");
|
||||
}
|
||||
|
||||
foreach (var item in sequenceNode.Children)
|
||||
{
|
||||
ValidateScalarNode(yamlPath, propertyName, item, property.ItemType!.Value, isArrayItem: true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateScalarNode(yamlPath, propertyName, node, property.PropertyType, isArrayItem: false);
|
||||
}
|
||||
|
||||
private static void ValidateScalarNode(
|
||||
string yamlPath,
|
||||
string propertyName,
|
||||
YamlNode node,
|
||||
YamlConfigSchemaPropertyType expectedType,
|
||||
bool isArrayItem)
|
||||
{
|
||||
if (node is not YamlScalarNode scalarNode)
|
||||
{
|
||||
var subject = isArrayItem
|
||||
? $"Array item in property '{propertyName}'"
|
||||
: $"Property '{propertyName}'";
|
||||
throw new InvalidOperationException(
|
||||
$"{subject} in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(expectedType)}'.");
|
||||
}
|
||||
|
||||
var value = scalarNode.Value;
|
||||
if (value is null)
|
||||
{
|
||||
var subject = isArrayItem
|
||||
? $"Array item in property '{propertyName}'"
|
||||
: $"Property '{propertyName}'";
|
||||
throw new InvalidOperationException(
|
||||
$"{subject} in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(expectedType)}'.");
|
||||
}
|
||||
|
||||
var tag = scalarNode.Tag.ToString();
|
||||
var isValid = expectedType switch
|
||||
{
|
||||
YamlConfigSchemaPropertyType.String => IsStringScalar(tag),
|
||||
YamlConfigSchemaPropertyType.Integer => long.TryParse(
|
||||
value,
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out _),
|
||||
YamlConfigSchemaPropertyType.Number => double.TryParse(
|
||||
value,
|
||||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture,
|
||||
out _),
|
||||
YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (isValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var subjectName = isArrayItem
|
||||
? $"Array item in property '{propertyName}'"
|
||||
: $"Property '{propertyName}'";
|
||||
throw new InvalidOperationException(
|
||||
$"{subjectName} in config file '{yamlPath}' must be of type '{GetTypeName(expectedType)}', but the current YAML scalar value is '{value}'.");
|
||||
}
|
||||
|
||||
private static string GetTypeName(YamlConfigSchemaPropertyType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
YamlConfigSchemaPropertyType.Integer => "integer",
|
||||
YamlConfigSchemaPropertyType.Number => "number",
|
||||
YamlConfigSchemaPropertyType.Boolean => "boolean",
|
||||
YamlConfigSchemaPropertyType.String => "string",
|
||||
YamlConfigSchemaPropertyType.Array => "array",
|
||||
_ => type.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsStringScalar(string tag)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !string.Equals(tag, "tag:yaml.org,2002:int", StringComparison.Ordinal) &&
|
||||
!string.Equals(tag, "tag:yaml.org,2002:float", StringComparison.Ordinal) &&
|
||||
!string.Equals(tag, "tag:yaml.org,2002:bool", StringComparison.Ordinal) &&
|
||||
!string.Equals(tag, "tag:yaml.org,2002:null", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示已解析并可用于运行时校验的 JSON Schema。
|
||||
/// 该模型只保留当前运行时加载器真正需要的最小信息,以避免在游戏运行时引入完整 schema 引擎。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个可用于运行时校验的 schema 模型。
|
||||
/// </summary>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="properties">Schema 属性定义。</param>
|
||||
/// <param name="requiredProperties">必填属性集合。</param>
|
||||
public YamlConfigSchema(
|
||||
string schemaPath,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaProperty> properties,
|
||||
IReadOnlyCollection<string> requiredProperties)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||
ArgumentNullException.ThrowIfNull(properties);
|
||||
ArgumentNullException.ThrowIfNull(requiredProperties);
|
||||
|
||||
SchemaPath = schemaPath;
|
||||
Properties = properties;
|
||||
RequiredProperties = requiredProperties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 文件路径。
|
||||
/// </summary>
|
||||
public string SchemaPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取按属性名索引的 schema 属性定义。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaProperty> Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 声明的必填属性集合。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> RequiredProperties { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示单个 schema 属性的最小运行时描述。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchemaProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个 schema 属性描述。
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称。</param>
|
||||
/// <param name="propertyType">属性类型。</param>
|
||||
/// <param name="itemType">数组元素类型;仅当属性类型为数组时有效。</param>
|
||||
public YamlConfigSchemaProperty(
|
||||
string name,
|
||||
YamlConfigSchemaPropertyType propertyType,
|
||||
YamlConfigSchemaPropertyType? itemType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
|
||||
Name = name;
|
||||
PropertyType = propertyType;
|
||||
ItemType = itemType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性名称。
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取属性类型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素类型;非数组属性时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType? ItemType { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前运行时 schema 校验器支持的属性类型。
|
||||
/// </summary>
|
||||
internal enum YamlConfigSchemaPropertyType
|
||||
{
|
||||
/// <summary>
|
||||
/// 整数类型。
|
||||
/// </summary>
|
||||
Integer,
|
||||
|
||||
/// <summary>
|
||||
/// 数值类型。
|
||||
/// </summary>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 布尔类型。
|
||||
/// </summary>
|
||||
Boolean,
|
||||
|
||||
/// <summary>
|
||||
/// 字符串类型。
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// 数组类型。
|
||||
/// </summary>
|
||||
Array
|
||||
}
|
||||
@ -3,14 +3,30 @@
|
||||
<!-- This file is automatically generated by the NuGet package -->
|
||||
<!-- It ensures that the source generators are properly registered during build -->
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
消费项目默认从 schemas/ 目录收集配置 schema。
|
||||
这样可以让配置生成器开箱即用,同时保留通过属性重定向目录的余地。
|
||||
-->
|
||||
<GFrameworkConfigSchemaDirectory Condition="'$(GFrameworkConfigSchemaDirectory)' == ''">schemas</GFrameworkConfigSchemaDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Analyzer Include="$(MSBuildThisFileDirectory)../analyzers/dotnet/cs/GFramework.SourceGenerators.dll"/>
|
||||
<Analyzer Include="$(MSBuildThisFileDirectory)../analyzers/dotnet/cs/GFramework.SourceGenerators.Abstractions.dll"/>
|
||||
<Analyzer Include="$(MSBuildThisFileDirectory)../analyzers/dotnet/cs/GFramework.SourceGenerators.Common.dll"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists('$(MSBuildProjectDirectory)/$(GFrameworkConfigSchemaDirectory)')">
|
||||
<!--
|
||||
将消费者项目中的 schema 文件自动暴露为 AdditionalFiles,
|
||||
避免每个游戏项目都手工维护同样的 MSBuild 配置。
|
||||
-->
|
||||
<AdditionalFiles Include="$(MSBuildProjectDirectory)/$(GFrameworkConfigSchemaDirectory)/**/*.schema.json"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Ensure the analyzers are loaded -->
|
||||
<Target Name="EnsureGFrameworkAnalyzers" BeforeTargets="CoreCompile">
|
||||
<Message Text="Loading GFramework source generators" Importance="high"/>
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@ -174,6 +174,7 @@ export default defineConfig({
|
||||
text: 'Game 游戏模块',
|
||||
items: [
|
||||
{ text: '概览', link: '/zh-CN/game/' },
|
||||
{ text: '内容配置系统', link: '/zh-CN/game/config-system' },
|
||||
{ text: '数据管理', link: '/zh-CN/game/data' },
|
||||
{ text: '场景系统', link: '/zh-CN/game/scene' },
|
||||
{ text: 'UI 系统', link: '/zh-CN/game/ui' },
|
||||
|
||||
126
docs/zh-CN/game/config-system.md
Normal file
126
docs/zh-CN/game/config-system.md
Normal file
@ -0,0 +1,126 @@
|
||||
# 游戏内容配置系统
|
||||
|
||||
> 面向静态游戏内容的 AI-First 配表方案
|
||||
|
||||
该配置系统用于管理怪物、物品、技能、任务等静态内容数据。
|
||||
|
||||
它与 `GFramework.Core.Configuration` 不同,后者面向运行时键值配置;它也不同于 `GFramework.Game.Setting`,后者面向玩家设置和持久化。
|
||||
|
||||
## 当前能力
|
||||
|
||||
- YAML 作为配置源文件
|
||||
- JSON Schema 作为结构描述
|
||||
- 一对象一文件的目录组织
|
||||
- 运行时只读查询
|
||||
- Source Generator 生成配置类型和表包装
|
||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开和轻量校验入口
|
||||
|
||||
## 推荐目录结构
|
||||
|
||||
```text
|
||||
GameProject/
|
||||
├─ config/
|
||||
│ ├─ monster/
|
||||
│ │ ├─ slime.yaml
|
||||
│ │ └─ goblin.yaml
|
||||
│ └─ item/
|
||||
│ └─ potion.yaml
|
||||
├─ schemas/
|
||||
│ ├─ monster.schema.json
|
||||
│ └─ item.schema.json
|
||||
```
|
||||
|
||||
## Schema 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"hp": { "type": "integer" },
|
||||
"dropItems": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## YAML 示例
|
||||
|
||||
```yaml
|
||||
id: 1
|
||||
name: Slime
|
||||
hp: 10
|
||||
dropItems:
|
||||
- potion
|
||||
- slime_gel
|
||||
```
|
||||
|
||||
## 运行时接入
|
||||
|
||||
当你希望加载后的配置在运行时以只读表形式暴露时,可以使用 `YamlConfigLoader` 和 `ConfigRegistry`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Config;
|
||||
|
||||
var registry = new ConfigRegistry();
|
||||
|
||||
var loader = new YamlConfigLoader("config-root")
|
||||
.RegisterTable<int, MonsterConfig>(
|
||||
"monster",
|
||||
"monster",
|
||||
"schemas/monster.schema.json",
|
||||
static config => config.Id);
|
||||
|
||||
await loader.LoadAsync(registry);
|
||||
|
||||
var monsterTable = registry.GetTable<int, MonsterConfig>("monster");
|
||||
var slime = monsterTable.Get(1);
|
||||
```
|
||||
|
||||
这个重载会先按 schema 校验,再进行反序列化和注册。
|
||||
|
||||
## 运行时校验行为
|
||||
|
||||
绑定 schema 的表在加载时会拒绝以下问题:
|
||||
|
||||
- 缺失必填字段
|
||||
- 未在 schema 中声明的未知字段
|
||||
- 标量类型不匹配
|
||||
- 数组元素类型不匹配
|
||||
|
||||
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
||||
|
||||
## 生成器接入约定
|
||||
|
||||
配置生成器会从 `*.schema.json` 生成配置类型和表包装类。
|
||||
|
||||
通过已打包的 Source Generator 使用时,默认会自动收集 `schemas/**/*.schema.json` 作为 `AdditionalFiles`。
|
||||
|
||||
如果你在仓库内直接使用项目引用而不是打包后的 NuGet,请确认 schema 文件同样被加入 `AdditionalFiles`。
|
||||
|
||||
## VS Code 工具
|
||||
|
||||
仓库中的 `tools/vscode-config-extension` 当前提供以下能力:
|
||||
|
||||
- 浏览 `config/` 目录
|
||||
- 打开 raw YAML 文件
|
||||
- 打开匹配的 schema 文件
|
||||
- 对必填字段和基础标量类型做轻量校验
|
||||
- 对顶层标量字段提供轻量表单入口
|
||||
|
||||
当前仍建议把复杂数组、嵌套对象和批量修改放在 raw YAML 中完成。
|
||||
|
||||
## 当前限制
|
||||
|
||||
以下能力尚未完全完成:
|
||||
|
||||
- 运行时热重载
|
||||
- 跨表引用校验
|
||||
- 更完整的 JSON Schema 支持
|
||||
- 更强的 VS Code 表单编辑器
|
||||
|
||||
因此,现阶段更适合作为你游戏项目的“受控试点配表系统”,而不是完全无约束的大规模内容生产平台。
|
||||
@ -8,6 +8,7 @@ GFramework.Game 是 GFramework 框架的游戏特定功能模块,提供了游
|
||||
|
||||
- [概述](#概述)
|
||||
- [核心特性](#核心特性)
|
||||
- [内容配置系统](#内容配置系统)
|
||||
- [架构模块系统](#架构模块系统)
|
||||
- [资产管理](#资产管理)
|
||||
- [存储系统](#存储系统)
|
||||
@ -57,6 +58,24 @@ GFramework.Game 为游戏开发提供了专门的功能模块,与 GFramework.C
|
||||
- **性能优化**:序列化缓存和优化策略
|
||||
- **类型安全**:强类型的序列化和反序列化
|
||||
|
||||
## 内容配置系统
|
||||
|
||||
`GFramework.Game` 当前包含面向静态游戏内容的 AI-First 配表能力,用于怪物、物品、技能、任务等只读内容数据。
|
||||
|
||||
这一能力的核心定位是:
|
||||
|
||||
- 使用 `YAML` 作为配置源文件
|
||||
- 使用 `JSON Schema` 描述结构和约束
|
||||
- 在运行时以只读配置表形式暴露
|
||||
- 通过 Source Generator 生成配置类型和表包装
|
||||
- 配套 VS Code 工具提供浏览、校验和轻量编辑入口
|
||||
|
||||
如果你准备在游戏项目中接入这套系统,请先阅读:
|
||||
|
||||
- [游戏内容配置系统](/zh-CN/game/config-system)
|
||||
|
||||
该页面包含目录约定、运行时注册方式、schema 绑定方式、生成器接入约定和当前限制说明。
|
||||
|
||||
## 架构模块系统
|
||||
|
||||
### AbstractModule 基础使用
|
||||
@ -1395,4 +1414,4 @@ graph TD
|
||||
- **.NET**: 6.0+
|
||||
- **Newtonsoft.Json**: 13.0.3+
|
||||
- **GFramework.Core**: 与 Core 模块版本保持同步
|
||||
---
|
||||
---
|
||||
|
||||
@ -10,6 +10,7 @@ GFramework.SourceGenerators 是 GFramework 框架的源代码生成器包,通
|
||||
- [核心特性](#核心特性)
|
||||
- [安装配置](#安装配置)
|
||||
- [Log 属性生成器](#log-属性生成器)
|
||||
- [Config Schema 生成器](#config-schema-生成器)
|
||||
- [ContextAware 属性生成器](#contextaware-属性生成器)
|
||||
- [GenerateEnumExtensions 属性生成器](#generateenumextensions-属性生成器)
|
||||
- [Priority 属性生成器](#priority-属性生成器)
|
||||
@ -36,6 +37,7 @@ GFramework.SourceGenerators 利用 Roslyn 源代码生成器技术,在编译
|
||||
### 🎯 主要生成器
|
||||
|
||||
- **[Log] 属性**:自动生成 ILogger 字段和日志方法
|
||||
- **Config Schema 生成器**:根据 `*.schema.json` 生成配置类型和表包装
|
||||
- **[ContextAware] 属性**:自动实现 IContextAware 接口
|
||||
- **[GenerateEnumExtensions] 属性**:自动生成枚举扩展方法
|
||||
- **[Priority] 属性**:自动实现 IPrioritized 接口,为类添加优先级标记
|
||||
@ -68,6 +70,45 @@ GFramework.SourceGenerators 利用 Roslyn 源代码生成器技术,在编译
|
||||
</Project>
|
||||
```
|
||||
|
||||
### Config Schema 文件约定
|
||||
|
||||
当项目引用 `GeWuYou.GFramework.SourceGenerators` 的打包产物时,生成器会默认从 `schemas/**/*.schema.json` 收集配置 schema
|
||||
文件并作为 `AdditionalFiles` 输入。
|
||||
|
||||
这意味着消费者项目通常只需要维护如下结构:
|
||||
|
||||
```text
|
||||
GameProject/
|
||||
├─ config/
|
||||
│ └─ monster/
|
||||
│ └─ slime.yaml
|
||||
└─ schemas/
|
||||
└─ monster.schema.json
|
||||
```
|
||||
|
||||
如果你需要完整接入运行时加载、schema 校验和 VS Code 工具链,请继续阅读:
|
||||
|
||||
- [游戏内容配置系统](/zh-CN/game/config-system)
|
||||
|
||||
## Config Schema 生成器
|
||||
|
||||
Config Schema 生成器会扫描 `*.schema.json` 文件,并生成:
|
||||
|
||||
- 配置数据类型
|
||||
- 与 `IConfigTable<TKey, TValue>` 对齐的表包装类型
|
||||
|
||||
这一生成器适合与 `GFramework.Game.Config.YamlConfigLoader` 配合使用,让 schema、运行时和工具链共享同一份结构约定。
|
||||
|
||||
当前支持的 schema 子集以内容配置系统文档中的说明为准,重点覆盖:
|
||||
|
||||
- `object` 根节点
|
||||
- `required`
|
||||
- `integer`
|
||||
- `number`
|
||||
- `boolean`
|
||||
- `string`
|
||||
- `array`
|
||||
|
||||
### 项目文件配置
|
||||
|
||||
```xml
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user