我还记得今年年初的项目中,我们对数据的读取都是从execl导出为json文件,通过解析json文件信息,来获取游戏的配置信息。这样做简单但是当我们插入或者是删除其中的某个数据项的时候,修改起来比较的麻烦而且容易出现错误,而且不容易查错。而后在网上看着别人分享使用sqlite数据库的方案,就果断去尝试。
对于sqlite数据库本身我就不多说了,这里主要是总结在游戏中使用sqlite数据步奏和方法。首先需要下载sqlite数据的界面可视化工具Navicat Premium,有了可视化工具,我们新建表和其中的表项就变得十分的容易和简单了。使用的界面和mysql数据可视化界面基本上差不多。更多情况下我们都需要对数据库进行加密,可以参考http://www.cocoachina.com/bbs/read.php?tid=199953
首先是打开数据库的操作:
void DataCtrl::openDB()
{
if(FileOperation::isFileExistInWRDir("data/test.sqlite") == false) //验证游戏的可读写区域是否存在该数据库
{
FileOperation::copyData("test.sqlite","data"); //从本地拷贝到可读写区域
}
sqlite3 * db;
std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK) //open database success
{
cocos2d::log("open database success!");
}
else
{
cocos2d::log("open database failed!");
}
}
我们本地新建一个test.sqlite放在Resources的data目录下,游戏首次运行时,数据库是不在游戏的可读取区域的,我们需要从本地拷贝进去。
bool FileOperation::isFileExistInWRDir(const char* fileName)
{
if( !fileName ) return false;
std::string filePath = FileUtils::getInstance()->getWritablePath();
filePath += fileName;
FILE *fp = fopen(filePath.c_str(),"r");
if(fp)
{
fclose(fp);
return true;
}
return false;
}
void FileOperation::copyData(const char* fileName, const char* dirName)
{
std::string symbol = "/";
std::string strPath = FileUtils::getInstance()->fullPathForFilename(dirName + symbol + fileName);
ssize_t len = 0;
unsigned char *data = NULL;
data = FileUtils::getInstance()->getFileData(strPath.c_str(),"r",&len);
std::string destPath = FileUtils::getInstance()->getWritablePath();
//目录不存在,则创建
if(FileUtils::getInstance()->isDirectoryExist(destPath + dirName) == false)
{
FileUtils::getInstance()->createDirectory(destPath + dirName);
}
destPath += dirName + symbol + fileName;
FILE *fp = fopen(destPath.c_str(),"w+");
if(fp != nullptr)
{
fwrite(data,sizeof(char),len,fp);
fclose(fp);
delete []data;
data = NULL;
CCLOG("Copy %s to WRDir Compeleted!", fileName);
}
else
{
if(data)
delete []data;
CCLOG("Copy %s to WRDir Failed!", fileName);
}
}
这是我们自己封装的文件操作类的两个方法。
好了数据库到是打开了,我们现在来验证数据的加密与解密。首先是加密
void HelloWorld::encryptDB()
{
std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
sqlite3 *db;
char pwd[100] = dbPassWord;//密码的宏定义
if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK)
{
printf("1.打开未加密数据库成功,正在加密……\n");
sqlite3_rekey(db,pwd,static_cast<int>(strlen(pwd)));
sqlite3_close(db);
}
else
{
printf("打开未加密数据库失败\n");
}
if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK)
{
if(sqlite3_key(db,pwd,static_cast<int>(strlen(pwd))) == SQLITE_OK)
{
printf("2.验证加密,加密成功\n");
}
else
{
printf("加密失败\n");
}
}
sqlite3_close(db);
}
在执行以上代码后,我们会发现test.sqlite已经不能打开了,说明加密成功了!对于加密的具体过程,可以去追究sqlite的源码(注意此时我们都是对可读写区域的数据库进行的操作)
然后进行数据库的解密测试:
void HelloWorld::decodingDB()
{
std::string dbPath = FileUtils::getInstance()->getWritablePath() + "data/test.sqlite";
cocos2d::log("%s",FileUtils::getInstance()->getWritablePath().c_str());
sqlite3 *db;
char pwd[100] = dbPassWord;
if (sqlite3_open(dbPath.c_str() , &db) == SQLITE_OK) //首先用密码打开数据库之后,然后再去除密码
{
if(sqlite3_key(db,pwd,static_cast<int>(strlen(pwd))) == SQLITE_OK)
{
printf("2.验证加密,加密成功\n");
if(sqlite3_rekey(db,NULL,0) == SQLITE_OK)
{
printf("3.解密数据库成功……\n");
sqlite3_close(db);
}
}
else
{
printf("加密失败\n");
}
}
}
刚开始使用出现错误,原来是加密之后的数据库必须使用密钥才能打开,这样就导致在代码中能看到我们的密码。如果要隐藏密码可以采取线上获取,或者是本地数据与线上的数据库来进行比对,如果出现冲突,以线上数据库为准。
数据的加密肯定是在之前就做好处理的,所以我们还得将以上代码增加封装到工具当中去,在本地进行数据的加密和解密。因为当我们编辑数据的时候,数据库就不能加密,而游戏中使用的数据库需要加密。
当然此时我们把数据库操作类进行单独的封装
#include "SQLiteManage.hpp"
SQLiteManage * SQLiteManage::m_Instance = nullptr;
sqlite3 * SQLiteManage::m_SqliteDB = nullptr;
SQLiteManage::SQLiteManage()
{
}
SQLiteManage::~SQLiteManage()
{
if(m_SqliteDB)
{
sqlite3_close(m_SqliteDB);
m_SqliteDB = nullptr;
}
}
SQLiteManage * SQLiteManage::getInstance()
{
if(m_Instance == nullptr)
{
m_Instance = new SQLiteManage();
}
return m_Instance;
}
bool SQLiteManage::openDB(std::string dbName, std::string password)
{
if(m_SqliteDB != nullptr)
closeDB();
std::string dbPath = cocos2d::FileUtils::getInstance()->getWritablePath() + dbName;
int result = -1;
result = sqlite3_open(dbPath.c_str(), &m_SqliteDB);
if(result == SQLITE_OK)
{
if(password.length() != 0)
{
if(sqlite3_key(m_SqliteDB, password.c_str(), (int)password.length())!= SQLITE_OK)
{
CCLOG("open database failed! password error!");
return false;
}
}
return true;
}
else
{
CCLOG("open databse failed! error code = %d",result);
return false;
}
}
void SQLiteManage::closeDB()
{
if(m_SqliteDB != nullptr)
sqlite3_close(m_SqliteDB);
m_SqliteDB = nullptr;
}
bool SQLiteManage::execSQL(std::string strSql, int(*callback)(void *,int,char **,char**))
{
if(m_SqliteDB == nullptr)
{
CCLOG("please open the database at first!");
return false;
}
char *errorMsg;
int result = sqlite3_exec(m_SqliteDB, strSql.c_str(), callback, NULL, &errorMsg);
if(result != SQLITE_OK)
{
CCLOG( "exec a sql error, error code:%d ,error msg:%s", result, errorMsg);
}
return true;
}
其中callback回调函数必须输静态的函数,而且数据读取时一行则调用一次回调函数,所以我们需要在回调中把每行的数据放在早已定义好的数据结构当中去!而这个过程我们还可以进行优化,而且还需要增加一个数据管理类,对于游戏配置数据,不应该提供写的接口,玩家数据需要可读取的接口。当然了,这个类只给出了基本的数据库操作,可以根据实际情况增加我们需要的操作。