游戏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/ 目录。

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

  • 支持按第二行的类型将数字列转为 int 或 float。

为什么需要这一步?

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

五、CSV → JS + TS

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

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

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

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

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

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

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

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 配置管理的困扰,不妨参考本文的思路,搭建一套属于自己的配置导出工具,让开发更高效、更愉快!

相关推荐
在角落发呆18 小时前
工具配置备份步骤:从手动备份到自动化方案
运维·windows·自动化
回眸&啤酒鸭20 小时前
【回眸】自动化白盒测试落地实战指南
运维·自动化·log4j
优选资讯20 小时前
标签打印软件怎么对接 Excel 数据
前端·excel
河南花仙子科技1 天前
企业私域死气沉沉?小游戏盘活用户活跃度
游戏
机核研创社1 天前
睡裤明橡筋自动化选型指南与落地四步走
运维·自动化
志栋智能1 天前
超自动化运维,告别复杂系统的新范式
运维·自动化
泡海椒1 天前
JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示
开发语言·python·excel
AIGS0011 天前
从814张表到250个本体:本体建模在做什么
数据库·excel·erp·数据中台·智能问数·本体语义·企业大脑
Lonely丶墨轩1 天前
用 Vue + Spring Boot + Python 做一个 AI 双人海龟汤游戏:从出题、联机到自动审稿等核心技术设计与实现
spring boot·后端·游戏
泡海椒1 天前
JQuick-Excel VALIDATION 实战:用作用范围约束 Excel 导入校验
开发语言·python·excel