SDL农场游戏开发 2.地图与土壤层

本游戏的地图使用的是tiled这个软件导出的*.tmx(xml格式),地图类型是45度方向,以前曾经研究过45度与90度地图的区别,最后发现区别不是很大,主要在于图块的不同,90度地图的图块一般是矩形(正方形),而45度地图非透明图块一般是菱形。

本游戏地图共有以下几种图块:

用到的图块主要是第一个图块和第四个图块,其他的图块作为扩展图块。

tiled地图分为两层:

bg层主要负责显示背景图片,而"soil layer"则负责显示土壤,比如本游戏共有18块土地。

本节有3个类,分别是FarmScene、SoilLayer、和Soil。顾名思义,Soil为土壤对象,除了保存土壤等级和土壤ID之外,还会有着指针指向了当前土壤种植的作物对象以及一个土壤的贴图精灵。该精灵和土壤ID是从tiled地图中获取到的。

一般情况下,右下的渲染方式决定id是从上自下、从左自右、以0开始依次递增。

此id是唯一的,像cocos2dx的TMXTiledLayer类中的getTileGIDAt(const Point& tileCoordinate)的内部代码中,由于使用的是一维数组保存的地图信息,所以其中进行了一个转换:

z = int(tileCoordinate.x  + tileCoordinate.y * _width)。(_width为地图宽度)。

1.Soil类

首先是Soil.h:

#ifndef __Soil_H__
#define __Soil_H__

#include "SDL_Engine/SDL_Engine.h"
USING_NS_SDL;

class Crop;

class Soil : public Node
{
private:
        Sprite* m_pSprite;
        int m_id;
        //TODO:当前的土地等级1-4
        int m_level;
        Crop* m_pCrop;
public:
        Soil();
        ~Soil();
        static Soil* create(Sprite* sprite, int id, int level);
        bool init(Sprite* sprite, int id, int level);

        int getSoilID() const { return m_id; }
        int getSoilLv() const { return m_level; }
        void setSoilLv(int lv) { m_level = lv; }

        Sprite* getSprite() { return m_pSprite; }

        Crop* getCrop();
        void setCrop(Crop* crop);
};
#endif

然后是实现Soil.cpp

#include "Soil.h"

Soil::Soil()
        :m_pSprite(nullptr)
        ,m_id(0)
        ,m_level(1)
        ,m_pCrop(nullptr)
{
}

Soil::~Soil()
{
        SDL_SAFE_RELEASE(m_pSprite);
}

Soil* Soil::create(Sprite* sprite,int id, int level)
{
        auto soil = new Soil();

        if (soil && soil->init(sprite, id, level))
                soil->autorelease();
        else
                SDL_SAFE_DELETE(soil);

        return soil;
}

bool Soil::init(Sprite* sprite, int id, int level)
{
        SDL_SAFE_RETAIN(sprite);
        SDL_SAFE_RELEASE(m_pSprite);

        m_pSprite = sprite;
        m_id = id; 
        m_level = level;

        this->setContentSize(m_pSprite->getContentSize());
        this->setAnchorPoint(Point(0.5f, 0.5f));

        return true;
}

Crop* Soil::getCrop()
{
        return m_pCrop;
}

void Soil::setCrop(Crop* crop)
{
        m_pCrop = crop;
}

土壤类的代码较少,值得注意的就是m_level,它主要的作用就是上层(SoilLayer)会根据这个属性来更新土壤的贴图,那么问题来了:

问:为什么不在Soil类中进行更新呢?

答:主要是因为土壤类的贴图是从TMXTiledMap中获取到的,TMXTiledMap对象负责修改tiled图块的贴图。

2.SoilLayer类

SoilLayer.h

#ifndef __SoilLayer_H__
#define __SoilLayer_H__
#include <vector>
#include <iostream>
#include "SDL_Engine/SDL_Engine.h"

USING_NS_SDL;
using namespace std;

class Soil;

class SoilLayer : public Layer
{
private:
        //地图
        TMXTiledMap* m_pTiledMap;
        //保存土壤对象
        vector<Soil*> m_soilVec;
public:
        SoilLayer();
        ~SoilLayer();

