1. 程式人生 > >SDL農場遊戲開發 2.地圖與土壤層

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