Cocos2d-x配置表解析(Excel/CSV转JSON/Dictionary)详解

举报
William 发表于 2025/12/04 09:42:44 2025/12/04
【摘要】 引言在游戏开发中,配置表是管理游戏数据的重要工具。Excel和CSV因其易用性和灵活性成为策划人员编辑配置的首选格式,而游戏运行时需要将这些数据转换为高效的数据结构。Cocos2d-x作为主流游戏引擎,需要一套高效的配置表解析方案来实现Excel/CSV到JSON/Dictionary的转换。本文将深入探讨配置表解析的完整实现方案,帮助开发者建立高效的数据管理流程。技术背景配置表在游戏开发中...

引言

在游戏开发中,配置表是管理游戏数据的重要工具。Excel和CSV因其易用性和灵活性成为策划人员编辑配置的首选格式,而游戏运行时需要将这些数据转换为高效的数据结构。Cocos2d-x作为主流游戏引擎,需要一套高效的配置表解析方案来实现Excel/CSV到JSON/Dictionary的转换。本文将深入探讨配置表解析的完整实现方案,帮助开发者建立高效的数据管理流程。

技术背景

配置表在游戏开发中的作用

  1. 数据驱动设计:分离数据与逻辑,提高开发效率
  2. 平衡性调整:策划可通过修改表格调整游戏参数
  3. 多语言支持:统一管理不同语言的文本内容
  4. 热更新支持:动态更新配置数据
  5. 版本管理:清晰追踪配置变更历史

常见配置表格式对比

格式
优点
缺点
适用场景
Excel (.xlsx)
可视化编辑,公式支持
解析复杂,体积较大
复杂数据结构
CSV (.csv)
轻量简单,通用性强
无格式,不支持复杂结构
简单表格数据
JSON (.json)
结构化好,易于解析
编辑不便,易出错
最终游戏数据
XML (.xml)
结构化好,支持注释
冗长,解析慢
传统项目
Lua (.lua)
可直接执行,灵活
安全性问题
脚本化配置

Cocos2d-x数据处理能力

  • FileUtils:跨平台文件读取
  • rapidjson:高效JSON解析库
  • ValueMap/ValueVector:引擎内置数据结构
  • UserDefault:轻量级键值存储

应用使用场景

  1. 角色属性配置:生命值、攻击力、防御力等
  2. 物品数据配置:道具效果、价格、图标
  3. 关卡设计配置:敌人分布、地形布局
  4. 技能系统配置:冷却时间、伤害公式
  5. 多语言文本配置:界面文字、对话内容
  6. 经济系统配置:货币汇率、掉落概率
  7. AI行为配置:行为树参数、权重设置

不同场景下详细代码实现

场景1:CSV解析器实现

// CSVParser.h
#ifndef __CSV_PARSER_H__
#define __CSV_PARSER_H__

#include "cocos2d.h"
#include <vector>
#include <string>
#include <sstream>

class CSVParser {
public:
    // 解析CSV文件内容
    static std::vector<std::vector<std::string>> parseCSV(const std::string& content);
    
    // 解析CSV文件
    static std::vector<std::vector<std::string>> parseCSVFile(const std::string& filePath);
    
    // 将CSV数据转换为ValueMap
    static cocos2d::ValueMap convertToValueMap(
        const std::vector<std::vector<std::string>>& data, 
        const std::string& keyColumn);
    
    // 将CSV数据转换为ValueVector
    static cocos2d::ValueVector convertToValueVector(
        const std::vector<std::vector<std::string>>& data);

private:
    // 分割CSV行(处理引号和逗号)
    static std::vector<std::string> splitCSVLine(const std::string& line);
    
    // 去除字符串两端空格
    static std::string trim(const std::string& str);
};

#endif // __CSV_PARSER_H__
// CSVParser.cpp
#include "CSVParser.h"
#include "platform/CCFileUtils.h"
#include <algorithm>
#include <cctype>
#include <locale>

USING_NS_CC;

std::vector<std::vector<std::string>> CSVParser::parseCSV(const std::string& content) {
    std::vector<std::vector<std::string>> data;
    std::istringstream stream(content);
    std::string line;
    
    while (std::getline(stream, line)) {
        if (!line.empty()) {
            data.push_back(splitCSVLine(line));
        }
    }
    
    return data;
}

std::vector<std::vector<std::string>> CSVParser::parseCSVFile(const std::string& filePath) {
    auto fileUtils = FileUtils::getInstance();
    if (!fileUtils->isFileExist(filePath)) {
        CCLOG("CSV file not found: %s", filePath.c_str());
        return {};
    }
    
    std::string content = fileUtils->getStringFromFile(filePath);
    return parseCSV(content);
}

