SDL農場遊戲開發 4.Crop類,作物的產生及成長
首先,先建立一個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不同、種植時間不同的作物,接下來執行,介面如下: