Cocos2d-x:存储游戏数据

推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生

存储游戏数据

在大多数游戏中,可能都有物品需要在游戏间隙再次存储和重新读取。这可能是玩家信息、统计数据、排行榜、关卡进度等等。与往常一样,开发人员可以选择多种方式来存储此数据。每种方法都有优点和缺点。在本教程中,我们将探讨使用 和 存储游戏数据。UserDefaultSQLite

用户默认

UserDefault是简单的键/值对数据结构。它是一个全局单例,可以随时访问,就像导演一样。 始终存在,即使您从未调用它也是如此。它只是空的。首次添加键/值对时,将创建实例。UserDefault

访问用户默认

访问非常简单:UserDefault

cocos2d::UserDefault::getInstance()->someFunction();

但是,如果您计划多次访问实例,最好获取一次实例,然后使用它,而不是每次需要时都访问它。例:UserDefault

auto userdefaults = cocos2d::UserDefault::getInstance();

userdefaults->setStringForKey("message", "Hello");
userdefaults->setIntegerForKey("score", 10);

将值添加到用户默认

添加键/值对很容易:UserDefault

auto userdefaults = cocos2d::UserDefault::getInstance();

userdefaults->setStringForKey("message", "Hello");
userdefaults->setIntegerForKey("score", 10);
userdefaults->setFloatForKey("some_float", 2.3f);
userdefaults->setDoubleForKey("some_double", 2.4);
userdefaults->setBoolForKey("some_bool", true);

更改用户默认值中的值

可能需要更改 中的键/值对。也许您正在存储球员分数,需要更新。这是通过简单地第二次设置值来实现的。例:UserDefault

auto userdefaults = cocos2d::UserDefault::getInstance();

userdefaults->setStringForKey("message", "Hello");
userdefaults->setStringForKey("message", "Hello Again");

删除值到用户默认

从中删除键/值对也很容易:UserDefault

auto userdefaults = cocos2d::UserDefault::getInstance();

userdefaults->deleteValueForKey("message");
userdefaults->deleteValueForKey("score");
userdefaults->deleteValueForKey("some_float");
userdefaults->deleteValueForKey("some_double");
userdefaults->deleteValueForKey("some_bool");

重置用户默认值

如果您希望完全清除并从头开始,您可以简单地调用:UserDefault

cocos2d::UserDefault::getInstance()->flush();

将用户默认值分配给标签

您可能希望使用存储在中的值,并将它们分配给对象以供玩家查看。只需几行代码即可实现此目的。例:UserDefaultLabel

char strTemp[256] = "";

std::string ret = UserDefault::getInstance()->getStringForKey("message");
sprintf(strTemp, "string is %s", ret.c_str());
some_label->setString(strTemp);

SQLite

如果您的需求比简单的键/值对更高级,则可以使用数据库来存储和操作游戏数据进行评估。SQLite是一个非常流行和常用的关系数据库。您可以在SQLite网站上阅读有关SQLite的更多信息。

设置 SQLite

下载适合您需求的 SQLite 捆绑包。有源代码预编译的二进制版本。如果你使用源代码版本,你可以简单地将sqlite3.h和sqlite3.c放到你的源代码树中,并使用include来引入SQLite。如果使用预编译的二进制文件,则需要将其添加为库搜索路径的一部分。

创建数据库

有几种方法可以创建新的 SQLite 数据库。

传送默认数据库

如果您下载SQLite CLI,则可以使用命令行与SQLite及其所有功能进行交互。如果选择此方法,则需要随游戏一起提供数据库,因为您不是在代码中创建数据库。此方法允许您预先使用较少的 SQL 代码,从而减少编码。但是,当您的游戏需要与数据库交互时,您仍然需要使用 SQL 代码

以编程方式创建数据库

如果您不希望发布默认/预填充的数据库,您可以随时在游戏首次启动时创建一个新数据库,然后在每次后续启动时检查数据库是否存在。这种方法意味着更多的代码。我们将在下一节中介绍这种方法。

以编程方式使用 SQLite

与任何数据库(而不仅仅是 SQlite)交互都需要几个工作部分。让我们来看看它们是什么:

