Cocos2d-x配置表解析(Excel/CSV转JSON/Dictionary)详解
【摘要】 引言在游戏开发中,配置表是管理游戏数据的重要工具。Excel和CSV因其易用性和灵活性成为策划人员编辑配置的首选格式,而游戏运行时需要将这些数据转换为高效的数据结构。Cocos2d-x作为主流游戏引擎,需要一套高效的配置表解析方案来实现Excel/CSV到JSON/Dictionary的转换。本文将深入探讨配置表解析的完整实现方案,帮助开发者建立高效的数据管理流程。技术背景配置表在游戏开发中...
引言
技术背景
配置表在游戏开发中的作用
-
数据驱动设计:分离数据与逻辑,提高开发效率 -
平衡性调整:策划可通过修改表格调整游戏参数 -
多语言支持:统一管理不同语言的文本内容 -
热更新支持:动态更新配置数据 -
版本管理:清晰追踪配置变更历史
常见配置表格式对比
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Cocos2d-x数据处理能力
-
FileUtils:跨平台文件读取 -
rapidjson:高效JSON解析库 -
ValueMap/ValueVector:引擎内置数据结构 -
UserDefault:轻量级键值存储
应用使用场景
-
角色属性配置:生命值、攻击力、防御力等 -
物品数据配置:道具效果、价格、图标 -
关卡设计配置:敌人分布、地形布局 -
技能系统配置:冷却时间、伤害公式 -
多语言文本配置:界面文字、对话内容 -
经济系统配置:货币汇率、掉落概率 -
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");
}
原理解释
-
文件读取层: -
使用Cocos2d-x的FileUtils跨平台读取文件 -
支持CSV和JSON格式 -
处理文件路径解析和编码问题
-
-
数据解析层: -
CSV解析器处理逗号分隔值,支持引号转义 -
JSON解析器使用rapidjson库处理结构化数据 -
数据类型自动推断(字符串、数字、布尔值)
-
-
数据管理层: -
单例模式管理所有配置表 -
提供多种数据访问接口(Map/Vector/单值) -
支持JSON缓存加速加载 -
内存管理(按需加载和释放)
-
核心特性
-
多格式支持:CSV/JSON双向转换 -
高效解析:流式处理大文件 -
数据缓存:JSON缓存加速二次加载 -
灵活访问:支持Map和Vector两种数据结构 -
类型安全:自动数据类型转换 -
错误处理:完善的错误日志和恢复机制 -
内存优化:按需加载和释放配置表 -
跨平台:支持所有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
-
原始配置表文件(Excel/CSV)作为输入 -
文件读取模块加载文件内容 -
根据文件扩展名选择解析器(CSV或JSON) -
CSV解析器将文本转换为二维字符串数组 -
JSON解析器将文本转换为JSON对象 -
数据结构转换模块将解析结果转为引擎可用的ValueMap/ValueVector -
配置管理器存储并管理所有配置表 -
游戏逻辑通过配置管理器访问数据 -
修改后的配置可导出为CSV/JSON供策划使用
环境准备
开发环境要求
-
引擎版本:Cocos2d-x 3.10+ -
开发工具:Visual Studio 2017+/Xcode 10+/Android Studio 3.5+ -
编程语言:C++11或更高版本 -
依赖库:rapidjson(通常包含在Cocos2d-x中)
项目配置步骤
-
创建Cocos2d-x项目 cocos new ConfigDemo -p com.example.configdemo -l cpp -d ./projects -
添加配置文件 -
在Resources目录下创建configs文件夹 -
添加示例CSV文件(如items.csv)
-
-
添加解析器代码 -
创建CSVParser.h/cpp -
创建ExcelToJsonConverter.h/cpp -
创建ConfigManager.h/cpp
-
-
配置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 ) -
初始化配置管理器 // 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;
}
运行结果
-
加载CSV配置文件并转换为游戏可用数据结构 -
自动生成JSON缓存加速二次加载 -
通过配置管理器访问数据 -
导出配置为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));
}
手动测试步骤
-
创建测试CSV文件 id,name,type,price 101,Sword,Weapon,150 102,Shield,Shield,120 -
加载配置表 auto configMgr = ConfigManager::getInstance(); configMgr->loadConfig("items", "configs/items.csv"); -
访问配置数据 auto itemMap = configMgr->getConfigMap("items"); auto sword = itemMap["101"].asValueMap(); CCLOG("Sword price: %d", sword["price"].asInt()); -
导出为JSON ExcelToJsonConverter::convertCSVToJsonFile( "configs/items.csv", "configs/items.json" ); -
验证JSON文件内容 { "headers": ["id", "name", "type", "price"], "rows": [ {"id": "101", "name": "Sword", "type": "Weapon", "price": "150"}, {"id": "102", "name": "Shield", "type": "Shield", "price": "120"} ] }
部署场景
-
开发阶段: -
策划使用Excel编辑配置 -
导出CSV供程序员使用 -
配置管理器加载CSV到内存
-
-
生产环境: -
使用JSON缓存加速加载 -
按需加载配置表 -
定期更新配置(热更新)
-
-
多平台部署: -
移动端:使用JSON缓存减少解析时间 -
PC端:直接从CSV加载便于调试 -
Web端:使用JSON格式减少传输大小
-
-
持续集成: -
自动将Excel转为CSV/JSON -
验证配置表格式正确性 -
生成配置文档
-
-
热更新系统: -
从服务器下载最新配置 -
替换本地配置缓存 -
重新加载配置表
-
疑难解答
常见问题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"
未来展望
-
二进制格式支持:开发自定义二进制格式提升加载速度 -
数据压缩:集成LZ4/Zstandard压缩配置数据 -
Schema验证:基于JSON Schema验证配置结构 -
热重载:运行时动态更新配置 -
可视化编辑器:开发图形化配置编辑工具 -
数据版本控制:集成Git管理配置变更 -
AI辅助校验:自动检测配置错误和不平衡
技术趋势与挑战
趋势
-
数据驱动开发:配置表作为游戏设计核心 -
实时协作编辑:多人同时编辑配置 -
云原生配置:配置存储于云端动态下发 -
类型安全DSL:领域特定语言定义配置 -
自动化测试:基于配置的自动化测试
挑战
-
大规模数据:万行级配置表处理 -
数据安全:防止敏感配置泄露 -
版本兼容:新旧配置格式兼容 -
跨团队协作:策划与程序的数据契约 -
国际化:多语言配置管理
总结
-
核心价值: -
实现Excel/CSV到游戏数据的无缝转换 -
提供灵活的数据访问接口 -
优化加载性能和内存使用 -
支持配置热更新
-
-
关键技术: -
健壮的CSV解析器 -
高效的JSON转换器 -
智能的配置管理器 -
完善的错误处理
-
-
最佳实践: -
使用JSON缓存加速加载 -
按需加载大型配置表 -
分离开发和生产配置 -
自动化配置验证
-
-
应用场景: -
游戏数值配置 -
多语言文本管理 -
关卡设计数据 -
热更新内容
-
-
提高策划和程序协作效率 -
简化游戏数据调整流程 -
增强配置数据的安全性和可维护性 -
为后续热更新和数据驱动开发奠定基础
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)