SDL農場遊戲開發 5.作物層和動態資料
在前幾節實現了Soil和SoilLayer,本節有兩個任務,首先是實現CropLayer,之後是實現DynamicData。
無論是SoilLayer,還是CropLayer,其內部的程式碼相對較少,它們的作用類似於stl的vector,vector是把c/c++中的陣列和對應的運算元組的方法結合起來;而SoilLayer和CropLayer亦是如此。作為容器,一般會有新增方法、刪除方法、以及滿足某種條件的元素等。
1.CropLayer
首先是CropLayer.h
#ifndef __CropLayer_H__ #define __CropLayer_H__ #include <vector> #include <algorithm> #include "SDL_Engine/SDL_Engine.h" USING_NS_SDL; using namespace std; class Crop; class CropLayer : public Layer { public: static string CUSTOM_EVENT_STRING; private: vector<Crop*> m_cropVec; public: CropLayer(); ~CropLayer(); CREATE_FUNC(CropLayer); bool init(); void update(float dt); //新增作物 Crop* addCrop(int id, int start, int harvestCount, float rate); //刪除作物 void removeCrop(Crop* crop); }; #endif
CropLayer作為Crop的容器,其內部有著新增作物,刪除作物的方法。
CropLayer.cpp
#include "CropLayer.h"
#include "Crop.h"
#include "StaticData.h"
string CropLayer::CUSTOM_EVENT_STRING = "Crop Ripe";
當作物成熟時,作物的頭上會有一個成熟特效,CropLayer負責找到第一個成熟的作物併發送事件來通知特效層有作物成熟。
void CropLayer::update(float dt) { //僅僅通知成熟動畫一次 Crop* pCrop = nullptr; for (auto it = m_cropVec.begin(); it != m_cropVec.end(); it++) { auto crop = *it; //更新狀態 crop->update(dt); //如果有作物成熟 if (crop->isRipe() && pCrop == nullptr) { pCrop = crop; } } _eventDispatcher->dispatchCustomEvent(CUSTOM_EVENT_STRING, pCrop); }
作物類有一個update函式,它會對流逝時間進行計時。CropLayer中的update函式中會呼叫作物的update函式,找到第一個成熟的作物,併發送事件;需要注意的是,無論有沒有成熟的作物都會發送事件,這樣是為了及時更新成熟特效(顯示 or 隱藏)。
Crop* CropLayer::addCrop(int id, int start, int harvestTime, float rate) { Crop* crop = Crop::create(id, start, harvestTime, rate); this->addChild(crop); SDL_SAFE_RETAIN(crop); m_cropVec.push_back(crop); return crop; } void CropLayer::removeCrop(Crop* crop) { //從容器中刪除 auto it = find(m_cropVec.begin(), m_cropVec.end(), crop); if (it != m_cropVec.end()) { m_cropVec.erase(it); crop->removeFromParent(); SDL_SAFE_RELEASE(crop); } } CropLayer::~CropLayer() { for (auto it = m_cropVec.begin(); it != m_cropVec.end();) { auto crop = *it; SDL_SAFE_RELEASE(crop); it = m_cropVec.erase(it); } }
addCrop負責生成作物,並儲存起來;removeCrop則負責把作物從容器中移除出去,至於作物內部的土壤指標,則交給上層處理。(上面的retain和release可以全部刪除的-可以,但沒必要)。
2.FarmScene的改變與測試
CropLayer是FarmScene的一個成員,需要修改FarmScene。
首先在FarmScene.h新增:
class SoilLayer;
class CropLayer;
class FarmScene : public Scene
{
//...
private:
SoilLayer* m_pSoilLayer;
CropLayer* m_pCropLayer;
};
之後在FarmScene.cpp中初始化m_pCropLayer並使用它。
bool FarmScene::init()
{
///...
//建立土壤層
m_pSoilLayer = SoilLayer::create();
this->addChild(m_pSoilLayer);
//建立作物層
m_pCropLayer = CropLayer::create();
this->addChild(m_pCropLayer);
//初始化土壤和作物
this->initializeSoilsAndCrops();
//...
return true;
}
void FarmScene::initializeSoilsAndCrops()
{
//test
int soilIDs[] = {12, 13, 14, 15, 16, 17};
auto currTime = time(NULL);
for (int i = 0; i < 6; i++)
{
auto soil = m_pSoilLayer->addSoil(soilIDs[i], 1);
int id = 101 + i;
auto startTime = currTime - i * 3600;
int harvestCount = 0;
float rate = 0.f;
auto crop = m_pCropLayer->addCrop(id, startTime, harvestCount, rate);
crop->setPosition(soil->getPosition());
crop->setSoil(soil);
soil->setCrop(crop);
}
}
上面的程式碼和上一節的測試程式碼大致相同,只不過作物物件的生成交給了作物層。
編譯執行,其介面應該與上一節測試程式碼完全一致。
3.GoodInterface、Good、Fruit、Seed類
在實現DynamicData類之前,還需要實現GoodInterface、Good、Fruit和Seed這幾個類,其繼承關係大致如下:
GoodInterface為介面,主要用於GoodLayer層,而GoodLayer層的作用則是負責顯示物品、選中物品和一些回撥函式,如下:
此介面就是GoodLayer產生的介面,GoodLayer不用關心它顯示的是什麼物品和處理邏輯 ,即物品的填充和回撥函式的處理都交給上層(在本遊戲中是FarmScene)處理。每一個需要在GoodLayer中顯示的物品都需要實現GoodInterface介面,其內容如下:
GoodInterface.h
#ifndef __GoodInterface_H__
#define __GoodInterface_H__
#include <string>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
USING_NS_SDL;
/**
* GoodLayer所需要的抽象類
*/
class GoodInterface
{
public:
/*獲取icon*/
virtual SpriteFrame* getIcon() const = 0;
//物品名稱
virtual string getName() const = 0;
//物品個數
virtual int getNumber() const = 0;
//物品價格
virtual int getCost() const = 0;
//物品描述
virtual string getDescription() const = 0;
//物品型別 string
virtual string getType() const = 0;
};
#endif
各個函式對應著顯示的資訊(getType當前未在GoodLayer使用)。
當前並不編寫GoodLayer的具體實現。
#include "GoodInterface.h"
USING_NS_SDL;
using namespace std;
//物品型別
enum class GoodType
{
Seed,//種子
Fruit,//作物 果實
};
class Good : public Object, public GoodInterface
{
public:
/*
* 獲取物品名 如 101 或Stick
*/
virtual string getGoodName() const = 0;
//設定數目
virtual void setNumber(int number) = 0;
//執行函式
virtual void execute(int userID, int targetID) = 0;
//是否是消耗品
virtual bool isDeleption() const = 0;
//獲取物品型別
virtual GoodType getGoodType() const = 0;
//獲取型別對應字串
static string toString(GoodType type)
{
if (type == GoodType::Seed)
return "Seed";
else if (type == GoodType::Fruit)
return "Fruit";
return "";
}
static GoodType toType(const string& str)
{
auto type = GoodType::Seed;
if (str == "Seed")
type = GoodType::Seed;
else if (str == "Fruit")
type = GoodType::Fruit;
return type;
}
};
如果說GoodLayer和GoodInterface繫結的話,那麼 Good抽象類則是在DynamicData類中所需要的資料型別。execute等幾個函式作為擴充套件介面,目前暫時用不到。
另外,cocos2dx在2.x中的基類為Object,而3.x時把Object更名為Ref(應該是Reference的簡寫),命名倒是貼切。
之後則是Seed和Fruit類的實現了,Seed和Fruit的不同其一在於GoodType型別不同,還有就在於它們從StaticData中獲取的欄位不同。
#ifndef __Seed_H__
#define __Seed_H__
#include "Good.h"
class Seed : public Good
{
private:
//種子ID 同作物ID
int m_nID;
int m_nNumber;
public:
Seed();
virtual ~Seed();
static Seed* create(int id, int number);
bool init(int id, int number);
virtual string getGoodName() const;
virtual SpriteFrame* getIcon() const;
virtual string getName() const;
virtual int getNumber() const;
virtual int getCost() const;
virtual string getDescription() const;
virtual string getType() const;
virtual void setNumber(int number);
//執行函式
virtual void execute(int userID, int targetID);
//是否是消耗品
virtual bool isDeleption() const;
//獲取物品型別
virtual GoodType getGoodType() const;
};
#endif
Seed.cpp的部分實現
bool Seed::init(int id, int number)
{
m_nID = id;
m_nNumber = number;
return true;
}
string Seed::getGoodName() const
{
return StringUtils::toString(m_nID);
}
SpriteFrame* Seed::getIcon() const
{
auto fruit_format = STATIC_DATA_STRING("fruit_filename_format");
auto fruitName = StringUtils::format(fruit_format.c_str(), m_nID);
auto frameCache = Director::getInstance()->getSpriteFrameCache();
return frameCache->getSpriteFrameByName(fruitName);
}
string Seed::getName() const
{
auto cropSt = StaticData::getInstance()->getCropStructByID(m_nID);
auto type = this->getType();
string text = StringUtils::format("%s(%s)", cropSt->name.c_str(), type.c_str());
return text;
}
int Seed::getNumber() const
{
return m_nNumber;
}
int Seed::getCost() const
{
auto cropSt = StaticData::getInstance()->getCropStructByID(m_nID);
return cropSt->seedValue;
}
string Seed::getDescription() const
{
auto format = STATIC_DATA_STRING("seed_desc_format");
auto cropSt = StaticData::getInstance()->getCropStructByID(m_nID);
//先生成種子屬性
auto text = StringUtils::format(format.c_str(), cropSt->level, cropSt->exp
, cropSt->harvestCount, cropSt->number);
//新增描述
auto text2 = StringUtils::format("%s\n%s", text.c_str(), cropSt->desc.c_str());
return text2;
}
string Seed::getType() const
{
return STATIC_DATA_STRING("seed_text");
}
void Seed::setNumber(int number)
{
m_nNumber = number;
}
void Seed::execute(int userID, int targetID)
{
}
bool Seed::isDeleption() const
{
return false;
}
GoodType Seed::getGoodType() const
{
return GoodType::Seed;
}
Seed和Fruit中用到了StaticData類中的函式來獲取屬性,並且還用到了static_data.plist中的值,具體可以去Resources/data檢視。
Fruit類的實現類似Seed,詳情可在github中檢視。
4.DynamicData
DynamicData類管理的就是存檔,比如遊戲在第一次執行時的預設存檔(default_data.plist,儲存在Resources/data/),以及之後的存檔儲存和讀取。
在農場遊戲中,主要儲存的資料有:
- 土壤資訊和對應的作物資訊。
- 金錢。
- 等級和經驗。
- 揹包:種子和果實。
而DynamicData類主要處理的就是以上的這些資料。
DynamicData.h
#ifndef __DynamicData_H__
#define __DynamicData_H__
#include <map>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>
#include "SDL_Engine/SDL_Engine.h"
#include "Good.h"
using namespace std;
USING_NS_SDL;
class Crop;
class Soil;
class Good;
enum class GoodType;
//農場等級和農場經驗
#define FARM_LEVEL_KEY "farm_level"
#define FARM_EXP_KEY "farm_exp"
#define GOLD_KEY "gold"
記得使用超前引用。
class DynamicData : public Object
{
private:
static DynamicData* s_pInstance;
public:
static DynamicData* getInstance();
static void purge();
private:
DynamicData();
~DynamicData();
private:
//存檔
ValueMap m_valueMap;
//是否第一次進入遊戲
bool m_bFirstGame;
//存檔名稱
string m_filename;
//存檔索引
int m_nSaveDataIndex;
//揹包物品列表
vector<Good*> m_bagGoodList;
private:
bool init();
DynamicData負責讀取/儲存存檔,如果是第一次進入遊戲則讀取預設存檔;同時,為了可擴充套件性,還有一個存檔索引來標識不同的存檔。由FileUtils讀取存檔檔案並賦值給m_valueMap,在遊戲過程中,對動態資料改變的同時還應該修改m_valueMap中相應的值,此時快取的存檔並不會更改存檔檔案,只有在主動點選了存檔按鈕才會把m_value回寫到對應的存檔中。
public:
/* 讀取存檔
* @param idx 對應索引的存檔名稱
*/
bool initializeSaveData(int idx);
//儲存資料
bool save();
/**
* @param type 物品型別 為擴充套件作準備
* @param goodName 物品名 對於作物 種子來說為ID字串
* @param number 物品的添加個數
* @return 返回對應的Good
*/
Good* addGood(GoodType type, const string& goodName, int number);
/**
* 減少物品
* @param: goodName 物品名
* @param: number 減少個數
* return: 存在足夠的數目則返回true,否則返回false
*/
bool subGood(GoodType type, const string& goodName, int number);
/* 減少物品
* @param good 物品物件
* @param number 減少物品個數
* @return 減少成功返回true,否則返回false
*/
bool subGood(Good* good, int number);
vector<Good*>& getBagGoodList() { return m_bagGoodList; }
//--------------------------資料庫相關---------------------------
//獲取資料
Value* getValueOfKey(const string& key);
//設定資料
void setValueOfKey(const string& key, Value& value);
//移除資料
bool removeValueOfKey(const string& key);
一些常用函式。
//--------------------------農場相關---------------------------
//更新作物
void updateCrop(Crop* crop);
//更新土壤
void updateSoil(Soil* soil);
//剷除作物
void shovelCrop(Crop* crop);
//獲取對應等級需要的經驗
int getFarmExpByLv(int lv);
updateCrop更新的是作物存檔,作物存檔只有在收穫時才會被呼叫。
updateSoil一般用於擴建土地。
shovelCrop用於剷除土壤。
以上三個函式內部都是僅僅對m_valueMap的值進行了更改,至於作物當前的貼圖更改等則不在DynamicData的範圍之內。
private:
//更新物品存檔
void updateSaveData(ValueVector& array, Good* good);
//根據型別和名稱建立Good
Good* produceGood(GoodType type, const string& goodName, int number);
updateSaveData主要用於更新陣列型別的存檔,比如揹包物品。
produceGood是一個工廠方法(雖然只是根據型別產生對應的物件)。
之後則是DynamicData.cpp
#include "DynamicData.h"
#include "Soil.h"
#include "Crop.h"
#include "Seed.h"
#include "Fruit.h"
//--------------------------------------------DynamicData---------------------------------------
DynamicData* DynamicData::s_pInstance = nullptr;
DynamicData* DynamicData::getInstance()
{
if (s_pInstance == nullptr)
{
s_pInstance = new DynamicData();
s_pInstance->init();
}
return s_pInstance;
}
void DynamicData::purge()
{
SDL_SAFE_RELEASE_NULL(s_pInstance);
}
DynamicData::DynamicData()
:m_bFirstGame(true)
,m_nSaveDataIndex(0)
{
}
DynamicData::~DynamicData()
{
for (auto it = m_bagGoodList.begin(); it != m_bagGoodList.end();)
{
auto good = *it;
SDL_SAFE_RELEASE(good);
it = m_bagGoodList.erase(it);
}
}
DynamicData是一個單例類,應注意在合適的位置釋放記憶體。
bool DynamicData::initializeSaveData(int idx)
{
auto fileUtil = FileUtils::getInstance();
//獲取存檔路徑
string path = fileUtil->getWritablePath();
//對應的存檔完整路徑
string filepath = m_filename = StringUtils::format("%ssave%d.plist", path.c_str(), idx);
//不存在對應存檔,則使用預設存檔
if ( !fileUtil->isFileExist(m_filename))
{
filepath = "data/default_data.plist";m_bFirstGame = true;
}
else
m_bFirstGame = false;
m_nSaveDataIndex = idx;
//獲得對應存檔的鍵值對
m_valueMap = fileUtil->getValueMapFromFile(filepath);
//反序列化揹包物品
auto& goodList = m_valueMap.at("bag_good_list").asValueVector();
for (auto& value : goodList)
{
auto vec = StringUtils::split(value.asString(), " ");
string sType = vec[0].asString();
string goodName = vec[1].asString();
int number = vec[2].asInt();
//建立並新增
Good* good = this->produceGood(Good::toType(sType), goodName, number);
SDL_SAFE_RETAIN(good);
m_bagGoodList.push_back(good);
}
return true;
}
為了使得遊戲可移植,尤其是檔案操作,應該使用引擎所提供的函式進行操作,比如這裡就是通過getWritablePath來獲得存檔路徑,之後判斷是否存在存檔:若不存在,則使用預設存檔;存在則讀取該存檔。之後反序列化,生成物品列表。
Good* DynamicData::addGood(GoodType type, const string& goodName, int number)
{
Good* good = nullptr;
//是否存在該物品
auto it = find_if(m_bagGoodList.begin(), m_bagGoodList.end(), [&goodName, &type](Good* good)
{
return good->getGoodName() == goodName
&& good->getGoodType() == type;
});
//揹包中存在該物品
if (it != m_bagGoodList.end())
{
good = *it;
good->setNumber(good->getNumber() + number);
}//揹包中不存在該物品,建立
else
{
good = this->produceGood(type, goodName, number);
SDL_SAFE_RETAIN(good);
m_bagGoodList.push_back(good);
}
//新增成功,更新存檔資料
if (good != nullptr)
{
auto &goodList = m_valueMap["bag_good_list"].asValueVector();
this->updateSaveData(goodList, good);
}
return good;
}
addGood,顧名思義,就是新增物品,不存在對應的物品則先建立,然後更新m_valueMap。這個函式比較常用,比如購買種子、或者收穫時都會用到這個函式。
bool DynamicData::subGood(Good* good, int number)
{
bool ret = false;
auto goodNum = good->getNumber();
SDL_SAFE_RETAIN(good);
//個數足夠
if (goodNum > number)
{
good->setNumber(goodNum - number);
ret = true;
}
else if (goodNum == number)
{
good->setNumber(goodNum - number);
auto it = find_if(m_bagGoodList.begin(),m_bagGoodList.end(),[good](Good* g)
{
return good == g;
});
if (it != m_bagGoodList.end())
{
m_bagGoodList.erase(it);
SDL_SAFE_RELEASE(good);
ret = true;
}
}
//操作成功,才進行存檔更新
if (ret)
{
auto &goodList = m_valueMap["bag_good_list"].asValueVector();
this->updateSaveData(goodList, good);
}
SDL_SAFE_RELEASE(good);
return ret;
}
subGood和addGood相對應,表示減少對應的物品個數。當沒有足夠多的物品時,減少失敗;否則扣除個數並更新對應存檔。
Value* DynamicData::getValueOfKey(const string& key)
{
Value* value = nullptr;
//查詢
auto it = m_valueMap.find(key);
if (it != m_valueMap.end())
{
value = &(it->second);
}
return value;
}
void DynamicData::setValueOfKey(const string& key, Value& value)
{
auto it = m_valueMap.find(key);
if (it != m_valueMap.end())
{
it->second = value;
}
else//直接插入
{
m_valueMap.insert(make_pair(key, value));
}
}
bool DynamicData::removeValueOfKey(const string& key)
{
auto it = m_valueMap.find(key);
bool bRet = false;
if (it != m_valueMap.end())
{
m_valueMap.erase(it);
bRet = true;
}
return bRet;
}
類似於StaticData。
void DynamicData::updateCrop(Crop* crop)
{
//獲取作物相關資訊
int cropID = crop->getCropID();
int cropStart = crop->getStartTime();
int harvestCount = crop->getHarvestCount();
float cropRate = crop->getCropRate();
//獲取作物對應土壤
auto soil = crop->getSoil();
auto soilID = soil->getSoilID();
//獲取對應存檔valueMap
auto& soilArr = m_valueMap["soils"].asValueVector();
//找到對應的土壤,並更新
for (auto& value : soilArr)
{
auto& dict = value.asValueMap();
if (dict["soil_id"].asInt() == soilID)
{
dict["crop_start"] = Value(cropStart);
dict["harvest_count"] = Value(harvestCount);
dict["crop_rate"] = Value(cropRate);
dict["crop_id"] = Value(cropID);
break;
}
}
}
updateCrop、updateSoil和shovelCrop這三個函式與存檔的結構有關。土壤的存檔結構大致如下:
<key>soils</key>
<array>
<dict>
<key>harvest_count</key>
<integer>1</integer>
<key>crop_rate</key>
<real>0</real>
<key>crop_start</key>
<integer>1543970457</integer>
<key>crop_id</key>
<integer>104</integer>
<key>soil_id</key>
<integer>12</integer>
<key>soil_lv</key>
<integer>1</integer>
</dict>
</array>
土壤是一個dict列表,每一個dict至少有兩個鍵,soil_id和soil_lv,其他的crop_*為作物的引數。以上的三個函式功能類似,只不過更新的是不同的鍵,比如updateCrop更新的是crop_start和harvest_count;updateSoil則是在soils列表中建立一個新的dict;shovelCrop則是刪除與作物相關的鍵值對。
int DynamicData::getFarmExpByLv(int lv)
{
return lv * 200;
}
void DynamicData::updateSaveData(ValueVector& array, Good* good)
{
auto goodName = good->getGoodName();
auto number = good->getNumber();
auto sType = Good::toString(good->getGoodType());
ValueVector::iterator it;
//獲得對應的迭代器
for (it = array.begin();it != array.end(); it++)
{
auto str = it->asString();
//先按名稱尋找
auto index = str.find(goodName);
//判斷型別是否正確
if (index != string::npos && str.find(sType) != string::npos)
{
break;
}
}
//物品型別 物品ID 物品個數
string text = StringUtils::format("%s %s %d",sType.c_str(), goodName.c_str(), number);
//找到對應欄位,則進行覆蓋
if (it != array.end())
{
if (number > 0)
array[it - array.begin()] = Value(text);
else if (number == 0)
array.erase(it);
}
else if (number > 0)//物品個數大於0,在後面新增
{
array.push_back(Value(text));
}
}
updateSaveData函式對m_valueMap進行更新,它根據物品的名稱和型別找到對應的迭代器,之後進行更新。
Good* DynamicData::produceGood(GoodType type, const string& goodName, int number)
{
Good* good = nullptr;
switch (type)
{
case GoodType::Seed: good = Seed::create(atoi(goodName.c_str()), number); break;
case GoodType::Fruit: good = Fruit::create(atoi(goodName.c_str()), number); break;
default: LOG("not found the type %s\n", Good::toString(type).c_str());
}
return good;
}
produceGood為簡單的工廠方法。
5.FarmScene的更新
有了DynamicData後,就可以讀取存檔了。目前更新的還是FarmScene的initializeSoilsAndCrops():
void FarmScene::initializeSoilsAndCrops()
{
//讀取存檔
auto& farmValueVec = DynamicData::getInstance()->getValueOfKey("soils")->asValueVector();
for (auto& value : farmValueVec)
{
int soilID = 0;
int soilLv = 0;
int cropID = 0;
int startTime = 0;
int harvestCount = 0;
float rate = 0.f;
auto& valueMap = value.asValueMap();
for (auto it = valueMap.begin(); it != valueMap.end(); it++)
{
auto& name = it->first;
auto& value = it->second;
if (name == "soil_id")
soilID = value.asInt();
else if (name == "soil_lv")
soilLv = value.asInt();
else if (name == "crop_id")
cropID = value.asInt();
else if (name == "crop_start")
startTime = value.asInt();
else if (name == "harvest_count")
harvestCount = value.asInt();
else if (name == "crop_rate")
rate = value.asFloat();
}
//生成土壤物件
Soil* soil = m_pSoilLayer->addSoil(soilID, soilLv);
//是否存在對應的作物ID
CropStruct* pCropSt = StaticData::getInstance()->getCropStructByID(cropID);
if (pCropSt == nullptr)
continue;
Crop* crop = m_pCropLayer->addCrop(cropID, startTime, harvestCount, rate);
crop->setSoil(soil);
soil->setCrop(crop);
//設定位置
crop->setPosition(soil->getPosition());
}
}
現在的農場遊戲可以讀取預設的存檔(default_data.plist),然後創建出soil和crop。
編譯執行,本節的介面如下:
本節程式碼:https://github.com/sky94520/Farm/pull/new/Farm-04