std::vector<std::string> CSVParser::splitCSVLine(const std::string& line) {
    std::vector<std::string> fields;
    std::string field;
    bool inQuotes = false;
    
    for (size_t i = 0; i < line.length(); ++i) {
        char c = line[i];
        
        if (c == '"') {
            // 处理转义引号
            if (inQuotes && i + 1 < line.length() && line[i + 1] == '"') {
                field += '"';
                ++i;
            } else {
                inQuotes = !inQuotes;
            }
        } else if (c == ',' && !inQuotes) {
            // 字段分隔符
            fields.push_back(trim(field));
            field.clear();
        } else {
            field += c;
        }
    }
    
    // 添加最后一个字段
    fields.push_back(trim(field));
    
    return fields;
}

std::string CSVParser::trim(const std::string& str) {
    size_t start = str.find_first_not_of(" \t\n\r");
    size_t end = str.find_last_not_of(" \t\n\r");
    
    if (start == std::string::npos) {
        return "";
    }
    
    return str.substr(start, end - start + 1);
}

ValueMap CSVParser::convertToValueMap(
    const std::vector<std::vector<std::string>>& data, 
    const std::string& keyColumn) {
    
    ValueMap result;
    
    if (data.empty()) {
        return result;
    }
    
    // 获取列名
    std::vector<std::string> headers = data[0];
    int keyIndex = -1;
    
    // 查找key列索引
    for (size_t i = 0; i < headers.size(); ++i) {
        if (headers[i] == keyColumn) {
            keyIndex = static_cast<int>(i);
            break;
        }
    }
    
    if (keyIndex == -1) {
        CCLOG("Key column '%s' not found", keyColumn.c_str());
        return result;
    }
    
    // 处理数据行
    for (size_t row = 1; row < data.size(); ++row) {
        const auto& fields = data[row];
        if (fields.size() <= static_cast<size_t>(keyIndex)) {
            continue; // 跳过无效行
        }
        
        std::string key = fields[keyIndex];
        ValueMap rowMap;
        
        for (size_t col = 0; col < headers.size(); ++col) {
            if (col < fields.size()) {
                rowMap[headers[col]] = Value(fields[col]);
            } else {
                rowMap[headers[col]] = Value("");
            }
        }
        
        result[key] = Value(rowMap);
    }
    
    return result;
}

ValueVector CSVParser::convertToValueVector(
    const std::vector<std::vector<std::string>>& data) {
    
    ValueVector result;
    
    if (data.empty()) {
        return result;
    }
    
    // 获取列名
    std::vector<std::string> headers = data[0];
    
    // 处理数据行
    for (size_t row = 1; row < data.size(); ++row) {
        const auto& fields = data[row];
        ValueMap rowMap;
        
        for (size_t col = 0; col < headers.size(); ++col) {
            if (col < fields.size()) {
                rowMap[headers[col]] = Value(fields[col]);
            } else {
                rowMap[headers[col]] = Value("");
            }
        }
        
        result.push_back(Value(rowMap));
    }
    
    return result;
}

场景2:Excel转JSON工具

// ExcelToJsonConverter.h
#ifndef __EXCEL_TO_JSON_CONVERTER_H__
#define __EXCEL_TO_JSON_CONVERTER_H__

#include "cocos2d.h"
#include "json/document.h"

class ExcelToJsonConverter {
public:
    // 将CSV数据转换为JSON字符串
    static std::string convertToJson(const std::vector<std::vector<std::string>>& data);
    
    // 将CSV文件转换为JSON文件
    static bool convertCSVToJsonFile(const std::string& csvPath, const std::string& jsonPath);
    
    // 将JSON字符串解析为ValueMap
    static cocos2d::ValueMap parseJsonToValueMap(const std::string& jsonStr);
    
    // 将JSON字符串解析为ValueVector
    static cocos2d::ValueVector parseJsonToValueVector(const std::string& jsonStr);

private:
    // 将CSV行转换为JSON对象
    static rapidjson::Value convertRowToJson(
        rapidjson::Document::AllocatorType& allocator,
        const std::vector<std::string>& headers,
        const std::vector<std::string>& row);
};

#endif // __EXCEL_TO_JSON_CONVERTER_H__
// ExcelToJsonConverter.cpp
#include "ExcelToJsonConverter.h"
#include "CSVParser.h"
#include "json/document.h"
#include "json/writer.h"
#include "json/stringbuffer.h"
#include "platform/CCFileUtils.h"

using namespace rapidjson;

std::string ExcelToJsonConverter::convertToJson(const std::vector<std::vector<std::string>>& data) {
    Document document;
    document.SetObject();
    auto& allocator = document.GetAllocator();
    
    if (data.empty()) {
        StringBuffer buffer;
        Writer<StringBuffer> writer(buffer);
        document.Accept(writer);
        return buffer.GetString();
    }
    
    // 获取列名
    std::vector<std::string> headers = data[0];
    
    // 创建行数组
    Value rows(kArrayType);
    
    // 处理数据行
    for (size_t i = 1; i < data.size(); ++i) {
        rows.PushBack(convertRowToJson(allocator, headers, data[i]), allocator);
    }
    
    document.AddMember("headers", Value(kArrayType), allocator);
    Value& headersArray = document["headers"];
    for (const auto& header : headers) {
        headersArray.PushBack(Value(header.c_str(), allocator), allocator);
    }
    
    document.AddMember("rows", rows, allocator);
    
    // 序列化为字符串
    StringBuffer buffer;
    Writer<StringBuffer> writer(buffer);
    document.Accept(writer);
    
    return buffer.GetString();
}

