在大型游戏服务器中,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 版本控制 |
关键设计
- XML 定义 API - 参数、返回值、注释
- Excel 定义配置 - 非技术人员可编辑
- 三层结构(表、Bean、枚举)- 灵活组合复杂数据
- Scriban 模板 - 可定制的代码生成规则
- 多目标并行生成 - 服务器、Unity、Web 一次生成
- 编译验证 - 生成后立即验证,防止问题
- 原子替换 - 保证一致性,避免部分更新
- 版本控制 - 追踪配置变化
这个模式在很多大型游戏和企业系统中都能看到,是从数据定义到代码的自动化的典范。
对开发的启发
- 减少重复: 一处定义,多处使用
- 提高一致性: 自动同步,不易出错
- 降低维护成本: 修改一个地方即可
- 提升开发效率: 自动生成 1000+ 行代码,节省时间
- 类型安全: 编译时检查,比运行时发现更好
这不仅仅是一个工具,更是一种开发哲学:用最少的手工工作,产生最多的类型安全代码。