← 返回首页

ARTICLE

从数据定义到类型安全代码:自动化生成系统设计

在大型游戏服务器中,API 定义、配置管理、协议文档往往需要在服务器、客户端、文档之间保持一致。手工维护这些内容容易出错且工作量巨大。本文介绍一个优雅的解决方案:通过 XML 和 Excel 定义数据,自动生成服务器、客户端和文档的完整代码。 问题:手工维护的痛点 API 定义的重复工作 传统开发流

在大型游戏服务器中,API 定义、配置管理、协议文档往往需要在服务器、客户端、文档之间保持一致。手工维护这些内容容易出错且工作量巨大。本文介绍一个优雅的解决方案:通过 XML 和 Excel 定义数据,自动生成服务器、客户端和文档的完整代码。

问题:手工维护的痛点

API 定义的重复工作

传统开发流程:
1. 服务器:写 API 端点 + 参数定义 + 返回值
2. 客户端:写 API 调用 + 请求类 + 响应类
3. 文档:手写 API 文档 + 参数说明 + 返回值说明

问题:
❌ 定义重复(服务器和客户端都要写)
❌ 容易不一致(改一个地方忘改另一个)
❌ 手工代码多(容易拼写错误)
❌ 维护困难(改一次要改三个地方)

配置管理的复杂性

游戏需要管理的配置:
├─ 资源配置(多种资源类型、上限值)
├─ 功能配置(功能ID、奖励、条件)
├─ 道具配置(道具ID、属性、价格)
├─ 等级配置(等级经验、等级奖励)
├─ 活动配置(活动时间、活动奖励)
└─ ... 更多配置

传统问题:
❌ 配置分散(Excel、JSON、XML、代码混杂)
❌ 服务器客户端配置不一致
❌ 修改配置需要重新编译
❌ 版本管理困难
❌ 难以支持多个版本共存

解决方案:自动化代码生成系统

整体工作流

数据定义(Excel + XML)
    ↓
配置文件(luban.conf)
    ↓
编排脚本(gen.sh)
    ↓
Luban 代码生成器
    ↓
Scriban 模板(生成规则)
    ↓