bool ExcelToJsonConverter::convertCSVToJsonFile(const std::string& csvPath, const std::string& jsonPath) {
    // 解析CSV文件
    auto data = CSVParser::parseCSVFile(csvPath);
    if (data.empty()) {
        CCLOG("Failed to parse CSV file: %s", csvPath.c_str());
        return false;
    }
    
    // 转换为JSON
    std::string jsonStr = convertToJson(data);
    
    // 写入文件
    auto fileUtils = FileUtils::getInstance();
    return fileUtils->writeStringToFile(jsonStr, jsonPath);
}

ValueMap ExcelToJsonConverter::parseJsonToValueMap(const std::string& jsonStr) {
    ValueMap result;
    Document document;
    
    if (document.Parse(jsonStr.c_str()).HasParseError()) {
        CCLOG("JSON parse error: %s", document.GetParseError());
        return result;
    }
    
    if (!document.IsObject()) {
        CCLOG("JSON root is not an object");
        return result;
    }
    
    // 假设JSON结构为{"key": {"field1": "value1", ...}, ...}
    for (auto it = document.MemberBegin(); it != document.MemberEnd(); ++it) {
        const char* key = it->name.GetString();
        Value& value = it->value;
        
        if (value.IsObject()) {
            ValueMap rowMap;
            for (auto rowIt = value.MemberBegin(); rowIt != value.MemberEnd(); ++rowIt) {
                const char* field = rowIt->name.GetString();
                Value& fieldValue = rowIt->value;
                
                if (fieldValue.IsString()) {
                    rowMap[field] = Value(fieldValue.GetString());
                } else if (fieldValue.IsInt()) {
                    rowMap[field] = Value(fieldValue.GetInt());
                } else if (fieldValue.IsDouble()) {
                    rowMap[field] = Value(fieldValue.GetDouble());
                } else if (fieldValue.IsBool()) {
                    rowMap[field] = Value(fieldValue.GetBool());
                }
            }
            result[key] = Value(rowMap);
        }
    }
    
    return result;
}

ValueVector ExcelToJsonConverter::parseJsonToValueVector(const std::string& jsonStr) {
    ValueVector result;
    Document document;
    
    if (document.Parse(jsonStr.c_str()).HasParseError()) {
        CCLOG("JSON parse error: %s", document.GetParseError());
        return result;
    }
    
    if (!document.IsArray()) {
        CCLOG("JSON root is not an array");
        return result;
    }
    
    for (SizeType i = 0; i < document.Size(); ++i) {
        Value& item = document[i];
        if (!item.IsObject()) continue;
        
        ValueMap rowMap;
        for (auto it = item.MemberBegin(); it != item.MemberEnd(); ++it) {
            const char* field = it->name.GetString();
            Value& fieldValue = it->value;
            
            if (fieldValue.IsString()) {
                rowMap[field] = Value(fieldValue.GetString());
            } else if (fieldValue.IsInt()) {
                rowMap[field] = Value(fieldValue.GetInt());
            } else if (fieldValue.IsDouble()) {
                rowMap[field] = Value(fieldValue.GetDouble());
            } else if (fieldValue.IsBool()) {
                rowMap[field] = Value(fieldValue.GetBool());
            }
        }
        result.push_back(Value(rowMap));
    }
    
    return result;
}

Value ExcelToJsonConverter::convertRowToJson(
    rapidjson::Document::AllocatorType& allocator,
    const std::vector<std::string>& headers,
    const std::vector<std::string>& row) {
    
    Value obj(kObjectType);
    
    for (size_t i = 0; i < headers.size(); ++i) {
        if (i < row.size()) {
            obj.AddMember(
                Value(headers[i].c_str(), allocator).Move(),
                Value(row[i].c_str(), allocator).Move(),
                allocator
            );
        } else {
            obj.AddMember(
                Value(headers[i].c_str(), allocator).Move(),
                Value("").Move(),
                allocator
            );
        }
    }
    
    return obj;
}

场景3:配置表管理器

// ConfigManager.h
#ifndef __CONFIG_MANAGER_H__
#define __CONFIG_MANAGER_H__

#include "cocos2d.h"
#include "CSVParser.h"
#include "ExcelToJsonConverter.h"

class ConfigManager {
public:
    static ConfigManager* getInstance();
    static void destroyInstance();
    
    bool init();
    
    // 加载配置表
    bool loadConfig(const std::string& tableName, const std::string& filePath, bool useJsonCache = true);
    
