首先,先创建一个Entity类。该类的内部有一个精灵对象及相关操作来专门负责显示,以后需要显示的类都可继承自Entity类。比如Crop类的父类就是Entity。
问:为什么Soil类不继承自Entity类呢?
答:Soil类其本身并不负责显示,它的内部精灵只是指向了TMXTiledMap对象中的精灵。
1.Entity
Entity.h
#ifndef __Entity_H__
#define __Entity_H__
#include<string>
#include "SDL_Engine/SDL_Engine.h"
using namespace SDL;
using namespace std;
class Entity:public Node
{
public:
Entity();
~Entity();
Sprite* getSprite() const;
//和bind不同,此函数不改变content size
void setSprite(Sprite* sprite);
void bindSprite(Sprite* sprite);
Sprite* bindSpriteWithSpriteFrame(SpriteFrame* spriteFrame);
Sprite* bindSpriteWithSpriteFrameName(const string& spriteName);
//以animation 的第一帧为贴图 并且运行该动画
Sprite* bindSpriteWithAnimate(Animate* animate);
void unbindSprite();
//创建动画
static Animate* createAnimate(const string& format, int begin, int end
, float delayPerUnit, unsigned int loops = -1);
public:
static const int ANIMATION_TAG;
static const int ACTION_TAG;
protected:
Sprite* m_pSprite;
};
#endif
Entity类内部使用了组合(Entity继承自Sprite类也是可以的),其内部封装了一些常用的显示方法。
Entity.cpp
#include "Entity.h"
const int Entity::ANIMATION_TAG = 100;
const int Entity::ACTION_TAG = 101;
Entity::Entity()
:m_pSprite(nullptr)
{
}
Entity::~Entity()
{
}
游戏中Action大致分为两类,动作和动画。比如一个角色类继承自Entity,它有一个行走方法:在发生位移的过程中,其贴图也会发生变化。那么该角色在行走中就至少有两个Action,其一为动作,它主要负责设置角色的位置;另一个则是动画,它仅仅会更改贴图(内部的m_pSprite)。使用组合会让这两类Action各司其职,便于管理。
void Entity::setSprite(Sprite* sprite)
{
if(m_pSprite)
m_pSprite->removeFromParent();
m_pSprite = sprite;
Size size = this->getContentSize();
m_pSprite->setPosition(size.width / 2, size.height / 2);
this->addChild(m_pSprite);
}
void Entity::bindSprite(Sprite* sprite)
{
if(m_pSprite)
m_pSprite->removeFromParent();
m_pSprite = sprite;
auto size = m_pSprite->getContentSize();
this->setContentSize(size);
m_pSprite->setPosition(size.width / 2, size.height / 2);
this->addChild(m_pSprite);
}
以上的两个方法功能类似,都是设置当前显示的精灵。最大的不同就是setSprite不会调用setContentSize()方法;而bindSprite()会调用该方法。
Sprite* Entity::bindSpriteWithSpriteFrame(SpriteFrame* spriteFrame)
{
if(spriteFrame != nullptr)
{
Sprite* sprite = Sprite::createWithSpriteFrame(spriteFrame);
Entity::bindSprite(sprite);
return sprite;
}
return nullptr;
}
Sprite* Entity::bindSpriteWithSpriteFrameName(const string& spriteName)
{
//获取精灵帧
auto frameCache = Director::getInstance()->getSpriteFrameCache();
auto spriteFrame = frameCache->getSpriteFrameByName(spriteName);
return this->bindSpriteWithSpriteFrame(spriteFrame);
}
Sprite*Entity::bindSpriteWithAnimate(Animate* animate)
{
auto animation = animate->getAnimation();
auto firstFrame = animation->getFrames().front()->getSpriteFrame();
auto sprite = this->bindSpriteWithSpriteFrame(firstFrame);
//运行动画
sprite->runAction(animate);
return sprite;
}
这几个方法是bindSprite的扩展方法,精灵来源虽然不同,但最后其内部都是调用了bindSprite函数。
void Entity::unbindSprite()
{
if (m_pSprite != nullptr)
{
m_pSprite->removeFromParent();
m_pSprite = nullptr;
}
}
Sprite*Entity::getSprite()const
{
return m_pSprite;
}
Animate* Entity::createAnimate(const string& format, int begin, int end
, float delayPerUnit, unsigned int loops)
{
vector<SpriteFrame*> frames;
auto frameCache = Director::getInstance()->getSpriteFrameCache();
//添加资源
for(int i = begin;i <= end;i++)
{
auto frame = frameCache->getSpriteFrameByName(StringUtils::format(format.c_str(),i));
frames.push_back(frame);
}
Animation*animation = Animation::createWithSpriteFrames(frames,delayPerUnit,loops);
return Animate::create(animation);
}
此方法为静态方法,主要是根据参数创建一个Animate(该方法完全可以使用AnimationCache代替)。
2.Crop类
在实现了Entity类后,接下来则是实现Crop类。
首先,先分析一下作物至少应该有的属性:
- 作物ID:该ID唯一标识作物,对应于crop.csv。
- 开始时间:作物种植的时间。在本游戏中使用当前时间减去开始时间来得到该作物的成长时间。
- 收获次数:作物已经收获的次数。游戏中的作物有的可以收获多次,该属性用来记录当前的收获次数。
Crop.h
class Soil;
class Crop : public Entity
{
SDL_BOOL_SYNTHESIZE(m_bWitherred, Witherred);//是否是枯萎的 默认为false
private:
//当前作物ID
int m_cropID;
//开始时间 秒数
time_t m_startTime;
//作物当前收货季数
int m_harvestCount;
//作物修正率[-1~1]
float m_cropRate;
//流逝时间 用于1秒更新作物贴图
float m_elpased;
//作物小时、分钟和秒数
int m_hour;
int m_minute;
int m_second;
//设置作物所在土壤
Soil* m_pSoil;
bool _first;
除了之前所说的属性之外,还增加了一些辅助属性,比如m_hour、m_minute、m_second,这三个属性是为了避免频繁的计算,有了这三个属性,游戏每过一秒就只需要使得m_second++,之后判断是否进位即可,而不需要再次根据开始时间和当前时间进行计算。
public:
Crop();
~Crop();
static Crop* create(int id, int startTime, int harvestCount, float rate);
bool init(int id, int startTime, int harvestCount, float rate);
void update(float dt);
Soil* getSoil();
void setSoil(Soil* soil);
create静态方法中有一个名称为rate的参数,该参数用在收获时对果实的个数的影响。
//作物是否成熟
bool isRipe() const;
//获取到从a阶段到b阶段的总时间 a的值应小于b
int getGrowingHour(int a, int b = -1);
//收获 返回果实的个数,返回-1表示不可收获
int harvest();
//获取时间
int getHour() const { return m_hour; }
int getMinute() const { return m_minute; }
int getSecond() const { return m_second; }
//获取作物ID
int getCropID() const { return m_cropID; }
time_t getStartTime() const { return m_startTime; }
int getHarvestCount() const { return m_harvestCount; }
float getCropRate() const { return m_cropRate; }
外部常用的公有函数。
private:
void addOneSecond();
//根据当前时间获取作物的贴图名
string getSpriteFrameName();
//获取作物的当前生长阶段
int getGrowingStep();
顾名思义,它们都是一些辅助函数,比如+1s,获取作物贴图,以及作物的生长阶段。
Crop.cpp
#include "Crop.h"
#include "Soil.h"
#include "StaticData.h"
Crop::Crop()
:m_bWitherred(false)
,m_cropID(0)
,m_startTime(0)
,m_harvestCount(0)
,m_cropRate(0.f)
,m_elpased(0.f)
,m_hour(0)
,m_minute(0)
,m_second(0)
,m_pSoil(nullptr)
,_first(true)
{
}
Crop::~Crop()
{
SDL_SAFE_RELEASE_NULL(m_pSoil);
}
Crop* Crop::create(int id, int startTime, int harvestCount, float rate)
{
Crop* crop = new Crop();
if (crop != nullptr && crop->init(id, startTime, harvestCount, rate))
crop->autorelease();
else
SDL_SAFE_DELETE(crop);
return crop;
}
bool Crop::init(int id, int startTime, int harvestCount, float rate)
{
//赋值
m_cropID = id;
m_startTime = startTime;
m_harvestCount = harvestCount;
m_cropRate = rate;
//获取作物的秒数
time_t now = time(nullptr);
time_t deltaSec = now - startTime;
//计算小时、分钟、和秒数
m_hour = deltaSec / 3600;
m_minute = (deltaSec - m_hour * 3600) / 60;
m_second = deltaSec - m_hour * 3600 - m_minute * 60;
string spriteName;
//检测是否已经枯萎
auto pCropSt = StaticData::getInstance()->getCropStructByID(m_cropID);
int totalHarvestCount = pCropSt->harvestCount;
if (m_harvestCount > totalHarvestCount)
{
m_bWitherred = true;
spriteName = STATIC_DATA_STRING("crop_end_filename");
}
else
{
spriteName = this->getSpriteFrameName();
}
//设置贴图
this->bindSpriteWithSpriteFrameName(spriteName);
//设置锚点
if(this->getGrowingStep() == 1)
{
this->setAnchorPoint(Point(0.5f, 0.5f));
}
else
{
this->setAnchorPoint(Point(0.5f, 0.8f));
}
return true;
}
init函数除了对一些基本的属性赋值之外,还计算得到了m_hour等的值,并且还判断当前的生长阶段的贴图和锚点。在这里,除了种子的锚点外,其余的都为(0.5f, 0.8f),该设置勉勉强强。
可以在最新版的texture packer pro(专业版 需要花钱买)中为每个需要的图片设置其锚点,然后在程序中进行读取即可(cocos2dx中的SpriteFrameCache类应该没有读取这个参数),也可以自己设置一个额外的文件来管理不同图片所对应的锚点。
void Crop::update(float dt)
{
//TODO:已经枯萎
if (m_bWitherred)
return ;
m_elpased += dt;
//第一次直接更新 以后一秒更新一次
if (m_elpased < 1.f && !_first)
return;
_first = false;
m_elpased = m_elpased - 1.f > 0.f ? m_elpased - 1.f: 0.f;
int beforeStep = this->getGrowingStep();
//增加一秒时间
this->addOneSecond();
//阶段是否改变
int afterStep = this->getGrowingStep();
//贴图将要发生变化
if (afterStep > beforeStep)
{
auto spriteName = this->getSpriteFrameName();
this->bindSpriteWithSpriteFrameName(spriteName);
}
}
update函数会在_first == true或者一秒后进行更新,它会使得作物的贴图发生改变。如果已经枯萎,则不再进行任何更新。当作物枯萎后,也可以做一些额外的操作,有一句古诗说得好,“化作春泥更护花”,枯萎的作物可以作为土地的养分,不过这样需要额外的判断。
Soil* Crop::getSoil()
{
return m_pSoil;
}
void Crop::setSoil(Soil* soil)
{
SDL_SAFE_RETAIN(soil);
SDL_SAFE_RELEASE(m_pSoil);
m_pSoil = soil;
}
内部保存了对应的土壤指针。
bool Crop::isRipe() const
{
//枯萎,则不定不成熟
if (m_bWitherred)
return false;
auto pCropSt = StaticData::getInstance()->getCropStructByID(m_cropID);
return pCropSt->growns.back() <= m_hour;
}
当前作物不枯萎,而成长时间大于等于总生长期,表示该作物已经成熟。
int Crop::getGrowingHour(int a, int b)
{
if ( a > b)
return -1;
auto pCropSt = StaticData::getInstance()->getCropStructByID(m_cropID);
auto& growns = pCropSt->growns;
auto size = growns.size();
if (a < 0)
a = size + a;
if (b < 0)
b = size + b;
//兼容判断
if (a == b)
return growns[a];
else
return growns[b] - growns[a];
}
该函数是获取[a, b]区间内的时间差,注意这里的a、b的值可以为负数(受到python的list切片的影响。。。)
int Crop::harvest()
{
auto staticData = StaticData::getInstance();
//不可收获,退出
if ( !this->isRipe())
{
return 0;
}
string spriteName;
//获取该作物的总季数
auto pCropSt = staticData->getCropStructByID(m_cropID);
int totalHarvestCount = pCropSt->harvestCount;
//进行收获
m_harvestCount++;
//已经超过,则贴图变为枯萎的作物
if (m_harvestCount > totalHarvestCount)
{
spriteName = STATIC_DATA_STRING("crop_end_filename");
m_bWitherred = true;
}
else
{
//获取倒数第二个时间段的时间
int hour = this->getGrowingHour(-2, -2);
//设置时间
m_startTime = time(NULL) - hour * 3600;
m_hour = hour;
m_minute = 0;
m_second = 0;
spriteName = this->getSpriteFrameName();
}
this->bindSpriteWithSpriteFrameName(spriteName);
//获取个数和果实个数浮动值
int number = pCropSt->number;
int numberVar = pCropSt->numberVar;
//获取随机值
int randomVar = rand() % numberVar + 1;
float scope = RANDOM_0_1();
if (fabs(m_cropRate) < scope)
{
number += m_cropRate > 0 ? randomVar : -randomVar;
}
return number;
}
首先,会判断是否成熟,不成熟,直接退出即可。之后收获次数++,如果超出了总收获次数,则枯萎;否则,该作物回溯到倒数第二个阶段,重新生长。最后,如果收获成功,则会返回果实的个数。
void Crop::addOneSecond()
{
m_second ++;
if (m_second >= 60)
{
m_minute++;
m_second -= 60;
}
if (m_minute >= 60)
{
m_hour++;
m_minute -= 60;
}
}
对时间进行计时,注意此时的进位。
string Crop::getSpriteFrameName()
{
auto staticData = StaticData::getInstance();
auto pCropSt = staticData->getCropStructByID(m_cropID);
string filename;
auto& growns = pCropSt->growns;
//获取贴图名称
//第一阶段 种子
if (m_hour < growns[0])
{
filename = staticData->getValueForKey("crop_start_filename")->asString();
}
else
{
size_t i = 0;
while (i < growns.size())
{
if (m_hour >= growns[i])
i++;
else
break;
}
auto format = staticData->getValueForKey("crop_filename_format")->asString();
filename = StringUtils::format(format.c_str(), m_cropID, i);
}
return filename;
}
不同类型的作物会在不同的生长期而贴图不同,该函数会获取到作物对应生长期的贴图文件名,它并不包括枯萎图片文件名。
int Crop::getGrowingStep()
{
auto pCropSt = StaticData::getInstance()->getCropStructByID(m_cropID);
auto& growns = pCropSt->growns;
auto len = growns.size();
size_t i = 0;
while (i < len)
{
if (m_hour < growns[i])
break;
i++;
}
return i + 1;
}
此函数会根据m_hour来判断该作物所处的生长阶段。在上面的update函数会根据此函数判断当前的贴图是否需要更新。
3.代码测试
继续在FarmScene::initializeCropsAndSoils()函数中进行添加代码:
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 = Crop::create(id, startTime, harvestCount, rate);
crop->setPosition(soil->getPosition());
crop->setSoil(soil);
this->addChild(crop);
soil->setCrop(crop);
}
}
6块土地,分别种植了6个ID不同、种植时间不同的作物,接下来运行,界面如下: