1. 程式人生 > >SDL農場遊戲開發 4.Crop類,作物的產生及成長

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類。

首先,先分析一下作物至少應該有的屬性:

  1. 作物ID:該ID唯一標識作物,對應於crop.csv。
  2. 開始時間:作物種植的時間。在本遊戲中使用當前時間減去開始時間來得到該作物的成長時間。
  3. 收穫次數:作物已經收穫的次數。遊戲中的作物有的可以收穫多次,該屬性用來記錄當前的收穫次數。

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不同、種植時間不同的作物,接下來執行,介面如下:

本節程式碼: https://github.com/sky94520/Farm/tree/Farm-03