    // 获取数据
    cocos2d::ValueMap getConfigMap(const std::string& tableName);
    cocos2d::ValueVector getConfigVector(const std::string& tableName);
    cocos2d::Value getConfigValue(const std::string& tableName, const std::string& key, const std::string& field);
    
    // 预加载所有配置
    void preloadAllConfigs();
    
    // 释放配置
    void releaseConfig(const std::string& tableName);
    void releaseAllConfigs();

private:
    ConfigManager();
    ~ConfigManager();
    
    static ConfigManager* _instance;
    
    std::map<std::string, ValueMap> _configMaps;
    std::map<std::string, ValueVector> _configVectors;
    std::map<std::string, std::string> _configPaths;
    
    // 配置表元数据
    struct ConfigMeta {
        std::string filePath;
        std::string keyField;
        bool loaded;
    };
    std::map<std::string, ConfigMeta> _configMetas;
};

#endif // __CONFIG_MANAGER_H__
// ConfigManager.cpp
#include "ConfigManager.h"

USING_NS_CC;

ConfigManager* ConfigManager::_instance = nullptr;

ConfigManager::ConfigManager() {}

ConfigManager::~ConfigManager() {
    releaseAllConfigs();
}

ConfigManager* ConfigManager::getInstance() {
    if (!_instance) {
        _instance = new (std::nothrow) ConfigManager();
        if (_instance && _instance->init()) {
            _instance->autorelease();
        } else {
            CC_SAFE_DELETE(_instance);
            _instance = nullptr;
        }
    }
    return _instance;
}

void ConfigManager::destroyInstance() {
    CC_SAFE_DELETE(_instance);
    _instance = nullptr;
}

bool ConfigManager::init() {
    return true;
}

bool ConfigManager::loadConfig(const std::string& tableName, const std::string& filePath, bool useJsonCache) {
    // 检查是否已加载
    if (_configMaps.find(tableName) != _configMaps.end() || 
        _configVectors.find(tableName) != _configVectors.end()) {
        CCLOG("Config table '%s' already loaded", tableName.c_str());
        return true;
    }
    
    std::string resolvedPath = FileUtils::getInstance()->fullPathForFilename(filePath);
    if (resolvedPath.empty()) {
        CCLOG("Config file not found: %s", filePath.c_str());
        return false;
    }
    
    // 检查JSON缓存
    std::string jsonPath = resolvedPath + ".json";
    bool useCache = useJsonCache && FileUtils::getInstance()->isFileExist(jsonPath);
    
    if (useCache) {
        // 从JSON缓存加载
        std::string jsonStr = FileUtils::getInstance()->getStringFromFile(jsonPath);
        _configMaps[tableName] = ExcelToJsonConverter::parseJsonToValueMap(jsonStr);
    } else {
        // 从CSV加载
        auto data = CSVParser::parseCSVFile(resolvedPath);
        if (data.empty()) {
            CCLOG("Failed to parse config file: %s", resolvedPath.c_str());
            return false;
        }
        
        // 存储为ValueVector
        _configVectors[tableName] = CSVParser::convertToValueVector(data);
        
        // 同时存储为ValueMap(使用第一列作为key)
        if (!data.empty() && !data[0].empty()) {
            _configMaps[tableName] = CSVParser::convertToValueMap(data, data[0][0]);
        }
        
        // 创建JSON缓存
        if (useJsonCache) {
            std::string jsonStr = ExcelToJsonConverter::convertToJson(data);
            FileUtils::getInstance()->writeStringToFile(jsonStr, jsonPath);
        }
    }
    
    // 记录配置信息
    _configPaths[tableName] = resolvedPath;
    
    CCLOG("Loaded config table: %s from %s", tableName.c_str(), resolvedPath.c_str());
    return true;
}

ValueMap ConfigManager::getConfigMap(const std::string& tableName) {
    auto it = _configMaps.find(tableName);
    if (it != _configMaps.end()) {
        return it->second;
    }
    
    CCLOG("Config table '%s' not loaded", tableName.c_str());
    return ValueMap();
}

ValueVector ConfigManager::getConfigVector(const std::string& tableName) {
    auto it = _configVectors.find(tableName);
    if (it != _configVectors.end()) {
        return it->second;
    }
    
    CCLOG("Config table '%s' not loaded", tableName.c_str());
    return ValueVector();
}

Value ConfigManager::getConfigValue(const std::string& tableName, const std::string& key, const std::string& field) {
    auto mapIt = _configMaps.find(tableName);
    if (mapIt != _configMaps.end()) {
        auto rowIt = mapIt->second.find(key);
        if (rowIt != mapIt->second.end()) {
            ValueMap row = rowIt->second.asValueMap();
            auto fieldIt = row.find(field);
            if (fieldIt != row.end()) {
                return fieldIt->second;
            }
        }
    }
    
    CCLOG("Config value not found: %s.%s.%s", tableName.c_str(), key.c_str(), field.c_str());
    return Value::Null;
}

void ConfigManager::preloadAllConfigs() {
    // 实际项目中应从配置文件读取所有表信息
    // 这里简化处理,加载几个示例表
    loadConfig("items", "configs/items.csv");
    loadConfig("skills", "configs/skills.csv");
    loadConfig("levels", "configs/levels.csv");
}

void ConfigManager::releaseConfig(const std::string& tableName) {
    _configMaps.erase(tableName);
    _configVectors.erase(tableName);
    CCLOG("Released config table: %s", tableName.c_str());
}

void ConfigManager::releaseAllConfigs() {
    _configMaps.clear();
    _configVectors.clear();
    CCLOG("Released all config tables");
}

原理解释

配置表解析系统的核心原理分为三个层次:
  1. 文件读取层
    • 使用Cocos2d-x的FileUtils跨平台读取文件
    • 支持CSV和JSON格式
    • 处理文件路径解析和编码问题
  2. 数据解析层
    • CSV解析器处理逗号分隔值,支持引号转义
    • JSON解析器使用rapidjson库处理结构化数据
    • 数据类型自动推断(字符串、数字、布尔值)
  3. 数据管理层
    • 单例模式管理所有配置表
    • 提供多种数据访问接口(Map/Vector/单值)
    • 支持JSON缓存加速加载
    • 内存管理(按需加载和释放)

核心特性

  1. 多格式支持:CSV/JSON双向转换
  2. 高效解析:流式处理大文件
  3. 数据缓存:JSON缓存加速二次加载
  4. 灵活访问:支持Map和Vector两种数据结构
  5. 类型安全:自动数据类型转换
  6. 错误处理:完善的错误日志和恢复机制
  7. 内存优化:按需加载和释放配置表
  8. 跨平台:支持所有Cocos2d-x目标平台

原理流程图及解释

graph TD
    A[Excel/CSV文件] --> B[文件读取]
    B --> C{格式判断}
    C -->|CSV| D[CSV解析器]
    C -->|JSON| E[JSON解析器]
    D --> F[二维字符串数组]
    E --> G[JSON对象]
    F --> H[数据结构转换]
    G --> H
    H --> I[ValueMap/ValueVector]
    I --> J[配置管理器]
    J --> K[游戏逻辑访问]
    L[配置修改] --> M[导出CSV/JSON]
    M --> A
流程图解释
  1. 原始配置表文件(Excel/CSV)作为输入
  2. 文件读取模块加载文件内容
  3. 根据文件扩展名选择解析器(CSV或JSON)
  4. CSV解析器将文本转换为二维字符串数组
  5. JSON解析器将文本转换为JSON对象
  6. 数据结构转换模块将解析结果转为引擎可用的ValueMap/ValueVector
  7. 配置管理器存储并管理所有配置表
  8. 游戏逻辑通过配置管理器访问数据
  9. 修改后的配置可导出为CSV/JSON供策划使用

环境准备

开发环境要求

  • 引擎版本:Cocos2d-x 3.10+
  • 开发工具:Visual Studio 2017+/Xcode 10+/Android Studio 3.5+
  • 编程语言:C++11或更高版本
  • 依赖库:rapidjson(通常包含在Cocos2d-x中)

项目配置步骤

  1. 创建Cocos2d-x项目
    cocos new ConfigDemo -p com.example.configdemo -l cpp -d ./projects
  2. 添加配置文件
    • 在Resources目录下创建configs文件夹
    • 添加示例CSV文件(如items.csv)
  3. 添加解析器代码
    • 创建CSVParser.h/cpp
    • 创建ExcelToJsonConverter.h/cpp
    • 创建ConfigManager.h/cpp
  4. 配置CMakeLists.txt
    # 添加源文件
    list(APPEND GAME_SOURCE
         Classes/CSVParser.cpp
         Classes/ExcelToJsonConverter.cpp
         Classes/ConfigManager.cpp
    )
    
    # 包含目录
    include_directories(
         Classes
         ${COCOS2D-X_ROOT}/cocos
         ${COCOS2D-X_ROOT}/cocos/platform
         ${COCOS2D-X_ROOT}/external/rapidjson
    )
  5. 初始化配置管理器
    // AppDelegate.cpp
    #include "ConfigManager.h"
    
    bool AppDelegate::applicationDidFinishLaunching() {
        // 初始化导演等...
    
        // 初始化配置管理器
        auto configMgr = ConfigManager::getInstance();
        configMgr->preloadAllConfigs();
    
        return true;
    }

实际详细应用代码示例实现

示例CSV文件

// Resources/configs/items.csv
id,name,type,price,attack,defense
1001,Sword,Weapon,150,25,5
1002,Shield,Shield,120,0,30
1003,Potion,Consumable,20,0,0
1004,Helmet,Armor,80,0,15

游戏中使用配置数据