        CREATE_FUNC(SoilLayer);
        bool init();
        /* 获取点击的在m_soilVec数组中的土壤对象
         * @param pos 世界位置
         * @return pos对应的土壤对象或nullptr
         */
        Soil* getClickingSoil(const Point& pos);
        /* 根据id获取土壤精灵的坐标 搜寻的是tiledMap中的土壤精灵
         * @param z tiled地图对应id
         * @return 对应瓦片的世界位置
         */
        Point getSoilPositionByID(int z); 
        /* 更新并获得土壤精灵
         * @param soilLv 土壤ID
         * @param soilLv 土壤等级
         * @return soilID对应的精灵
         */
        Sprite* updateSoil(int soilID, int soilLv);
        /* 根据id和等级生成土壤并返回
         * @param soilID 土壤id
         * @param soilLv 土壤等级
         * @return 初始化完成的土壤对象
         */
        Soil* addSoil(int soilID, int soilLv);
};
#endif

SoilLayer负责显示地图(内部的TMXTiledMap对象的功能)以及管理土壤对象,比如添加土壤、更新土壤贴图等。

#include "SoilLayer.h"
#include "Soil.h"

SoilLayer::SoilLayer()
        :m_pTiledMap(nullptr)
{
}

SoilLayer::~SoilLayer()
{
}

bool SoilLayer::init()
{
        m_pTiledMap = TMXTiledMap::create("farm_map/farm.tmx");
        m_pTiledMap->setPosition(50, 150);

        this->addChild(m_pTiledMap);
    
        return true;
}

init函数中创建了一个TMXTiledMap对象,并稍微设置了其显示位置,farm.tmx保存在项目的Resources文件夹中。

Soil* SoilLayer::getClickingSoil(const Point& loc)
{
        Soil* soil = nullptr;
        auto it = find_if(m_soilVec.begin(), m_soilVec.end(), [&loc](Soil* soil)
        {
                //菱形对角线大小
                auto size = soil->getContentSize();
                auto soilPos = soil->getPosition();
                //进行坐标的变换
                auto pos = loc - soilPos;
                //计算矩形的大小的一半
                auto halfOfArea = size.width * size.height / 4;
                //计算点击点的面积大小
                auto clickOfArea = fabs(pos.x * size.height * 0.5f) + fabs(pos.y * size.width * 0.5f);
                //判断是否在菱形内
                return clickOfArea <= halfOfArea;

        });

        if (it != m_soilVec.end())
                soil = *it;

        return soil;
}

getClickingSoil()函数主要是获取点所对应的土壤,为了使得碰撞精确,因此使用的是菱形判断。关于菱形判断,可以参考这篇

Point SoilLayer::getSoilPositionByID(int soilID)
{
        //读取土壤层所有图块
        auto layer = m_pTiledMap->getLayer<TMXLayer*>("soil layer");
        int width = (int)mapSize.width;
        int x = soilID % width;
        int y = soilID / width;
        auto sprite = layer->getTileAt(Point(x, y));
        //layer->getChildByTag<Sprite*>(soilID);
        //位置转换
        auto pos = m_pTiledMap->convertToWorldSpace(sprite->getPosition());

        return pos;
}

这个函数和上面函数的检测范围不同,上一个主要是判断的土壤对象列表;而getSoilPositionByID()则是判断的是ID所对应的图块精灵的位置,之后变成世界坐标并返回,此函数主要用在之后的扩充面板所处的位置,如图所示:

Sprite* SoilLayer::updateSoil(int soilID, int soilLv)
{
        //读取土壤层所有土壤
        auto layer = m_pTiledMap->getLayer<TMXLayer*>("soil layer");

        //找到对应的土壤精灵
        auto mapSize = m_pTiledMap->getMapSize();
        int width = (int)mapSize.width;

        int x = soilID % width;
        int y = soilID / width;

        auto tileCoordinate = Point(x, y);
        //根据等级设置贴图
        int gid = 3 + soilLv;
        layer->setTileGID(gid, tileCoordinate);

        return layer->getTileAt(tileCoordinate);
}

updateSoil函数的作用就是根据soilID和soilLv来更新tiled对应瓦片的贴图,因为当前默认认为第四块图片为正常土地,所以需要进行转换一下,即gid = 3 + soilLv,gid是tiled中的称呼,在这里表示的是土壤的不同贴图,从左往右依次为1,2,3,4,5,。