┌─────────────────────────────┐
│  三个并行生成目标           │
├─────────────────────────────┤
│ • 服务器(C# + .NET JSON)  │
│ • Unity 客户端(C# + JSON)  │
│ • Web 客户端(TypeScript)   │
└─────────────────────────────┘
    ↓
生成的代码 + 数据文件
    ↓
版本控制(.gen/ 目录)
    ↓
项目编译集成

第一部分:API 端点的自动化生成

XML 定义 API

API 不再需要手工编写,只需用 XML 定义:

<!-- Protocol/Source/proc.xml -->
<controller name="User" token="true">
    <url id="10300"
         name="Login"
         type="Post"
         return="LoginResult"
         comment="用户登录">
        <arg type="int"
             name="timestamp"
             comment="时间戳"/>
        <arg type="string"
             name="deviceId"
             comment="设备ID"
             default='""'/>
    </url>

    <url id="10301"
         name="GetInfo"
         type="Post"
         return="UserInfo"
         comment="获取用户信息">
        <arg type="string"
             name="userId"
             comment="用户ID"/>
    </url>
</controller>

<controller name="Item" token="true">
    <url id="11001"
         name="PurchaseItem"
         type="Post"
         return="PurchaseResult"
         comment="购买物品">
        <arg type="int"
             name="itemId"
             comment="物品ID"/>
        <arg type="int"
             name="quantity"
             comment="购买数量"/>
    </url>
</controller>

自动生成的服务器代码

代码生成工具读取 XML,自动生成 Controller:

// 自动生成:GateServer/Controller/Gen/UserController.cs
[ApiController]
[Route("api")]
public partial class UserController : ControllerBase
{
    [EndpointSummary("Login")]
    [HttpPost("10300")]
    #if DEBUG
    [HttpPost("{version}/10300")]
    #endif
    public async Task<DataPlus> Login(
        [FromServices] IClusterClient cluster,
        [FromServices] IHmacJsonWebTokenReader jwtReader,
        [IgnoreCheckNull] string token,
        [Description("时间戳")] int timestamp,
        [Description("设备ID")][IgnoreCheckNull] string deviceId = "")
    {
        // 参数验证由过滤器处理(自动)

        // 调用业务逻辑(开发者编写)
        var result = await _Login_Implement(
            cluster, jwtReader, token, timestamp, deviceId);

        return new DataPlus(result);
    }

    // 开发者在 Custom 文件中实现这个方法
    private async Task<LoginResult> _Login_Implement(
        IClusterClient cluster,
        IHmacJsonWebTokenReader jwtReader,
        string token,
        int timestamp,
        string deviceId)
    {
        // 业务逻辑代码 ...
    }
}

关键特性:

  • ✅ 参数自动映射(无需手工写 FromBody、FromQuery)
  • ✅ 参数验证代码自动生成(由过滤器处理)
  • ✅ 返回值自动序列化
  • ✅ 方法分离(自动 + 业务逻辑)

第二部分:配置表的自动化生成

数据定义的三层结构

第 1 层:表定义(Table.xml)

定义配置表的整体结构:

<table name="ResourceConfigManager"
       value="ResourceConfig"
       input="Config/Resource.xlsx"
       mode="map"
       index="type"
       group="server,client">
</table>

<table name="SkillConfigManager"
       value="SkillConfig"
       input="Config/Skill.xlsx"
       mode="list"
       group="server">
</table>

属性说明:

  • name - 生成的 Manager 类名
  • value - 生成的数据项类名
  • input - Excel 文件路径
  • mode - 存储模式(map=字典、list=列表、singleton=单例)
  • index - 索引字段(支持复合:type+level)
  • group - 目标分组(server=服务器、client=客户端、all=两者)

第 2 层:Bean 定义(Bean.xml)

定义复杂的数据结构:

<!-- 基础 Bean -->
<bean name="ResourceCost">
    <var name="type" type="int"/>
    <var name="amount" type="long"/>
</bean>

<!-- 继承的 Bean -->
<bean name="UpgradeCost" parent="ResourceCost">
    <var name="level" type="int"/>
</bean>

<!-- 包含列表 -->
<bean name="RewardGroup">
    <var name="id" type="int"/>
    <var name="rewards" type="list" valueType="Reward"/>
</bean>

支持:

  • ✅ 继承(parent 属性)
  • ✅ 多态(子 Bean)
  • ✅ 复合类型(列表、字典、Bean 嵌套)
  • ✅ 枚举字段

第 3 层:枚举定义(Enum.xml)

定义常量枚举:

<enum name="ResultCode" group="all">
    <var name="Success" value="0"/>
    <var name="InvalidParam" value="40001"/>
    <var name="Unauthorized" value="40003"/>
    <var name="ServerError" value="60001"/>
</enum>

<enum name="ResourceType" flags="true">
    <var name="Gold" value="1"/>
    <var name="Gem" value="2"/>
    <var name="Token" value="4"/>
</enum>

自动生成的配置代码

生成的 Manager 类

负责配置的加载和查询:

// 接口(自动生成)
public interface IResourceConfigManager
{
    string Guid { get; }
    IReadOnlyList<IResourceConfig> Data { get; }
    IResourceConfig Get(int resourceType);
}

// 实现(自动生成)
public class ResourceConfigManager : ConfigManagerBase
{
    private readonly Dictionary<int, ResourceConfig> _dataMap = new();
    private readonly List<ResourceConfig> _dataList = new();

    public ResourceConfigManager(JsonElement buf, string guid, string json)
        : base(guid, json)
    {
        // 从 JSON 反序列化
        var root = JsonDocument.Parse(json).RootElement;

        foreach (var item in root.EnumerateArray())
        {
            var config = new ResourceConfig(item);
            _dataList.Add(config);
            _dataMap[config.Type] = config;
        }
    }

    public IReadOnlyList<IResourceConfig> Data => _dataList;

    public IResourceConfig Get(int resourceType)
    {
        return _dataMap.TryGetValue(resourceType, out var v) ? v : null;
    }
}

关键特性:

  • ✅ 自动索引构建(O(1) 查询)
  • ✅ 支持多种查询方式(按 ID、遍历列表)
  • ✅ 内存友好(预构建索引)

生成的数据类

// 接口(自动生成)
public interface IResourceConfig
{
    int Type { get; }
    string Name { get; }
    long MaxValue { get; }
}

// 实现(自动生成)
public class ResourceConfig : ConfigBase, IResourceConfig
{
    public int Type { get; private set; }
    public string Name { get; private set; }
    public long MaxValue { get; private set; }

    public ResourceConfig(JsonElement buf)
    {
        Type = buf.GetProperty("type").GetInt32();
        Name = buf.GetProperty("name").GetString();
        MaxValue = buf.GetProperty("maxValue").GetInt64();
    }

    // 序列化支持
    public string Serialize()
    {
        return JsonSerializer.Serialize(this);
    }
}

属性:

  • ✅ 只读属性(保证不可变)
  • ✅ 多序列化格式支持(JSON、ProtoBuf、Orleans)
  • ✅ 自动生成序列化代码

Excel 数据文件

配置数据在 Excel 中编辑:

Resource.xlsx:
┌────┬─────────┬─────────┐
│ id │  name   │ maxVal  │
├────┼─────────┼─────────┤
│ 1  │ Gold    │ 999999  │
│ 2  │ Gem     │ 99999   │
│ 3  │ Token   │ 100000  │
└────┴─────────┴─────────┘

Skill.xlsx:
┌────┬──────────┬──────┬────────┐
│ id │   name   │ type │ damage │
├────┼──────────┼──────┼────────┤
│ 1  │ FireBolt │ Atk  │  100   │
│ 2  │ Heal     │ Sup  │  50    │
└────┴──────────┴──────┴────────┘

优势:

  • ✅ 非技术人员可以编辑
  • ✅ 数据一目了然
  • ✅ 易于版本管理(Excel 文件)

第三部分:多目标并行生成

同一份定义,自动为多个平台生成代码:

三个并行目标

目标 语言 序列化 用途
Server C# .NET JSON 服务器配置
Unity C# NewtonsoftJson Unity 客户端
Web TypeScript JSON Web 客户端

分组过滤

在 XML 中标记配置属于哪个目标:

<!-- 所有目标都包含 -->
<table group="all">...</table>

<!-- 仅服务器 -->
<table group="server">...</table>

<!-- 仅客户端(Unity + Web) -->
<table group="client">...</table>

<!-- 仅 Unity -->
<table group="unity">...</table>

生成流程

#!/bin/bash

# 为三个目标分别生成
for TARGET in server unity web; do
    echo "Generating for $TARGET..."

    dotnet Luban.Runner.dll \
        -t $TARGET \
        --conf luban.conf \
        -x outputCodeDir=.tmp/Code/$TARGET \
        -x outputDataDir=.tmp/Data/$TARGET
done

# 验证编译
dotnet build Game.Config.csproj

# 原子替换(保证一致性)
rm -rf .gen
mv .tmp .gen

# 版本控制
git add .gen/
git commit -m "Update generated config"

关键点:

  • ✅ 为每个目标生成专用的代码和数据
  • ✅ 编译验证(防止生成错误)
  • ✅ 原子替换(保证一致性)
  • ✅ 版本控制(追踪变化)

Scriban 模板系统

代码生成的规则由 Scriban 模板定义,支持复杂的代码生成逻辑:

{{- # 生成 Manager 接口和实现 -}}
namespace {{ __namespace }}
{
    public partial interface I{{ __name }}
    {
        string Guid { get; }
        IReadOnlyList<I{{ __value_type }}> Data { get; }
        I{{ __value_type }} Get({{ __key_type }} key);
    }

    public sealed partial class {{ __name }} : ConfigManagerBase
    {
        {{- # 根据模式生成存储结构 -}}
        {{- if __mode == 'map' }}
        private readonly Dictionary<{{ __key_type }}, {{ __value_type }}> _dataMap;
        {{- else if __mode == 'list' }}
        private readonly List<{{ __value_type }}> _dataList;
        {{- end }}

        {{- # 根据是否有个性化字段生成相关方法 -}}
        {{- if has_personalized_fields(__table) }}
        public PersonalizedResult GetPersonalized(int userId)
        {
            // 个性化逻辑
        }
        {{- end }}
    }
}

模板能做的:

  • ✅ 条件生成(if、else)
  • ✅ 循环生成(for、foreach)
  • ✅ 函数调用(has_index()、get_field_names())
  • ✅ 字符串操作(snake_case、camelCase 转换)

完整的工作流程

1. 修改数据

编辑 Excel 文件,比如修改 Resource.xlsx:

Resource.xlsx:
┌────┬─────────┬──────────┐
│ id │  name   │ maxValue │
├────┼─────────┼──────────┤
│ 1  │ Gold    │ 1000000  │ ← 从 999999 改成 1000000
└────┴─────────┴──────────┘

2. 生成代码

运行生成脚本:

./gen.sh

3. 自动进行的步骤

  • ✅ Luban 检测到 Resource.xlsx 变化
  • ✅ 仅重新生成 ResourceConfigManager 和 ResourceConfig
  • ✅ 其他配置不变(增量更新)
  • ✅ 生成 C#、TypeScript、JSON 数据
  • ✅ 编译验证(无编译错误)
  • ✅ 原子替换 .gen 目录

4. 代码集成

生成的代码自动被项目引用:

<!-- Game.Config.csproj -->
<ItemGroup>
    <Compile Include="..\..\config\.gen\Code\Server\*.cs">
        <Link>Generate\%(RecursiveDir)%(Filename)%(Extension)</Link>
    </Compile>

    <None Include="..\..\config\.gen\Data\Server\*">
        <Link>ConfigData\%(RecursiveDir)%(Filename)%(Extension)</Link>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
</ItemGroup>

5. 版本控制

生成的代码提交到 Git,追踪配置变化:

.gen/
├── Code/
│   ├── Server/
│   │   ├── ResourceConfig.cs
│   │   ├── SkillConfig.cs
│   │   └── ...
│   ├── Unity/
│   └── Web/
└── Data/
    ├── Server/
    │   ├── resource.json
    │   ├── skill.json
    │   └── ...
    ├── Unity/
    └── Web/

设计的优雅之处

1. 单一数据源原则

Excel + XML → 唯一的真实来源
           ↓
        一次生成
           ↓
    ┌──────┴──────┬──────────┐
    ↓             ↓          ↓
  Server       Unity       Web

不再需要在三个地方维护同样的配置。

2. 类型安全

手工编写配置代码时,容易出现拼写错误:

// ❌ 容易出错
var gold = config["gold"];           // 字符串键,容易拼错
var maxValue = (long)config[0];      // 位置索引,容易搞反

// ✅ 自动生成的类型安全
var gold = resourceConfig.Get(ResourceType.Gold);  // 编译时检查
var maxValue = gold.MaxValue;                      // 类型检查

3. 自动化索引

手工写配置查询时:

// ❌ 需要手工构建索引
private Dictionary<int, Resource> _index = new();

public void Load(List<Resource> items)
{
    foreach (var item in items)
    {
        _index[item.Id] = item;  // 手工索引
    }
}

// ✅ 自动生成,O(1) 查询
public Resource Get(int id)
{
    return _dataMap.TryGetValue(id, out var v) ? v : null;
}

4. 多平台支持

一份配置,自动适配多个平台的需求。

5. 增量更新

修改一个配置表,只需重新生成那个表的代码,其他表不变。

总结

这个自动化代码生成系统解决的核心问题:

问题 解决方案
API 定义重复 XML 单一定义,自动生成服务器和客户端
配置分散 Excel 单一数据源,自动生成配置代码
类型不安全 自动生成强类型代码,编译时检查
维护困难 修改数据自动重新生成,保持一致
多平台同步 一次生成,多个平台同时支持
版本管理 生成的代码纳入 Git 版本控制

关键设计

  1. XML 定义 API - 参数、返回值、注释
  2. Excel 定义配置 - 非技术人员可编辑
  3. 三层结构(表、Bean、枚举)- 灵活组合复杂数据
  4. Scriban 模板 - 可定制的代码生成规则
  5. 多目标并行生成 - 服务器、Unity、Web 一次生成
  6. 编译验证 - 生成后立即验证,防止问题
  7. 原子替换 - 保证一致性,避免部分更新
  8. 版本控制 - 追踪配置变化

这个模式在很多大型游戏和企业系统中都能看到,是从数据定义到代码的自动化的典范。

对开发的启发

  • 减少重复: 一处定义,多处使用
  • 提高一致性: 自动同步,不易出错
  • 降低维护成本: 修改一个地方即可
  • 提升开发效率: 自动生成 1000+ 行代码,节省时间
  • 类型安全: 编译时检查,比运行时发现更好

这不仅仅是一个工具,更是一种开发哲学:用最少的手工工作,产生最多的类型安全代码。