// ItemSystem.cpp
#include "ItemSystem.h"
#include "ConfigManager.h"

USING_NS_CC;

ItemSystem::ItemSystem() {
    auto configMgr = ConfigManager::getInstance();
    _itemConfig = configMgr->getConfigMap("items");
}

ItemData ItemSystem::getItemData(int itemId) {
    ItemData data;
    std::string idStr = StringUtils::format("%d", itemId);
    
    ValueMap itemMap = _itemConfig[idStr].asValueMap();
    if (itemMap.empty()) {
        CCLOG("Item config not found for ID: %d", itemId);
        return data;
    }
    
    data.id = itemId;
    data.name = itemMap["name"].asString();
    data.type = itemMap["type"].asString();
    data.price = itemMap["price"].asInt();
    data.attack = itemMap["attack"].asInt();
    data.defense = itemMap["defense"].asInt();
    
    return data;
}

std::vector<ItemData> ItemSystem::getAllItemsByType(const std::string& type) {
    std::vector<ItemData> items;
    auto configMgr = ConfigManager::getInstance();
    auto allItems = configMgr->getConfigVector("items");
    
    for (const auto& itemVal : allItems) {
        ValueMap itemMap = itemVal.asValueMap();
        if (itemMap["type"].asString() == type) {
            ItemData data;
            data.id = atoi(itemMap["id"].asString().c_str());
            data.name = itemMap["name"].asString();
            data.type = itemMap["type"].asString();
            data.price = itemMap["price"].asInt();
            data.attack = itemMap["attack"].asInt();
            data.defense = itemMap["defense"].asInt();
            items.push_back(data);
        }
    }
    
    return items;
}

配置表编辑器工具

// ConfigEditor.cpp
#include "ConfigEditor.h"
#include "ExcelToJsonConverter.h"
#include "CSVParser.h"

USING_NS_CC;

bool ConfigEditor::exportCsvToJson(const std::string& csvPath, const std::string& jsonPath) {
    return ExcelToJsonConverter::convertCSVToJsonFile(csvPath, jsonPath);
}

bool ConfigEditor::importJsonToCsv(const std::string& jsonPath, const std::string& csvPath) {
    // 读取JSON文件
    auto fileUtils = FileUtils::getInstance();
    if (!fileUtils->isFileExist(jsonPath)) {
        CCLOG("JSON file not found: %s", jsonPath.c_str());
        return false;
    }
    
    std::string jsonStr = fileUtils->getStringFromFile(jsonPath);
    auto data = ExcelToJsonConverter::parseJsonToValueVector(jsonStr);
    
    // 转换为CSV
    if (data.empty()) {
        CCLOG("No data found in JSON");
        return false;
    }
    
    // 获取列名(取第一行的键)
    ValueMap firstRow = data[0].asValueMap();
    std::vector<std::string> headers;
    for (auto& kv : firstRow) {
        headers.push_back(kv.first);
    }
    
    // 构建CSV内容
    std::string csvContent;
    
    // 添加标题行
    for (size_t i = 0; i < headers.size(); ++i) {
        csvContent += headers[i];
        if (i < headers.size() - 1) {
            csvContent += ",";
        }
    }
    csvContent += "\n";
    
    // 添加数据行
    for (const auto& item : data) {
        ValueMap row = item.asValueMap();
        for (size_t i = 0; i < headers.size(); ++i) {
            auto it = row.find(headers[i]);
            if (it != row.end()) {
                csvContent += it->second.asString();
            }
            if (i < headers.size() - 1) {
                csvContent += ",";
            }
        }
        csvContent += "\n";
    }
    
    // 写入CSV文件
    return fileUtils->writeStringToFile(csvContent, csvPath);
}

bool ConfigEditor::validateCsvFile(const std::string& filePath) {
    auto data = CSVParser::parseCSVFile(filePath);
    if (data.empty()) {
        CCLOG("Invalid CSV file: %s", filePath.c_str());
        return false;
    }
    
    // 检查列数是否一致
    size_t colCount = data[0].size();
    for (size_t i = 1; i < data.size(); ++i) {
        if (data[i].size() != colCount) {
            CCLOG("Row %zu has inconsistent column count: expected %zu, got %zu", 
                  i, colCount, data[i].size());
            return false;
        }
    }
    
    return true;
}

运行结果

运行配置表解析系统后,可以实现以下功能:
  1. 加载CSV配置文件并转换为游戏可用数据结构
  2. 自动生成JSON缓存加速二次加载
  3. 通过配置管理器访问数据
  4. 导出配置为JSON或CSV格式
典型输出日志:
Loaded config table: items from /path/to/resources/configs/items.csv
Item config loaded: Sword (ID:1001)
All weapons: 
  Sword - Attack:25, Price:150
  Axe - Attack:35, Price:200
Exported items.csv to items.json

测试步骤以及详细代码

单元测试框架