Soil* SoilLayer::addSoil(int soilID, int soilLv)
{
        //更新并获得土壤精灵
        auto sprite = this->updateSoil(soilID, soilLv);
        Soil* soil = Soil::create(sprite, soilID, soilLv);

        m_soilVec.push_back(soil);
        this->addChild(soil);
        //设置位置 当前位置已经是世界坐标
        auto pos = m_pTiledMap->convertToWorldSpace(sprite->getPosition());

        soil->setPosition(pos);

        return soil;
}

addSoil()可以认为是一个工厂方法,根据原料加工出对应的对象,该方法调用了updateSoil方法来更新并获取对应的精灵。

3.FarmScene

FarmScene类作为Controller,当前的代码量倒是不太多。

FarmScene.h

#ifndef __FarmScene_H__
#define __FarmScene_H__

#include "SDL_Engine/SDL_Engine.h"

USING_NS_SDL;

class SoilLayer;

class FarmScene : public Scene
{
public:
        FarmScene();
        ~FarmScene();
        CREATE_FUNC(FarmScene);
        bool init();

public:
        //处理触碰事件
        bool handleTouchEvent(Touch* touch, SDL_Event* event);
private:
        bool preloadResources();
        //初始化土壤和作物
        void initializeSoilsAndCrops();
private:
        SoilLayer* m_pSoilLayer;
};
#endif

FarmScene.cpp

#include "FarmScene.h"
#include "SoilLayer.h"
#include "Soil.h"

FarmScene::FarmScene()
        :m_pSoilLayer(nullptr)
{
}

FarmScene::~FarmScene()
{
}

bool FarmScene::init()
{
        Size visibleSize = Director::getInstance()->getVisibleSize();

        this->preloadResources();

        m_pSoilLayer = SoilLayer::create();
        this->addChild(m_pSoilLayer);

        //初始化土壤和作物
        this->initializeSoilsAndCrops();
}

bool FarmScene::handleTouchEvent(Touch* touch, SDL_Event* event)
{
        return false;
}

bool FarmScene::preloadResources()
{
        //加载资源
        auto spriteCache = Director::getInstance()->getSpriteFrameCache();

        spriteCache->addSpriteFramesWithFile("sprite/farm_crop_res.xml");
        spriteCache->addSpriteFramesWithFile("sprite/farm_ui_res.xml");
        spriteCache->addSpriteFramesWithFile("sprite/good_layer_ui_res.xml");
        spriteCache->addSpriteFramesWithFile("sprite/slider_dialog_ui_res.xml");

        return true;

}

void FarmScene::initializeSoilsAndCrops()
{
}

FarmScene类目前代码较少,主要是预加载资源,然后创建了一个SoilLayer对象,之后调用initializeSoilsAndCrops(),这个方法当前为空,之后则是根据存档生成对应的Soil对象,然后再判断是否有该土地所对应的作物,如果有的话,则还生成作物对象。

好了,使用cocos2dx需要在AppDelegate类把启动场景改为FarmScene,然后编译、运行即可。

如果代码无误的话,程序应该返回的是一个全是荒地的农场,百废待兴。

最后则是对刚才写的代码进行小小的测试一番。

4.测试代码

首先,添加如下代码:

void FarmScene::initializeSoilsAndCrops()
{
        int soilIDs[] = {12, 13, 14, 15, 16, 17};

        for (int i = 0; i < 6; i++)
        {
                auto soil = m_pSoilLayer->addSoil(soilIDs[i], 1); 
        }
}

这里初始化了6块土壤,然后添加土壤。

编译运行,就会发现原本全是荒地的农场出现了6块肥沃的土地。之后再测试下SoilLayer类的其他函数。

在FarmScene::init中创建一个监听器:

bool FarmScene::init()
{
        //...
        //添加事件监听器
        auto listener = EventListenerTouchOneByOne::create();
        listener->onTouchBegan = SDL_CALLBACK_2(FarmScene::handleTouchEvent, this);

        _eventDispatcher->addEventListener(listener, this);

        return true;
}

之后完善一下handleTouchEvent:

bool FarmScene::handleTouchEvent(Touch* touch, SDL_Event* event)
{
        auto location = touch->getLocation();
        //是否点击了土地
        auto soil = m_pSoilLayer->getClickingSoil(location);

        if (soil != nullptr)
        {
                printf("clicked soil, the id is %d\n", soil->getSoilID());
        }
        return false;
}

此时点击了之前创建的农田后,就会在控制台输出你所点击的农田的id。本节演示:

 

本节代码:

https://github.com/sky94520/Farm/tree/Farm-01

猜你喜欢

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