与数据库的连接:您可以在游戏运行时保持持久数据库连接,也可以根据需要打开和关闭连接。如果维护持久数据库连接,则在 dataase 连接丢失时,您将需要处理这些类型的错误并重新连接(这在 SQLite 中不太可能)。如果您根据需要打开和关闭,则始终具有连接,但如果游戏的多个部分同时执行此操作,则可能会遇到锁定问题或数据库损坏。

要使用的数据库:随游戏一起发布一个数据库,或者在首次发布时以编程方式创建一个数据库。无论哪种情况,您都需要有一个数据库才能使用。

使用默认值填充数据库:随游戏一起提供预填充的数据库,或在首次启动时创建默认值。

从数据库中检查值:从数据库中读取数据以在游戏中做出决策。

随着值的变化而更新数据库:随着值的变化,存储它们以供未来使用(根据您的游戏要求)

在不使用数据库连接时关闭数据库连接:这应该通过持久连接和根据需要打开连接来完成。未能关闭连接可能会导致数据丢失。

使用经理类?

使用单例作为管理器类可能是一个不错的选择。这将所有SQL功能封装到一个地方,从而可以轻松访问功能而不会造成很多混乱。单例模式有助于提供类的单个(或全局)实例。您可以阅读有关单例模式的更多信息。

一个简单的单例可能看起来像这样:

C++ 标头

#ifndef  _SQLMANAGER_H_
#define  _SQLMANAGER_H_

#include <iostream>
#include <string>

class sqlite3;

class SQLManager
{
    public:
        static SQLManager* Instance();
        virtual ~SQLManager() {}

        void initInstance();
        bool connect();
        bool isDatabasePopulated();
        bool createDatabaseContents();
        bool createMainTable();

        inline bool getIsDatabaseReady() { return _bDatabaseReady; };
        inline sqlite3*& getDatabase() { return _pdb; };

        static int executeSelectQueryReturnSingleInt(const std::string& _sql);
        int getKeyByID(const std::string& _key, const std::string& _value);

        static std::string getSQLToCheckCounts(const std::string& _tableName);

        static void updateKey(const std::string& _key, const int& _value);

    private:
        SQLManager();
        SQLManager(const SQLManager&);
        SQLManager& operator= (const SQLManager&);
        static SQLManager* pinstance;

        bool _bDatabaseReady = false;
        sqlite3* _pdb;
        std::string _dbFile = "MyDatabase.db3";
        std::string _dbName = "MyDatabase";
        std::string _dbPath;
        int _dbVersion = 1; // increment this when database structure changes
};

#endif // _SQLMANAGER_H_

C++ 源代码


#include "SQLManager.hpp"
#include "sqlite3.h"
#include "cocos2d.h"

SQLManager* SQLManager::pinstance = 0;

SQLManager::SQLManager() {}

SQLManager* SQLManager::Instance()
{
    if (pinstance == 0)
    {
        pinstance = new SQLManager;
        pinstance->initInstance();
    }

    return pinstance;
}

void SQLManager::initInstance()
{
    // what do we need to do when this class is instantiated?
}

bool SQLManager::connect()
{
    // connecting to SQLite
}

bool SQLManager::isDatabasePopulated()
{
    // is our database already populated?
}

bool SQLManager::createDatabaseContents()
{
    // creating the database contents
}

bool SQLManager::createMainTable()
{
    // creating the main table, but you may need more tables...
}

int SQLManager::executeSelectQueryReturnSingleInt(const std::string& _sql)
{
    // returning an int as a result from a query
}

int SQLManager::getKeyByID(const std::string& _key, const std::string& _value)
{
    // obtaining a key
}

std::string SQLManager::getSQLToCheckCounts(const std::string& _tableName)
{
    // checking how many rows a table has
}

void SQLManager::updateKey(const std::string& _key, const int& _value)
{
    // when we need to update data
}

在本教程的其余部分中,将继续使用此单例类,并根据需要向其添加。 有关于为什么单例模式可能不好的争论

创建数据库

若要创建新数据库,最好查看现有数据库是否已存在。如果是这样,则表示您的游戏之前已在此设备上玩过,无需创建新游戏。让我们检查一个现有的数据库,如果没有,则使用 和 创建一个新数据库:SQLManager::initInstance()SQLManager::connect()SQLManager::isDatabasePopulated()

