游戏Excel配置自动化导出二进制工具链并生成对应配置类详解

一、需求背景

在游戏开发中,策划同学经常使用 Excel 来维护游戏的各种配置数据,例如装备表、关卡表、道具表等。这些表格通常包含大量的字段和行数。作为程序,我们需要将这些配置高效地加载到游戏中,并提供类型安全、易于使用的访问接口。

手动处理 Excel 转成游戏可读的格式(如 JSON、XML)不仅繁琐,而且容易出错。(++在最近两家公司中,使用的配置要么过大,要么转换繁琐,上家公司是将所有的配置都统一转化JSON到一个压缩包中,导致首次加载配置压缩包就要加载很长时间,现在这家公司则是直接将对应的表在线导出成txt文件格式放到项目中,每次新表都要重复以上操作,不仅繁琐txt内存还大++ )

因此,一套自动化的配置导出工具链显得尤为重要。

本文介绍的这套工具链,可以将 Excel 文件经过一系列转换和压缩,最终生成**二进制 .bin**文件,并在游戏内通过代码 ConfigManager 解析成游戏可读取的内容。同时,工具还会自动为每个配置表生成两个 TypeScript 类:表名+ConfMgr(配置管理器)和 表名+Conf(配置项),极大地提升了开发效率和代码的可维护性。

使用这套工具的好处

这套工具链实现了从 Excel 到游戏内高效配置的完整自动化流程,具备以下优点:

该工具适用所有excel文件,如果需要生成其他引擎的脚本提示 只需要修改生成代码的逻辑即可

  • 自动化:一键生成所有配置,减少人工操作错误。

  • 类型安全:生成的 TypeScript 代码提供智能提示,避免手写字符串键。

  • 高性能:二进制加载 + 内存数组存储,运行时查询速度快。

  • 灵活性:支持主键和数组索引,满足各种查询需求;支持独立输出,方便分包管理。

对比同一张表转出的 二进制文件、json文件、txt文件的大小

二、整体流程概览

工具链的核心是一个批处理文件 build.bat,它串联了四个 Python 脚本,每一步完成特定的功能:

bash 复制代码
build.bat
├─ 1. ExcelToCsv.py      # 将 Excel 文件转为 CSV 格式
├─ 2. dealConfig.py       # 解析 CSV,生成 JS 配置文件和 TypeScript 定义
├─ 3. uglify_all_js.py    # 压缩 JS 文件(去空格、混淆),生成 .min.js
└─ 4. zlibConfig.py       # 使用 zlib 进一步压缩 .min.js 为二进制 .bin 文件

此外,还有一个 conf.json 配置文件,用来控制整个流程的行为(如输入输出目录、哪些表需要独立输出等)。

下面我们将逐一详解每个环节。

三、配置文件 conf.json 详解

conf.json 是整个工具链的"大脑",我们先理解它的结构:

TypeScript 复制代码
{
    "author": "Danny",
    "inputDir": "csv/",
    "outputJs": "outputJs",
    "compressConf": "outputJs/config.min.js",
    "outputCompressedJs": "../../ConfigTest/assets/Configs/config/config.bin",
    "outputTsDir": "../../ConfigTest/assets/Script/configData/",
    "otherOutputDir": [
        { "name": "GradeDefine", "outputDir": "../../ConfigTest/assets/GameBundle/config/" },
        { "name": "CardDefine", "outputDir": "../../ConfigTest/assets/HomeBundle/config/" }
    ],
    "files": [
        { "name":"GradeDefine.csv", "keys":[["grade"]], "keys2Array":[["quality"]], "desc":"装备表" },
        { "name":"CardDefine.csv", "keys":[["cardTotal"],["card","cards"]], "keys2Array":[["card"]], "desc":"关卡表" },
        { "name":"TeamLogoDefine.csv", "keys":[["logoId"]], "keys2Array":[["logoType"]], "desc":"球队表" }
    ]
}
字段 含义
inputDir 存放 CSV 文件的目录(由第一步生成)
outputJs 生成的 JS 文件输出目录
outputTsDir 生成的 TypeScript 文件输出目录
otherOutputDir 指定某些表独立输出到指定目录,用于分包或模块化
files 需要处理的文件列表,每个对象包含: - name:CSV 文件名(必须与 Excel sheet 名一致) - keys:主键列表(支持复合主键),用于生成单条查询方法 - keys2Array:数组键列表,用于生成返回多条记录的方法 - desc:配置描述,会写入生成的 TS 注释中

特别注意:Excel 文件中的 sheet 名称会作为配置表的名字,且 CSV 文件前三行有特殊要求:

  • 第1行:字段中文名(仅用于生成注释)

  • 第2行:字段类型(如 int、float、string 等)

  • 第3行:字段英文名(程序内使用的变量名)