// ConfigManagerTest.cpp
#include "gtest/gtest.h"
#include "ConfigManager.h"
#include "CSVParser.h"

USING_NS_CC;

class ConfigManagerTest : public testing::Test {
protected:
    void SetUp() override {
        ConfigManager::destroyInstance();
        _configMgr = ConfigManager::getInstance();
    }
    
    void TearDown() override {
        ConfigManager::destroyInstance();
    }
    
    ConfigManager* _configMgr;
};

TEST_F(ConfigManagerTest, LoadCsvConfig) {
    bool result = _configMgr->loadConfig("test_items", "configs/test_items.csv");
    ASSERT_TRUE(result);
    
    auto configMap = _configMgr->getConfigMap("test_items");
    ASSERT_FALSE(configMap.empty());
    
    auto configVec = _configMgr->getConfigVector("test_items");
    ASSERT_EQ(configVec.size(), 3); // 假设有3行数据
}

TEST_F(ConfigManagerTest, GetConfigValue) {
    _configMgr->loadConfig("test_items", "configs/test_items.csv");
    
    Value value = _configMgr->getConfigValue("test_items", "1001", "name");
    ASSERT_TRUE(value.isString());
    ASSERT_EQ(value.asString(), "Sword");
    
    value = _configMgr->getConfigValue("test_items", "1002", "price");
    ASSERT_TRUE(value.isInt());
    ASSERT_EQ(value.asInt(), 120);
}

TEST_F(ConfigManagerTest, CsvParsing) {
    std::string csvContent = "id,name,price\n1001,Sword,150\n1002,Shield,120";
    auto data = CSVParser::parseCSV(csvContent);
    
    ASSERT_EQ(data.size(), 3); // 标题行 + 2数据行
    ASSERT_EQ(data[0].size(), 3);
    ASSERT_EQ(data[0][0], "id");
    ASSERT_EQ(data[1][1], "Sword");
}

TEST_F(ConfigManagerTest, JsonConversion) {
    std::string csvPath = "configs/test_items.csv";
    std::string jsonPath = csvPath + ".json";
    
    bool result = ExcelToJsonConverter::convertCSVToJsonFile(csvPath, jsonPath);
    ASSERT_TRUE(result);
    
    ASSERT_TRUE(FileUtils::getInstance()->isFileExist(jsonPath));
}

手动测试步骤

  1. 创建测试CSV文件
    id,name,type,price
    101,Sword,Weapon,150
    102,Shield,Shield,120
  2. 加载配置表
    auto configMgr = ConfigManager::getInstance();
    configMgr->loadConfig("items", "configs/items.csv");
  3. 访问配置数据
    auto itemMap = configMgr->getConfigMap("items");
    auto sword = itemMap["101"].asValueMap();
    CCLOG("Sword price: %d", sword["price"].asInt());
  4. 导出为JSON
    ExcelToJsonConverter::convertCSVToJsonFile(
        "configs/items.csv", 
        "configs/items.json"
    );
  5. 验证JSON文件内容
    {
      "headers": ["id", "name", "type", "price"],
      "rows": [
        {"id": "101", "name": "Sword", "type": "Weapon", "price": "150"},
        {"id": "102", "name": "Shield", "type": "Shield", "price": "120"}
      ]
    }

部署场景

  1. 开发阶段
    • 策划使用Excel编辑配置
    • 导出CSV供程序员使用
    • 配置管理器加载CSV到内存
  2. 生产环境
    • 使用JSON缓存加速加载
    • 按需加载配置表
    • 定期更新配置(热更新)
  3. 多平台部署
    • 移动端:使用JSON缓存减少解析时间
    • PC端:直接从CSV加载便于调试
    • Web端:使用JSON格式减少传输大小
  4. 持续集成
    • 自动将Excel转为CSV/JSON
    • 验证配置表格式正确性
    • 生成配置文档
  5. 热更新系统
    • 从服务器下载最新配置
    • 替换本地配置缓存
    • 重新加载配置表

疑难解答

常见问题1:CSV解析错误

症状:特殊字符导致解析失败
原因
  • 逗号出现在引号内
  • 换行符处理不当
  • 编码问题(UTF-8 BOM)
解决方案
// 增强CSV解析器
std::vector<std::string> CSVParser::splitCSVLine(const std::string& line) {
    std::vector<std::string> fields;
    std::string field;
    bool inQuotes = false;
    bool wasInQuotes = false;
    
    for (size_t i = 0; i < line.length(); ++i) {
        char c = line[i];
        
        if (c == '"') {
            if (inQuotes && i + 1 < line.length() && line[i + 1] == '"') {
                // 转义引号
                field += '"';
                ++i;
            } else {
                // 切换引号状态
                inQuotes = !inQuotes;
                wasInQuotes = true;
            }
        } else if (c == ',' && !inQuotes) {
            // 处理字段
            if (wasInQuotes) {
                // 移除包围的引号
                if (!field.empty() && field.front() == '"' && field.back() == '"') {
                    field = field.substr(1, field.size() - 2);
                }
                wasInQuotes = false;
            }
            fields.push_back(trim(field));
            field.clear();
        } else {
            field += c;
        }
    }
    
    // 处理最后一个字段
    if (wasInQuotes) {
        if (!field.empty() && field.front() == '"' && field.back() == '"') {
            field = field.substr(1, field.size() - 2);
        }
    }
    fields.push_back(trim(field));
    
    return fields;
}

