文章目录
  1. 1. CCUserDefault
  2. 2. 格式化存储
  3. 3. 本地文件存储
  4. 4. XML和JSON
  5. 5. 加密和解密
  6. 6. SQLite

下面介绍几种数据持久化的方法

CCUserDefault

CCUserDefault是Cocos2d-x引擎提供的持久化方案,其作用是存储所有游戏通用的用户配置信息,例如音乐和音效配置等。CCUserDefault可以看做一个永久存储的字典,本质是一个XML文件,将每个键及其对应的值以节点的形式存储到外存中。值只支持int和float等基本类型。使用接口非常简单,只需要一行代码:

CCUserDefault::sharedUserDefault()->setIntegerForKey("coin", coin - 1);

由于每次设置和读取都会遍历整棵XML树,效率不高,且值类型具有局限性,因此CCUserDefault只适合小规模使用,对于复杂的持久化场景就会显得很无力。

格式化存储

对于稍微复杂的持久化情景,还是可以借助CCUserDefault来满足我们的需求的。由于CCUserDefault是允许存储字符串值的,所以只要将需要保存的数据类型先转化为字符串,就可以写入外存中。

我们先将用户记录封装为类,由用户ID标识,在一个ID下存放金币、经验值和音乐3个值,这样游戏中就允许存在多个用户的记录了。我们创建了一个UserRecord类来读写用户记录,其定义如下:

class UserRecord : public CCObject
{
    CC_SYNTHESIZE_PASS_BY_REF(string, m_userID, UserID);
    CC_SYNTHESIZE_PASS_BY_REF(int, m_coin, Coin);
    CC_SYNTHESIZE_PASS_BY_REF(int, m_exp, Exp);
    CC_SYNTHESIZE_PASS_BY_REF(bool, m_isMusicOn, IsMusicOn);

public:
    UserRecord(const string& userID);
    void saveToCCUserDefault();
    void readFromCCUserDefault();
};

我们把需要存档的数据通过sprintf函数格式化成一个字符串,并把字符串保存到CCUserDefault之中。注意,这里我们做了一点小小的处理,存储的关键字除了用户ID之外,还添加了一个前缀”UserRecord”,这样可以保证,即使在存储时其他类型对象用了同样的用户ID,也可以被区分开。具体代码如下:

void UserRecord::saveToCCUserDefault()
{
    char buff[100];
    sprintf(buff, "%d %d %d",
    this->getCoin(),
    this->getExp(),
    this->getIsMusicOn() ? 1 : 0
);
const char* key = ("UserRecord." + this->getUserID()).c_str();
CCUserDefault::sharedUserDefault()->setStringForKey(key, buff);
}

有了写入存档的功能,我们还需要一个逆向的从存档读取的过程。读取过程与此过程刚好相反。我们从CCUserDefault来获取保存的字符串,再使用sscanf函数来得到每个数据的值,相关代码如下:

void UserRecord::readFromCCUserDefault()
{    
    string buff = CCUserDefault::sharedUserDef;    
    ault()->getStringForKey(("UserRecord." + this->getUserID()).c_str());

    int coin = 0;
    int experience = 0;
    int music = 0;

    sscanf(buff.c_str(), "%d %d %d", &coin, &experience, &music);
    this->setCoin(coin);
    this->setExp(experience);
    this->setIsMusicOn(music!=0);
}

这一写一读的过程可以称为序列化与反序列化,是立体的内存数据与一维的字符串间的相互转换。实际上,我们只完成了从数据到CCUserDefault的标准化存储间的转换,从标准化存储到实际存储在文件中的字符串间的转换是交由引擎封装完成的。

本地文件存储

如果将不同类别的数据(例如,NPC的状态和玩家完成的成就)存储到不同的文件中,既可以提高效率,也方便我们查找。

不同平台间的文件系统不尽相同,为了简化操作、方便开发,Cocos2d-x引擎为我们提供了CCFileUtil类,用于实现获取路径和读取内容等功能,其中两个最重要的接口如下:

static unsigned char* getFileData(const char* pszFileName,const char* pszMode, unsigned long * pSize);//装载文件内容
static std::string getWriteablePath(); //获得可读写路径

借助这两个接口,我们可以获得一个路径,然后对文件进行相应的读写。文件读写在实际开发中应用得比较直接,一般是批量集中写入和读出,在此不再赘述。对于稍微灵活的场景,尤其是需要在大量数据中随机读写一小部分的时候,直接的文件存储由于缺少寻址支持,会变得非常麻烦。我们可以借助XML和SQL这两种方式,来更好地解决这个问题。

XML和JSON

