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

猜你喜欢

转载自blog.csdn.net/bull521/article/details/84787322