最新文章
Cocos2d-x游戏开发实例详解7:对象释放时机
03-25 13:59
Cocos2d-x游戏开发实例详解6:自动释放池
03-25 13:55
Cocos2d-x游戏开发实例详解5:神奇的自动释放
03-25 13:49
Cocos2d-x游戏开发实例详解4:游戏主循环
03-25 13:44
Cocos2d-x游戏开发实例详解3:无限滚动地图
03-25 13:37
Cocos2d-x游戏开发实例详解2:开始菜单续
03-25 13:32
Cocos2d-x数据模块教程05:CSV文件解析
在游戏开发中,通常会涉及大量的怪物、关卡、技能等数据信息。为了提高代码的灵活性和可维护性,这些数据一般不会直接硬编码在代码里,而是使用配置文件进行保存,在需要使用时再加载到内存中。
CSV(逗号分隔值)文件由于编辑简单,常被用于配置游戏的数据信息。本篇文章将详细介绍如何读取CSV文件格式的数据。
CSVParser解析器下载
下载地址:https://github.com/shahdza/Cocos_LearningTest/tree/master/CSVParser
CSV简介
什么是CSV?
CSV,即逗号分隔值(Comma-Separated Values),有时也称为字符分隔值,因为分隔字符不一定是逗号,也可以是分号(;)。其文件以纯文本形式存储表格数据(包括数字和文本)。
这种文件格式常用于不同程序之间的数据交互。CSV格式数据的结构类似表格,不同的记录占用一行,一行中的字段用逗号(,)分隔。
例如:
Name,Age,City
John,25,New York
Jane,30,Los Angeles
编辑CSV文件
- Mac OS系统:可以使用Numbers创建表格文件,然后保存为CSV格式。
- 使用Numbers软件编辑表格数据。
- 将编辑好的表格保存为CSV格式文件。
- 打开导出的CSV文件,会发现每条数据占一行,且每一行的数据用逗号分割。
- Windows系统:可以使用Excel编辑CSV文件,当然也可以使用其他CSV编辑软件。
CSV格式规则
- 开头不留空:以行为单位。
- 记录分隔:每条记录占一行,以逗号为分隔符。即使列为空,也要表达其存在。
- 列名规则:可含或不含列名,如果包含列名,则列名位于文件第一行。
- 数据完整性:一行数据不跨行,无空行。
- 特殊字符处理 - 逗号:字段中包含逗号时,该字段必须用双引号括起来。
- 特殊字符处理 - 换行符:字段中包含换行符时,该字段必须用双引号括起来。
- 特殊字符处理 - 空格:字段前后包含空格时,该字段必须用双引号括起来(例如:
a b c应表示为"a b c")。 - 特殊字符处理 - 双引号:字段中的双引号,用两个双引号表示(例如:
我说:"abc"应表示为我说:""abc"")。 - 特殊字符处理 - 含双引号字段:字段中如果有双引号,该字段必须用双引号括起来(例如:
我说:"abc"应表示为"我说:""abc""。")。
注意:中文的逗号、双引号不需要用双引号包起来。
以下是一个符合CSV格式规则的例子:
"Name","Age","City"
"John","25","New York"
"Jane","30","Los Angeles"
为什么使用CSV?
- 空间占用小:CSV文件格式是文本文件,占用空间比较小。
- 编辑方便:可以用记事本打开,编辑修改方便,同时也可以用Excel打开。
- 与策划工作适配:在游戏项目中,策划通常喜欢用Excel做数值和配置,而Excel可以直接另存为CSV文件。
- 相比XML的优势:配置也可以使用XML,Excel同样可以导出XML文件格式。但C++的标准库没有XML的读取方法,通常C++项目读取XML需要依赖第三方库,如TinyXml之类的。
CSV解析
这里介绍C++版本的CSV解析。需要注意的是,CSV文件的编码格式在手游中一般使用UTF - 8编码格式。
另外,笨木头写过Lua版读取CSV数据,可参见《Cocos2d-x Lua 读取Csv文件,更方便的使用数据》。
以下的CSV解析代码是在上述文章的基础上进行修改的,使用C/C++标准库编写,不依赖Cocos2d - x里面的任何一个类或函数。这样做的好处是增强了CSV解析的通用性,即使在编写控制台应用程序时也可以正常使用。
CSVParser.h
#pragma once
#include <vector>
#include <string>
using namespace std;
namespace CSVParser {
// 每一行的记录
class Row {
public:
Row() {}
~Row() {}
void push_back(const string& value) { m_values.push_back(value); }
void setHeader(const vector<string>* header) { m_header = header; }
// 每行数据有多少字段
unsigned int size() const { return m_values.size(); }
// 运算符 [] 重载
string& operator[](unsigned int key) {
if (key < size()) return m_values[key];
throw "can't return this value (doesn't exist)";
}
// 运算符 [] 重载
string& operator[](const string& key) {
vector<string>::const_iterator it;
int pos = 0;
for (it = (*m_header).begin(); it != (*m_header).end(); it++) {
if (key == *it) return m_values[pos];
pos++;
}
throw "can't return this value (doesn't exist)";
}
private:
const vector<string>* m_header;
vector<string> m_values;
};
class Csv {
public:
Csv(const string& filename);
~Csv();
// 解析csv文件
void Parse(const string& filename);
// 错误信息
const string& getErrorInfo() const { return m_strErrorInfo; }
// 获取列头字段
vector<string> getHeader() const { return m_header; }
// 获取总行数
unsigned int getRowCount() const { return m_content.size(); }
// 获取总列数
unsigned int getColumnCount() const { return m_header.size(); }
// 运算符 [] 重载
Row& operator[](unsigned int key);
private:
// 读取整个文件的数据
void Load(const string& filename, string& Data);
// 设置列头字段,用于[]运算符,以键值对方式获取数据值
void setHeader();
private:
// 原始表格数据
vector<Row> m_content; // 所有行的数据(包含列头)
vector<string> m_header; // 列头字段
// 错误信息
string m_strErrorInfo;
};
}
代码说明:
- 命名空间:使用
CSVParser命名空间。 - Row类:表示一行的数据记录,重载了
[]运算符,可以通过“键值对”方式获取数据值。 - Csv类:用于解析CSV文件,也重载了
[]运算符,可以像数组一样获取数据值。
CSVParser.cpp
#include "CSVParser.h"
namespace CSVParser {
Csv::Csv(const string& filename) {
Parse(filename);
}
Csv::~Csv() {}
void Csv::Load(const string& filename, string& Data) {
// 读取文件数据
FILE* pFile = fopen(filename.c_str(), "rb");
if (!pFile) {
return;
}
fseek(pFile, 0, SEEK_END);
long len = ftell(pFile);
char* pBuffer = new char[len + 1];
fseek(pFile, 0, SEEK_SET);
fread(pBuffer, 1, len, pFile);
fclose(pFile);
pBuffer[len] = 0;
Data.assign(pBuffer, len);
delete[] pBuffer;
}
void Csv::Parse(const string& filename) {
// 清除之前的数据
m_content.clear();
m_strErrorInfo.clear();
string text;
Load(filename, text);
if (text.size() == 0) {
return;
}
// 定义状态
enum StateType {
NewFieldStart, // 新字段开始
NonQuotesField, // 非引号字段
QuotesField, // 引号字段
FieldSeparator, // 字段分隔
QuoteInQuotesField, // 引号字段中的引号
RowSeparator, // 行分隔符字符1,回车
Error, // 语法错误
};
Row Fields = Row();
string strField;
// 设置初始状态
StateType State = NewFieldStart;
for (int i = 0, size = text.size(); i < size; ++i) {
const char& ch = text[i];
switch (State) {
case NewFieldStart: { // 新字段开始
if (ch == '"') {
State = QuotesField;
} else if (ch == ',') {
Fields.push_back("");
State = FieldSeparator;
} else if (ch == '\r' || ch == '\n') {
m_strErrorInfo = "语法错误:有空行";
State = Error;
} else {
strField.push_back(ch);
State = NonQuotesField;
}
}
break;
case NonQuotesField: { // 非引号字段
if (ch == ',') {
Fields.push_back(strField);
strField.clear();
State = FieldSeparator;
} else if (ch == '\r') {
Fields.push_back(strField);
State = RowSeparator;
} else {
strField.push_back(ch);
}
}
break;
case QuotesField: { // 引号字段
if (ch == '"') {
State = QuoteInQuotesField;
} else {
strField.push_back(ch);
}
}
break;
case FieldSeparator: { // 字段分隔
if (ch == ',') {
Fields.push_back("");
} else if (ch == '"') {
strField.clear();
State = QuotesField;
} else if (ch == '\r') {
Fields.push_back("");
State = RowSeparator;
} else {
strField.push_back(ch);
State = NonQuotesField;
}
}
break;
case QuoteInQuotesField: { // 引号字段中的引号
if (ch == ',') {
// 引号字段闭合
Fields.push_back(strField);
strField.clear();
State = FieldSeparator;
} else if (ch == '\r') {
// 引号字段闭合
Fields.push_back(strField);
State = RowSeparator;
} else if (ch == '"') {
// 转义
strField.push_back(ch);
State = QuotesField;
} else {
m_strErrorInfo = "语法错误: 转义字符 \" 不能完成转义 或 引号字段结尾引号没有紧贴字段分隔符";
State = Error;
}
}
break;
case RowSeparator: { // 行分隔符字符1,回车
if (ch == '\n') {
m_content.push_back(Fields);
Fields = Row(); // Fields.clear();
strField.clear();
State = NewFieldStart;
} else {
m_strErrorInfo = "语法错误: 行分隔用了回车 \\r。但未使用回车换行 \\r\\n ";
State = Error;
}
}
break;
case Error: { // 语法错误
return;
}
break;
default: break;
}
}
// end for
switch (State) {
case NewFieldStart: {
// Excel导出的CSV每行都以/r/n结尾。包括最后一行
}
break;
case NonQuotesField: {
Fields.push_back(strField);
m_content.push_back(Fields);
}
break;
case QuotesField: {
m_strErrorInfo = "语法错误: 引号字段未闭合";
}
break;
case FieldSeparator: {
Fields.push_back("");
m_content.push_back(Fields);
}
break;
case QuoteInQuotesField: {
Fields.push_back(strField);
m_content.push_back(Fields);
}
break;
case RowSeparator: {
}
break;
case Error: {
}
break;
default: break;
}
setHeader();
}
void Csv::setHeader() {
m_header.clear();
for (int i = 0; i < m_content[0].size(); i++) {
m_header.push_back(m_content[0][i]);
}
for (int i = 0; i < m_content.size(); i++) {
m_content[i].setHeader(&m_header);
}
}
Row& Csv::operator[](unsigned int key) {
if (key < m_content.size()) return m_content[key];
throw "can't return this row (doesn't exist)";
}
}
代码说明:
- Load方法:用于读取整个CSV文件的数据。
- Parse方法:解析CSV文件,通过状态机的方式处理不同的字符,确保正确解析CSV文件的格式。
- setHeader方法:设置列头字段,以便通过“键值对”方式获取数据。
使用方法
1. csv文件数据
在Mac OS系统上,可以使用Numbers编辑表格,并导出为CSV格式。
2. 解析csv文件,获取数据
#include <stdio.h>
#include <iostream>
using namespace std;
//[1] 引入头文件、命名空间
#include "CSVParser.h"
using namespace CSVParser;
int main() {
//[2] csv文件完整路径
string path = "/soft/cocos2d-x-3.4/projects/Demo34/Resources/testCSV.csv";
//[3] 解析csv文件
Csv csv = Csv(path.c_str());
//[4] 获取总行数(包含列头)、总列数
printf("总共有 %d 行\n", csv.getRowCount());
printf("总共有 %d 列\n", csv.getColumnCount());
//[5] 获取所有数据(第0行为列头字段)
// csv.getRowCount() : 数据总行数(包含列头)
for (int i = 0; i < csv.getRowCount(); i++) {
// csv[i].size() : 每条数据有多少字段
for (int j = 0; j < csv[i].size(); j++) {
printf("%s,", csv[i][j].c_str());
}
puts("");
}
//[6] 也可以根据列头名称,获取数据
printf("%s\n", csv[2]["备注"].c_str());
//[7] 获取某一行数据
Row row = csv[4];
printf("%s\n", row["姓名"].c_str());
return 0;
}
代码说明:
- 引入头文件和命名空间:包含
CSVParser.h头文件,并使用CSVParser命名空间。 - 指定文件路径:设置CSV文件的完整路径。
- 解析文件:创建
Csv对象并传入文件路径进行解析。 - 获取基本信息:获取CSV文件的总行数和总列数。
- 遍历数据:通过两层循环遍历CSV文件的所有数据。
- 根据列头获取数据:可以根据列头名称获取特定的数据。
- 获取某一行数据:可以获取指定行的数据。
3. 运行结果
运行上述代码后,会输出CSV文件的总行数、总列数以及所有数据,同时可以根据列头名称获取特定的数据。