XML和JSON都是当下流行的数据存储格式,它们的共同特点就是数据明文,十分易于阅读。XML源自于SGML,是一种标记性数据描述语言,而JSON则是一种轻量级数据交换格式,比XML更为简洁。鉴于C++对XML的支持更为完善,Cocos2d-x选择了XML作为主要的文件存储格式。

XML文档的语法非常简洁。文档由节点组成,节点的定义是递归的,节点内可以是一个字符串,也可以是由一组包围的若干节点,其中tag可以是任意符合命名规则的标识符。这样的递归嵌套结构非常灵活,特别适合以键值对形式存储的数据,比如数组和字典等。对于游戏开发中的大部分情景,XML文档都可以游刃有余地处理它们。

随Cocos2d-x一起分发的还有一个处理XML的开源库LibXML2,它用纯C语言的接口封装了对XML的创建、寻址、读和写等操作,极大地方便了开发。这里我们可以仿照CCUserDefault的做法,将对象存储到指定的XML文件中。

和XML语言的规范相对应,LibXML2库同样十分简洁,只有两个核心的概念
xmlDocPtr 指向XML文档的指针 XML文档的创建、保存、文档基本信息存取、根节点存取等。
xmlNodePrt 指向XML文档中一个节点的指针 节点内容存取、子节点的增删改等。

在UserRecord类中,我们添加如下两个接口,分别负责将对象从XML文件中读出和写入:

void saveToXMLFile(const char* filename="default.xml");
void readFromXMLFile(const char* filename="default.xml");

在开始之前,我们可以进一步抽象出两个函数,完成对象和字符串间的序列化和反序列化,以便在XML的读写接口和CCUserDefault的读写接口间共享,相关代码如下:

void UserRecord::readFromString(const string& str)
{
    int coin = 0;    
    int experience = 0;
    int music = 0;

    sscanf(str.c_str(), "%d %d %d", &coin, &experience, &music);
    this->setCoin(coin);
    this->setExp(experience);
    this->setIsMusicOn(music != 0);
}
void UserRecord::writeToString(string& str)
{
    char buff[100] = "";
    sprintf(buff,"%d %d %d",
    this->getCoin(),
    this->getExp(),
    this->getIsMusicOn() ? 1 : 0
    );
    str = buff;
}

有了对字符的序列化和反序列化,实际上我们只需要关心如何正确地在XML文档中读写键值对。我们暂且将对象都写到文档的根节点下,不考虑存储数组等复合数据结构的情景,尽管这些情景在操作上是类似的。首先,我们在一个指定的文档的根节点下找到一个键值,如果根节点下不存在指定的键值,将根据参数指定来创建。相关代码略。

加密和解密

XML的一个很严重的问题是明文存储,存储在外部的数据一旦被截获,就将直接暴露在攻击者面前,小则篡改用户数据,大则泄露用户隐私信息。因此,对存储在文件中的信息加密不可忽视。

前面我们已经设计好了序列化和反序列化过程,只要在其中加入合适的加密和解密算法,即可保证我们的数据不会被轻易窃取。这里我们只使用一个简单的编码轮换来加密,相关代码如下:

void encode(string &str)
{
    for(int i = 0; i < str.length(); i++) {
        int ch = str[i];
        ch = 0xff & (((ch & (1 << 7)) >> 7) & (ch << 1));
        str[i] = ch;
    }
}
void decode(string &str)
{
    for(int i = 0; i < str.length(); i++) {
        int ch = str[i];
        ch = 0xff & (((ch & (1)) << 7) & (ch >> 1));
        str[i] = ch;
    }
}

SQLite

SQLite是移动设备上常用的一个嵌入式数据库,具有开源、轻量等特点,其源代码只有两个”.c”文件和两个”.h”文件,并且已经包括了充分的注释说明。相比MySQL或者SQL Server这样的专业级数据库,甚至是比起同样轻量级的Access,SQLite的部署都可谓非常简单,只要将这4个文件导入工程中即可,这使得编译之后的SQLite非常小。

SQLite将数据库的数据存储在磁盘的单一文件中,并通过简单的外部接口提供SQL支持。由于其设计之初即是针对小规模数据的操作,在查询优化、高并发读写等方面做了极简化的处理,可以保证不占用系统额外的资源,因此,在大多数的嵌入式开发中,会比专业数据库有更快速、高效的执行效率。

SQLite的核心接口函数只有一个,如下所示:

int sqlite3_exec(
    sqlite3*, //一个已打开的数据库
    const char *sql, //将要执行的SQL语句
    int (*callback)(void*, int, char**, char**), //回调函数
    void *, //回调函数的第一个参数(用于传递自定义数据)
    char **errmsg //出错时返回的错误信息
);
文章目录
  1. 1. CCUserDefault
  2. 2. 格式化存储
  3. 3. 本地文件存储
  4. 4. XML和JSON
  5. 5. 加密和解密
  6. 6. SQLite