四、Excel → CSV(ExcelToCsv.py

这一步将策划维护的 Excel 文件(.xlsx.xls)转换为工具链更容易解析的 CSV 格式。

核心逻辑

  • 遍历指定目录下的所有 Excel 文件。

  • 对于每个 Excel 文件,读取所有 sheet(忽略名称包含"sheet"的 sheet)。

  • 将每个 sheet 转换为同名的 CSV 文件,保存到 csv/ 目录。

  • 转换时保留前三行(中文名、类型、字段名),数据行从第四行开始。

  • 支持按第二行的类型将数字列转为 intfloat

为什么需要这一步?

Excel 文件是二进制格式,Python 通过 openpyxl 库读取较为方便,但后续处理使用 CSV 更轻量,也便于调试查看。

五、CSV → JS + TS

这是最核心的一步,它完成以下工作:

  1. 读取 conf.json 中的配置。

  2. 遍历 files 列表,对每个 CSV 文件进行解析。

  3. 根据 CSV 的三行元数据,构建内存中的配置数据结构(confData)。

  4. 根据 keyskeys2Array 建立字典索引,用于快速查找。

  5. 根据表名是否在 otherOutputDir 中,决定是独立输出为一个 JS 文件,还是合并到全局 config.js 中。

  6. 调用 tsHelper.py 为每个表生成对应的 TypeScript 类(xxxConfxxxConfMgr)。

5.1 数据结构设计

生成的 JS 文件内容格式如下:

javascript 复制代码
window.gameConfData = {
    "GradeDefine": {
        "data": { 0: [1001, "青铜", ...], 1: [1002, "白银", ...] },
        "dic_grade": { "1001": 0, "1002": 1 },
        "dic_qualityArr": { "1": [0,1] }   // 数组字典
    },
    ...
}
  • data 是一个数组(实际用对象模拟),每个元素是一条配置记录的数组表示(按字段顺序存储),这样可以节省内存,并且通过索引访问更快。

  • 字典 dic_xxx 用于快速根据某个键找到记录的索引,例如 dic_grade 的 key 是 grade 字段的值,value 是 data 中的索引。

  • 数组字典 dic_xxxArr 用于根据某个键获取一组记录的索引列表,例如根据 quality 字段获取所有该品质的装备索引。

5.2 TypeScript 代码生成(tsHelper.py

工具会为每个表生成两个类,放在 outputTsDir 中,例如 GradeDefine.ts

TypeScript 复制代码
/** 装备表配置数据管理 */
export class GradeDefineConfMgr {
    private static _ins: GradeDefineConfMgr;
    public static get ins(): GradeDefineConfMgr{
        GradeDefineConfMgr._ins = new GradeDefineConfMgr();
        return GradeDefineConfMgr._ins;
    }
    
    private _confDict:{[key: number]: GradeDefineConf} ;
    public data:any ;
    public confList:Array<GradeDefineConf>;
    private _dic_grade:any ;
    private _dic_qualityArr:any ;

    constructor(){ 
        this._confDict = {};
        var conf:any = (<any>window).gameConfData.GradeDefine;
        this.data = conf.data ;
        this._dic_grade = conf.dic_grade;
        this._dic_qualityArr = conf.dic_qualityArr;
    }

    public getConfByGrade(grade:number): GradeDefineConf { 
        return this.getConf(<number>this._dic_grade[grade]);
    }

    public getConfsByQuality(quality:number): Array<GradeDefineConf> { 
        var indexs:Array<number> = this._dic_qualityArr[quality]
        if(!indexs) return null;
        var arr:Array<GradeDefineConf> = [];
        indexs.forEach((index)=>{arr.push(this.getConf(index))});
        return arr;
    }

    private getConf(index:number):GradeDefineConf {  
        if(!this._confDict[index]){
            this._confDict[index] = new GradeDefineConf(this.data[index]);
        } 
        return  this._confDict[index];   
    }

    public getConfList():Array<GradeDefineConf> {
        if(!this.confList){
            this.confList = new Array();
            for(var id in this.data){
                this.confList.push(this.getConf(parseInt(id)));
            }
        }
        return this.confList;
    }
} 
   
/** 装备表配置数据 */
export class GradeDefineConf { 
    data:Array<any> ;
    constructor(data:Array<any>){ 
        this.data = data; 
    }
    /** 等级ID */
    get grade():number{ return this.data[0]; }
    /** 品质 */
    get quality():number{ return this.data[1]; }
    // ... 其他字段
}

优点

  • 通过 ConfMgr.ins 单例访问配置。

  • 自动生成的 getConfByXxx 方法,参数类型与 CSV 中定义的类型一致。

  • 配置数据存储在数组中,通过 getter 访问,既保证了类型安全,又节省了内存(相比直接存对象)。

六、JS 压缩

生成的 JS 文件包含大量空格和长变量名,为了减少体积,我们使用 uglify-js 工具进行压缩。

  • 脚本会自动查找系统中是否安装了 uglifyjs 命令(全局安装)或 npx uglifyjs

  • 遍历 outputJs 目录下所有 .js 文件(排除已压缩的 .min.js),生成对应的 .min.js 文件。

  • 压缩选项包括 --compress--mangle,可以极大地减小文件体积。

七、Zlib 压缩为二进制

最后一步,将 .min.js 文件进一步压缩为二进制 .bin 文件,因为游戏内加载二进制文件比文本文件更快,且体积更小。

  • 读取 conf.json 中的配置,对于独立表(在 otherOutputDir 中定义的),将其 .min.js 压缩后输出到对应的 outputDir 目录,文件名改为 表名.bin

  • 对于公共的 config.min.js,压缩后输出到 outputCompressedJs 指定的路径。

  • 使用 Python 内置的 zlib 库进行压缩,压缩级别为 9(最大压缩)。

八、游戏内读取与使用

在 Cocos Creator 中,我们通过一个 ConfigManager 来加载这些二进制文件。例如:

TypeScript 复制代码
/*
 * @Description: Cocos Creator 3.8.6 配置表加载(支持压缩包解压)
 * @Author: danny
 * @Date: 2025-02-15 15:20:00
 */
import pako from 'pako';
import { _decorator, assetManager, BufferAsset } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('ConfigManager')
export class ConfigManager {
  private static _instance: ConfigManager | null = null;

  public static get instance(): ConfigManager {
    if (!this._instance) {
      this._instance = new ConfigManager();
    }
    return this._instance;
  }

  /**加载压缩的二进制配置文件*/
  loadConfig(bundleName: string, path: string) {
    assetManager.loadBundle(bundleName, BufferAsset, (err, bundle) => {
      if (err) {
        console.error('bundle加载失败,bundleName:', bundleName, "err:", err);
        return;
      }
      bundle.load(path, (err, arrayBuffer) => {
        if (err) {
          console.error('本地加载失败,尝试加载远程config', ", bundleName:", bundleName, "path:", path, "err:", err);
          this.remoteConfig();
          return;
        }
        this.parseBinData(arrayBuffer);
      });
    });
  }

  /** 解析配置数据*/
  parseBinData(bufferAsset) {
    const arrayBuffer = bufferAsset.buffer();
    const compressedData = new Uint8Array(arrayBuffer);
    try {
      const inflated = pako.inflate(compressedData);

      const decoder = new TextDecoder('utf-8');
      const fullText = decoder.decode(inflated);

      // 提取 JSON 对象部分
      const jsonStart = fullText.indexOf('{');
      const jsonEnd = fullText.lastIndexOf('}');
      if (jsonStart === -1 || jsonEnd === -1) {
        console.error('无效的格式,未找到有效的 JSON 对象');
        return;
      }

      const jsonString = fullText.substring(jsonStart, jsonEnd + 1);

      if (!window["gameConfData"]) {
        window["gameConfData"] = new Function('return ' + jsonString)();
      } else {
        window["gameConfData"] = Object.assign(window["gameConfData"], new Function('return ' + jsonString)());
      }
      console.log("配置加载成功", window["gameConfData"]);
    } catch (e) {
      console.error('解压或解析失败:', e);
    }
  }
}

然后就可以在代码中愉快地使用自动生成的类了:

如果你正在使用 Cocos Creator 开发游戏,并且深受 Excel 配置管理的困扰,不妨参考本文的思路,搭建一套属于自己的配置导出工具,让开发更高效、更愉快!

相关推荐
henry1010102 小时前
DeepSeek生成的HTML5小游戏 -- 投篮小能手
前端·javascript·css·游戏·html5
道纪书生2 小时前
解决报错:很抱歉,powerpoint/word/excel遇到错误,使其无法正常工作......
word·powerpoint·excel
德育处主任Pro2 小时前
『NAS』在飞牛部署一个积木塔游戏-TowerBlocks
游戏
南部余额2 小时前
Apache POI 从入门到实战:Excel 与 Word操作攻略
java·word·excel·poi
Esaka_Forever2 小时前
「为什么不用 Flutter 做游戏」「为什么不用 Cocos 做 App 界面」
flutter·游戏
lihao lihao2 小时前
页面自动化常见函数重点说明
运维·自动化
iambooo2 小时前
基于日志的故障定位与自动化分析体系
运维·自动化
杜子不疼.9 小时前
UU云电脑深度测评:高性价比游戏云电脑,办公版本即将推出
服务器·游戏·电脑
你好!蒋韦杰-(烟雨平生)1 天前
Opengl模拟水面
c++·游戏·3d