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 <vector>
#include <algorithm>
#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<Crop*> 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()
{
        //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 = 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 <string>
#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;
        //物品类型 string
        virtual 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中获取的字段不同。

#ifndef __Seed_H__
#define __Seed_H__

#include "Good.h"

class Seed : public Good
{
private:
        //种子ID 同作物ID
        int m_nID;
        int m_nNumber;
public:
        Seed();
        virtual ~Seed();
        static Seed* create(int id, int number);
        bool init(int id, int number);

        virtual string getGoodName() const;
        virtual SpriteFrame* getIcon() const;
        virtual string getName() const;
        virtual int getNumber() const;
        virtual int getCost() const;
        virtual string getDescription() const;
        virtual string getType() const;

        virtual void setNumber(int number);
        //执行函数
        virtual void execute(int userID, int targetID);
        //是否是消耗品
        virtual bool isDeleption() const;
        //获取物品类型
        virtual GoodType getGoodType() const;
};
#endif

 Seed.cpp的部分实现

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/),以及之后的存档保存和读取。

在农场游戏中,主要保存的数据有:

  1. 土壤信息和对应的作物信息。
  2. 金钱。
  3. 等级和经验。
  4. 背包:种子和果实。

 而DynamicData类主要处理的就是以上的这些数据。

DynamicData.h

#ifndef __DynamicData_H__
#define __DynamicData_H__
#include <map>
#include <cmath>
#include <string>
#include <vector>
#include <algorithm>

#include "SDL_Engine/SDL_Engine.h"
#include "Good.h"

using namespace std;
USING_NS_SDL;

class Crop;
class Soil;
class Good;
enum class GoodType;

//农场等级和农场经验
#define FARM_LEVEL_KEY "farm_level"
#define FARM_EXP_KEY "farm_exp"
#define GOLD_KEY "gold"

记得使用超前引用。

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<Good*> 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<Good*>& 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);
        //根据类型和名称创建Good
        Good* 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;
        }
        else
                m_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();
        //获取对应存档valueMap
        auto& 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这三个函数与存档的结构有关。土壤的存档结构大致如下:

                <key>soils</key>
                <array>
                        <dict>
                                <key>harvest_count</key>
                                <integer>1</integer>
                                <key>crop_rate</key>
                                <real>0</real>
                                <key>crop_start</key>
                                <integer>1543970457</integer>
                                <key>crop_id</key>
                                <integer>104</integer>
                                <key>soil_id</key>
                                <integer>12</integer>
                                <key>soil_lv</key>
                                <integer>1</integer>
                        </dict>
                </array>

土壤是一个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 = 0;
                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);
                //是否存在对应的作物ID
                CropStruct* 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/pull/new/Farm-04

猜你喜欢

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