在前几节实现了Soil和SoilLayer,本节有两个任务,首先是实现CropLayer,之后是实现DynamicData。
无论是SoilLayer,还是CropLayer,其内部的代码相对较少,它们的作用类似于stl的vector,vector是把c/c++中的数组和对应的操作数组的方法结合起来;而SoilLayer和CropLayer亦是如此。作为容器,一般会有添加方法、删除方法、以及满足某种条件的元素等。
首先是CropLayer.h
#ifndef __CropLayer_H__
#define __CropLayer_H__
#include
#include
#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 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可以全部删除的-可以,但没必要)。
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);
}
}
上面的代码和上一节的测试代码大致相同,只不过作物对象的生成交给了作物层。
编译运行,其界面应该与上一节测试代码完全一致。
在实现DynamicData类之前,还需要实现GoodInterface、Good、Fruit和Seed这几个类,其继承关系大致如下:
GoodInterface为接口,主要用于GoodLayer层,而GoodLayer层的作用则是负责显示物品、选中物品和一些回调函数,如下:
此界面就是GoodLayer产生的界面,GoodLayer不用关心它显示的是什么物品和处理逻辑 ,即物品的填充和回调函数的处理都交给上层(在本游戏中是FarmScene)处理。每一个需要在GoodLayer中显示的物品都需要实现GoodInterface接口,其内容如下:
GoodInterface.h
#ifndef __GoodInterface_H__
#define __GoodInterface_H__
#include
#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中查看。
DynamicData类管理的就是存档,比如游戏在第一次运行时的默认存档(default_data.plist,保存在Resources/data/),以及之后的存档保存和读取。
在农场游戏中,主要保存的数据有:
而DynamicData类主要处理的就是以上的这些数据。
DynamicData.h
#ifndef __DynamicData_H__
#define __DynamicData_H__
#include
记得使用超前引用。
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 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& 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这三个函数与存档的结构有关。土壤的存档结构大致如下:
soils
harvest_count
1
crop_rate
0
crop_start
1543970457
crop_id
104
soil_id
12
soil_lv
1
土壤是一个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为简单的工厂方法。
有了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 = 1;
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/tree/Farm-04