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
#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可以全部删除的-可以,但没必要)。
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()
{//testint 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
#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;//物品类型 stringvirtual 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中获取的字段不同。
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
记得使用超前引用。
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);//根据类型和名称创建GoodGood* 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;}elsem_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();//获取对应存档valueMapauto& 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为简单的工厂方法。
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 = 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);//是否存在对应的作物IDCropStruct* 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
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