常见问题2:大数据量性能低下

症状:加载大型配置表时卡顿
原因
  • 一次性加载整个文件到内存
  • 频繁的字符串操作
  • 缺乏数据分块处理
解决方案
// 流式处理大型CSV
void CSVParser::parseLargeCSV(const std::string& filePath, const std::function<void(const std::vector<std::string>&)>& rowCallback) {
    auto fileUtils = FileUtils::getInstance();
    auto fileData = fileUtils->getDataFromFile(filePath);
    if (fileData.isNull()) {
        CCLOG("Failed to read file: %s", filePath.c_str());
        return;
    }
    
    std::string content(reinterpret_cast<char*>(fileData.getBytes()), fileData.getSize());
    std::istringstream stream(content);
    std::string line;
    bool firstLine = true;
    
    while (std::getline(stream, line)) {
        if (firstLine) {
            // 跳过标题行
            firstLine = false;
            continue;
        }
        
        if (!line.empty()) {
            auto fields = splitCSVLine(line);
            rowCallback(fields);
        }
    }
}

// 使用示例
CSVParser::parseLargeCSV("large_config.csv", [](const std::vector<std::string>& fields) {
    // 逐行处理数据
    processRow(fields);
});

常见问题3:跨平台路径问题

症状:不同平台下文件路径不一致
原因
  • Windows使用反斜杠
  • Linux/macOS使用正斜杠
  • 资源路径大小写敏感
解决方案
// 规范化路径处理
std::string normalizePath(const std::string& path) {
    std::string normalized = path;
    
    // 替换反斜杠为正斜杠
    std::replace(normalized.begin(), normalized.end(), '\\', '/');
    
    // 移除多余斜杠
    auto pos = normalized.find("//");
    while (pos != std::string::npos) {
        normalized.replace(pos, 2, "/");
        pos = normalized.find("//", pos);
    }
    
    // 转换为小写(可选,根据平台)
    #if defined(CC_TARGET_OS_WINDOWS)
        // Windows路径不区分大小写
        std::transform(normalized.begin(), normalized.end(), normalized.begin(), ::tolower);
    #endif
    
    return normalized;
}

// 使用示例
std::string path = "Configs\\Items.csv";
std::string normalizedPath = normalizePath(path); // "configs/items.csv"

未来展望

  1. 二进制格式支持:开发自定义二进制格式提升加载速度
  2. 数据压缩:集成LZ4/Zstandard压缩配置数据
  3. Schema验证:基于JSON Schema验证配置结构
  4. 热重载:运行时动态更新配置
  5. 可视化编辑器:开发图形化配置编辑工具
  6. 数据版本控制:集成Git管理配置变更
  7. AI辅助校验:自动检测配置错误和不平衡

技术趋势与挑战

趋势

  1. 数据驱动开发:配置表作为游戏设计核心
  2. 实时协作编辑:多人同时编辑配置
  3. 云原生配置:配置存储于云端动态下发
  4. 类型安全DSL:领域特定语言定义配置
  5. 自动化测试:基于配置的自动化测试

挑战

  1. 大规模数据:万行级配置表处理
  2. 数据安全:防止敏感配置泄露
  3. 版本兼容:新旧配置格式兼容
  4. 跨团队协作:策划与程序的数据契约
  5. 国际化:多语言配置管理

总结

Cocos2d-x配置表解析系统为游戏开发提供了高效的数据管理方案:
  1. 核心价值
    • 实现Excel/CSV到游戏数据的无缝转换
    • 提供灵活的数据访问接口
    • 优化加载性能和内存使用
    • 支持配置热更新
  2. 关键技术
    • 健壮的CSV解析器
    • 高效的JSON转换器
    • 智能的配置管理器
    • 完善的错误处理
  3. 最佳实践
    • 使用JSON缓存加速加载
    • 按需加载大型配置表
    • 分离开发和生产配置
    • 自动化配置验证
  4. 应用场景
    • 游戏数值配置
    • 多语言文本管理
    • 关卡设计数据
    • 热更新内容
通过实施本方案,开发团队可以:
  • 提高策划和程序协作效率
  • 简化游戏数据调整流程
  • 增强配置数据的安全性和可维护性
  • 为后续热更新和数据驱动开发奠定基础
随着游戏复杂度提升,配置表解析系统将成为游戏架构中越来越重要的组成部分。未来,结合云服务和AI技术,配置管理将更加智能化、自动化,为游戏开发带来更大便利。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。