void SQLManager::initInstance()
{
    if (connect())
    {
        if (isDatabasePopulated())
        {
           // the database was found and is populated.
        }
        else
        {
            // a new database was created, so we need to pupulate it.
        }
    }
    else
    {
        // we failed to connect, handle this more gracefully for production!!!
        std::cout << "We cannot connect to database" << std::endl;
    }
}

bool SQLManager::connect()
{
    bool _bConnect = false;

    _pdb = NULL;

    _dbPath = cocos2d::FileUtils::getInstance()->getWritablePath() + _dbFile;

    int result = sqlite3_open(_dbPath.c_str(), &_pdb);

    if(result == SQLITE_OK)
    {
        //std::cout << "open database successfull!" << std::endl;
        _bConnect = true;
    }

    return _bConnect;
}

bool SQLManager::isDatabasePopulated()
{
    // check to see if the database is populated, this means it already existed. 
    // If it isn't populated we need to populate it with initial values    
}

上面的代码尝试在指定的路径上打开指定的数据库。如果文件已经存在,它会打开,ig 文件不存在,它仍然打开,但它将是空的。我们不知道是哪个!一种解决方案是检查是否存在数据库表,如果您的游戏已经玩过,该数据库表将存在。我们接下来将详细介绍这一点。在我们这样做之前,让我们确保我们也了解调用将确保我们返回的路径确实是此特定设备上的可写路径。由于操作系统以不同的方式处理此问题,并且具有不同的位置,因此进行此调用并使用其返回值非常重要。不建议尝试在设备供应商希望允许的范围之外创建自定义位置。cocos2d::FileUtils::getInstance()->getWritablePath()

填充数据库

如上所述,当尝试打开SQLite数据库时,如果它不存在,它将由SQLite代码创建。因此,一旦您连接到数据库,您就不知道数据库是否已经包含您的游戏数据,或者这是否是第一次玩游戏并因此包含数据。您需要在继续之前做出此决定,因为如果数据库没有您的游戏所需的数据,您的游戏会立即失败并出现错误,并且玩家可能永远不会再玩它,因为他们认为这是一个损坏或制作不佳的游戏。我们该如何做到这一点?还记得上面那个无法解释的电话吗?让我们在那里进行检查并返回 true;如果数据库有以前的数据并返回 false;如果没有。如果返回 false;则必须使用默认数据填充数据库。考虑以下代码,该代码用于检查是否存在名为 Master 的表:zeroSQLManager::isDatabasePopulated()

bool SQLManager::isDatabasePopulated()
{
    bool _bPopulated = false;

    sqlite3_stmt *statement;

    std::string _sql = "SELECT count(*) FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type='table' AND name='" +
    std::string("Master") +
    std::string("' ORDER BY name LIMIT 1;");

    //std::cout << "sql: " << _sql << std::endl;

    if(sqlite3_prepare_v2(_pdb, _sql.c_str(), -1, &statement, 0) == SQLITE_OK)
    {
        int cols = sqlite3_column_count(statement);
        int result = 0;

        while(true)
        {
            result = sqlite3_step(statement);

            if(result == SQLITE_ROW)
            {
                for(int col = 0; col < cols; col++)
                {
                    std::string s = (char*)sqlite3_column_text(statement, col);

                    //std::cout << "s: " << s << std::endl;

                    int result = std::stoi(s);

                    if(result == 1) // table exists, 0 if it doesn't exist
                    {
                        //std::cout << "We have AppMaster table" << std::endl;
                        _bPopulated = true;
                    }
                    else
                    {
                        std::cout << "We dont have APPMaster table" << std::endl;
                    }
                }
            }
            else
            {
                break;
            }
        }

        sqlite3_finalize(statement);
    }

    return _bPopulated;
}

如果存在一个名为 Master 的表,则将返回 true,然后我们知道我们有一个在游戏启动之前已填充的数据库。这意味着我们不需要填充它。我们可以继续知道我们的数据库已准备好读取/写入/更新数据。

如果名为 Master 的表不存在,则返回 false,这意味着我们需要在使用它之前填充我们的数据库。为此,我们需要创建包含行的表。这里很难具体,因为每个游戏都需要唯一的数据。但是,这个想法是使用 SQL CREATE TABLE 命令以及插入几行:

// a vector with what values to initially need in the Master table.
const std::vector<std::string> AppDBMasterTableID = {"HighScore", "HighTime", 
        "CurrentScore", "CurrentTime", "CurrentVersion"};

//std::cout << "Creating DB Master Table" << std::endl;

_sql = "create table " +
std::string("Master") +
std::string(" (_id TEXT PRIMARY KEY, _value INT);");

//std::cout << "sql: " << _sql << std::endl;

result = sqlite3_exec(_pdb, _sql.c_str(), NULL, NULL, NULL);

if(result == SQLITE_OK)
{
    char buffer[300];

    std::vector<int> AppDBMasterTableValue = {0, 0, 0, 0, 1.0};

    for (unsigned i = 0; i < AppDBMasterTableID.size(); i++)
    {
        _sql = "INSERT INTO " +
        std::string("Master") +
        std::string(" VALUES ('%s', '%i')");

        sprintf(buffer, _sql.c_str(),
                AppDBMasterTableID.at(i).c_str(),
                AppDBMasterTableValue.at(i));

        result = sqlite3_exec(_pdb, buffer, NULL, NULL, NULL);
    }
}

有了这个概念,下次游戏启动时,数据库将存在并具有所需的默认值。它不会在每次启动游戏时创建。任何更新或插入到数据库中的新数据都将在两次启动之间保留!

查询数据库中的数据

在游戏开始时启动数据库并进行快速完整性检查以确保它具有所需的最小 daya 后,您需要不时查询数据库以获取值以做出决策。这可能意味着检查玩家是否击败了以前的高分,或者玩家是否击败了特定的老板。使用 SQL SELLECT 语句查询数据库是一项简单而常见的操作。例:

SELECT value from Master WHERE id="high score";

SQLite在获取数据并循环访问数据方面非常灵活。考虑如下内容:

std::string _sql = "SELECT " +
std::string(value) +
std::string(" FROM ") +
std::string("Master") +
std::string(" WHERE _id='high score' LIMIT 1;");

sqlite3_stmt* statement;

if (sqlite3_prepare_v2(Instance()->getDatabase(), _sql, -1, &statement, 0) == SQLITE_OK)
{
    int cols = sqlite3_column_count(statement);
    int result = 0;

    while(true)
    {
        result = sqlite3_step(statement);

        if(result == SQLITE_ROW)
        {
            for(int col = 0; col < cols; col++)
            {
                std::string s = (char*)sqlite3_column_text(statement, col);

                //std::cout << "s: " << s << std::endl;

                return std::stoi(s);
            }
        }
        else
        {
            break;
        }
    }
}

return -1;

使用此代码,高分以字符串形式返回,以便可以在对象中使用。请注意,由于我们只是选择列,因此我们仅迭代 SQL 表中的该列。Label

更新数据库中的数据

更新的工作方式与选择数据相同,但 SQL 语句格式不同。将改用 SQL 更新语句。考虑:

std::string _sql = "UPDATE " +
std::string("Master") +
std::string(" SET _value") +
std::string("=") +
std::to_string(10) +
std::string(" WHERE _id='high score';");

//std::cout << "sql: " << _sql << std::endl;

int result = sqlite3_exec(Instance()->getDatabase(), _sql, NULL, NULL, NULL);

if (result == SQLITE_OK)
{
    std::cout << (std::string("update to Master: ") + std::string(_key.c_str()) + std::string(" successful")) << std::endl;

    // continue on....
}
else
{
    std::cout << (std::string("update to Master: ") + std::string(_key.c_str()) + std::string(" Failed")) << std::endl;

    // throw an error
}

在此示例中,将更新高分

将新数据插入数据库

插入数据也很容易,格式看起来与 SQL SELECTSQL UPDATE 语句一样熟悉。考虑:

std::string _sql = "INSERT INTO " +
std::string("Master") +
std::string(" VALUES ('15')");

关闭数据库连接

虽然关闭SQLite数据库并非完全必要,但最好谨慎行事并这样做。如果不这样做,数据库可能会损坏。考虑在代码中有意义的位置关闭数据库,可能在游戏退出时关闭。

sqlite3_close();

3D建模学习工作室 翻译整理,转载请注明出处!

上一篇:Cocos2d-x:creator_to_cocos2dx 插件 (mvrlink.com)

下一篇:Cocos2d-x:使用相机的基础知识 (mvrlink.com)

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号