SDL遊戲開發之三-瓦片地圖

一.瓦片地圖

1)瓦片地圖簡介

瓦片地圖(Tiled Map),又稱爲瓷磚地圖,是在遊戲開發中經常使用到的技術,它是由少量的尺寸相同的、小的瓦片圖片拼接而成的很大的地圖。相對於使用一張張圖片來繪製地圖而言,瓦片地圖不僅大大地節省了內容,而且增加了圖片的重用性和繪製性能。

使用一般的背景圖會面臨很多問題:

①:OpenGL ES對於紋理有大小限制,最大支持2048*2048像素,且超出這個範圍則會無法顯示。這個問題就可以使用瓦片地圖來解決,因爲瓦片地圖是拼接而成的大圖,從理論上說可以設置一個無限大的地圖,故不存在此類問題。

②:地圖中的有些位置是玩家不能進入的,比如說障礙,另外就是地圖與角色之間的遮擋處理,如果使用整張的背景圖,則需要額外的工作(圖片和代碼)來支持障礙處理和遮擋處理等問題;而使用瓦片地圖的話,對於一般的碰撞能精確到瓦片級別,而對於遮擋處理也是能夠做到。

瓦片地圖的設計較爲複雜,通常情況下,瓦片地圖都是使用一些編輯器來完成的,這裏使用的軟件是tiled,tiled地圖編輯器功能強大,除了能創建繪製層外,還可以創建對象層。對象層中包括了對象的相關數據,除了具有座標、名稱、類型等基本屬性外,還可以額外添加任意的鍵值對,這些數據可以在代碼中解析來達到特定的功能。瓦片地圖主要負責代替背景圖,以及處理碰撞。

2)解析及繪製原理

一般情況下,爲使得文件佔用體積變小,都會對tmx(tmx文件內部是xml格式)文件的繪製層的數據進行zlib壓縮和base64加密;而在使用前時則要先解壓,然後再base64解密,之後得到的數據即可在程序中使用(zlib是對數據進行壓縮,base64是爲了把壓縮後的二進制數據可以通過ASCII字符串顯式地表達出來)。

簡單地說,瓦片地圖就是從瓦片圖片中找到對應的瓦片並繪製到相應的位置。

若新建一個瓦片地圖,對應的瓦片圖片爲:

圖1-瓦片圖

在上圖中,每個瓦片都有一個唯一的ID,左上角爲1,依次遞增爲1、2、3...(在繪製時需要id-1,如果id-1 == 0,則不繪製)。

創建的瓦片地圖的顯示和對應數據如下: 

 

圖2-瓦片地圖

 

該圖的數據如下:

37,29,29,29

29,24,25,26

45,32,33,34

29,40,41,42

 

單純地使用瓦片地圖是相對來說比較簡單的,只需要解析一個外部的地圖文件,或者直接寫在程序裏面。

瓦片地圖的原理也比較簡單,思路大致如下圖: 

圖3-瓦片地圖的繪製(圖片來源於網絡 )

 

總地來說,瓦片地圖的基本思路就是把瓦片圖中的瓦片繪製到屏幕上對應的位置。

3)示例代碼

//TMXTiledMap.h
/*圖塊集*/
struct Tileset
{
    int firstGirdID;
    int tileWidth;
    int tileHeight;
    int spacing;
    int margin;
    int width;
    int height;
    int numColumns;
    std::string name;
};

Tileset結構體用來保存地圖文件(*.tmx)中使用到的圖塊的信息,其在tmx文件的結構如下:

 <tileset firstgid="1" name="blocks1" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18">
  <image source="blocks1.png" width="614" height="376"/>
 <tileset firstgid="199" name="blocks2" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18">
  <image source="blocks2.png" width="614" height="376"/>

本示例的tmx文件中使用到了兩個瓦片集,所以會有兩個tileset標籤,爲了保證在一個瓦片地圖中的用到的瓦片的id是唯一的,所以在tileset標籤中有一個屬性firstgid,它用來標識本圖塊的第一個瓦片的id是多少。

class TMXTiledMap
{
 public:
    TMXTiledMap(const std::string& tmxFile, SDL_Renderer* ren, int width, int height);
	~TMXTiledMap();
    void draw();
  private:
	bool initWithFile( const std::string& filepath);
    //解析layer中的data
    void parseTileLayer(TiXmlElement*pRoot);
    //解析tileset
    void parseTilesets(TiXmlElement*pTilesetRoot);
    //根據id獲得相應的圖塊集
    Tileset* getTilesetByID(unsigned int tileID);
	//根據座標獲取對應的gid
	int getTileGIDAt(int tileCoordinateX, int tileCoordinateY);
    //內部的draw,封裝了SDL_RenderCopyEx
    void drawTile(std::string id,int margin,int spacing,int x,int y,int width,int height,int currentRow,int currentFame);

TMXTiledMap類除了構造函數和析構函數外,目前僅有一個公有函數draw函數,它的作用就是繪製地圖,其他的私有函數則是解析文件和輔助地圖的繪製。

private:
    //保存圖塊集
    std::vector<Tileset*> _tilesets;
	//保存地圖數據
	std::vector<unsigned> _data;

    //tmx文件的寬/高瓦片個數
    int _mapRowTileNum;
    int _mapColTileNum;

    //tileset size
    int _tileSize;

    //save picture
    std::map<std::string,SDL_Texture*> _textures;
    //renderer
    SDL_Renderer*_pRenderer;
	//可視大小,可以認爲是屏幕大小
	int _visibleWidth;
	int _visibleHeight;

一維數組_data用於保存地圖數據,目前僅僅用到了一個_data,因此當前的TMXTiledMap類不支持多個圖層(擴展也比較簡單,可以新建一個TMXLayer類,每個TMXLayer對象保存着本層的地圖數據);一維數組可以用來保存二維數據,只不過在存取數據時需要一個映射(也可以使用二維數組)。

_tileSize爲整型,其實瓦片的寬和高是可以不同的,不過這裏爲方便而使用了_tileSize同時表示瓦片的寬度和高度。

//TMXTiled.cpp
TMXTiledMap::TMXTiledMap(const std::string& tmxPath,SDL_Renderer*ren, int width, int height)
	:_mapRowTileNum(0)
	,_mapColTileNum(0)
	,_tileSize(0)
	,_pRenderer(ren)
	,_visibleWidth(width)
	,_visibleHeight(height)
{
    bool ret = this->initWithFile(txtPath);
}

構造函數除了初始化變量外,還會調用initWithFile函數來讀取tmx文件並把數據賦給對應的變量。

bool TMXTiledMap::initWithFile(const std::string& filepath)
{
    TiXmlDocument doc;
    //加載tmx
    if(!doc.LoadFile(filepath.c_str()))
    {
        std::cout<<"error:"<<doc.ErrorDesc()<<std::endl;
        return false;
    }
    //獲得根節點
    TiXmlElement*pRoot = doc.RootElement();

    pRoot->Attribute("width",&_mapRowTileNum);
    pRoot->Attribute("height",&_mapColTileNum);
    pRoot->Attribute("tilewidth",&_tileSize);

    //parse the tilesets
    for(TiXmlElement*e = pRoot->FirstChildElement();e != NULL;e = e->NextSiblingElement())
    {
		std::string value = e->Value();
        if(value == "tileset")
        {
            parseTilesets(e);
        }
		else if (value == "layer")
		{
			this->parseTileLayer(e);
		}
    }
	return true;
}

tmx文件內部是xml格式,而解析tmx文件採用了第三方庫tinyxml。需要注意的是,本次示例代碼使用tinyxml自帶的功能來加載文件,這樣的使用不便於移植,若想要移植的話建議使用SDL_RWread等函數

本示例的tmx文件內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="32" tileheight="32" infinite="0" nextlayerid="4" nextobjectid="1">
 <tileset firstgid="1" name="blocks1" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18">
  <image source="blocks1.png" width="614" height="376"/>
 </tileset>
 <tileset firstgid="199" name="blocks2" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18">
  <image source="blocks2.png" width="614" height="376"/>
 </tileset>
 <layer id="3" name="塊層 1" width="30" height="20">
  <data encoding="base64" compression="zlib">
   eJzN08lKxEAQgOEOit68Org8hcsbuLyBK+6CO6LeZiau49WDD+Hu0eVF1Js7qHhxBcGDfzBCKNLdyWCLBR+hu6u7K1TSo5TqRZ8qPs4s42KizlOq3jPnPFjGtjjAIY5wbMkdxBCG8SnW5NhVVHjmcRDROpPEBjaxhW3sYBd72NfsSdvffgyIuaylt3GRtr9J4uc7a0CjpiYX/W3irma0oFVzb7S/V7jGjeHMeSxgEUtYxgoKkZw2zmxHBzrRhW5DL17xhnfrG5ljlDvGMI6JhL0vt+Sd4wKXmvVb9f2d5ZCHj48U987ynIup4QnPeInZa/ufKjkv4+nzg5wC66thzglOw9wS5kpFPcF5WZ455OH/kjvcI4MqVKMGtSgL79RZE/VMhnwxlnNyzWRdjB9T7HVpRjM//Q9qc/l+I47uS3KurGnKUutf+gLLXZ7t
  </data>
 </layer>
</map

initWithFile函數的功能就是依次讀取上面的標籤的屬性和值,並賦給對應的變量(這裏的layer標籤就是地圖的圖層,因爲代碼原因,目前只能有一個圖層)。

initWithFile在解析標籤時,碰到map的子節點tileset標籤和layer標籤時,會分別調用parseTileset()和parseTileLayer()函數。

void TMXTiledMap::parseTilesets(TiXmlElement*pTilesetRoot)
{
    //create a tileset object
    Tileset* tileset = new Tileset();

    pTilesetRoot->FirstChildElement()->Attribute("width",&tileset->width);
    pTilesetRoot->FirstChildElement()->Attribute("height",&tileset->height);

    pTilesetRoot->Attribute("firstgid",&tileset->firstGirdID);
    pTilesetRoot->Attribute("tilewidth",&tileset->tileWidth);
    pTilesetRoot->Attribute("tileheight",&tileset->tileHeight);
    pTilesetRoot->Attribute("spacing",&tileset->spacing);
    pTilesetRoot->Attribute("margin",&tileset->margin);

    tileset->name = pTilesetRoot->Attribute("name");

    tileset->numColumns = tileset->width / (tileset->tileWidth + tileset->spacing);

    _tilesets.push_back(tileset);
    //TODO:load texture在這裏需要注意路徑的修改
    std::string filePath = std::string("assets/") + pTilesetRoot->FirstChildElement()->Attribute("source");
    SDL_Texture*tex = IMG_LoadTexture(_pRenderer,filePath.c_str());

    _textures[tileset->name] = tex;
}

在parseTileset()中,除了獲取用到的值之外,還會加載對應的瓦片圖。注意這裏的文件路徑爲當前路徑下的assets文件夾下,可根據需要自行修改。

void TMXTiledMap::parseTileLayer(TiXmlElement*pTilesetRoot)
{
    std::string decodedIDs;
    TiXmlElement*pDataNode = nullptr;

    for(TiXmlElement*e = pTilesetRoot->FirstChildElement();e != NULL;e = e->NextSiblingElement())
    {
        if(e->Value() == std::string("data"))
        {
            pDataNode = e;
        }
    }
    for(TiXmlNode*e = pDataNode->FirstChild(); e != NULL; e = e->NextSibling())
    {
        //解碼數據,並用string保存
        TiXmlText*text = e->ToText();
        std::string t = text->Value();
        decodedIDs = base64_decode(t);
    }
    //uncompress解壓
    uLongf numGids = _mapRowTileNum * _mapColTileNum * sizeof(int);
    std::vector<unsigned> gids(numGids);

    uncompress((Bytef*)&gids[0],&numGids,(const Bytef*)decodedIDs.c_str(),decodedIDs.size());
    std::vector<int> layerRow(_mapRowTileNum);

	//_data = gids;
	_data = std::vector<unsigned>(_mapRowTileNum * _mapColTileNum);
    //拷貝數組
	std::copy(gids.begin(), gids.begin() + _mapRowTileNum * _mapColTileNum, _data.begin());
}

parseTileLayer會獲取到<layer>的子標籤<data>中的數據,先對這些數據進行解壓縮,然後再使用base64進行解密,最後把得到的數據賦給_data變量。

現在默認認爲採用了zlib壓縮和base64加密,如果tmx文件未壓縮和加密的話,則可以省略掉上述的解壓和解密過程

Tileset* TMXTiledMap::getTilesetByID(unsigned int tileID)
{
    for(unsigned int i = 0;i < _tilesets.size();i++)
    {
        /*這裏的判斷是如果tileID不在前m_tilesets.size()-1裏面,
        就必定是最後一個*/
        if(i + 1 <= _tilesets.size() - 1)
        {
            if(tileID >= _tilesets[i]->firstGirdID && tileID < _tilesets[i + 1]->firstGirdID)
            {
                return _tilesets[i];
            }
        }
        else
        {
            return _tilesets[i];
        }
    }
    std::cout<<"did not find tileset,rerturning empty tileset\n";
    return nullptr;
}

getTilesetByID()函數會根據瓦片的id來獲取它所對應的Tileset對象,如果未找到則返回空指針。

int TMXTiledMap::getTileGIDAt(int tileCoordinateX, int tileCoordinateY)
{
	if (tileCoordinateX >= 0 && tileCoordinateX < _mapRowTileNum &&
		tileCoordinateY >= 0 && tileCoordinateY < _mapColTileNum)
	{
		int z = (int)(tileCoordinateX + tileCoordinateY * _mapRowTileNum);
		return _data[z];
	}
	return 0;

getTileGIDAt函數會根據提供的圖塊座標來獲取所對應的瓦片id。

在瓦片地圖中,一般包含兩種座標:第一種是像素級座標,這個是較爲常用的座標,無論是繪製地圖,還是邏輯處理等一般都是使用的此類座標;第二類則是圖塊座標圖塊座標是以瓦片的大小爲一個單位的座標。

以上兩種座標是可以相互轉換的,像素座標除以瓦片尺寸就會得到瓦片座標(整型的除法)。

void TMXTiledMap::drawTile(std::string name,int margin,int spacing,int x,int y,int width,int height,int currentRow,int currentFrame)
{
    SDL_Rect srcRect;
    SDL_Rect destRect;

    srcRect.x = margin + (spacing + width) * currentFrame;
    srcRect.y = margin + (spacing + height) * currentRow;

    srcRect.w = destRect.w = width;
    srcRect.h = destRect.h = height;

    destRect.x = x;
    destRect.y = y;

	SDL_RenderCopyEx(_pRenderer, _textures[name], &srcRect, &destRect, 0, 0, SDL_FLIP_NONE);
}

drawTile封裝了一個專門用於繪製瓦片的函數:name是用到的SDL_Texture紋理名稱(紋理是存儲在_textures中的);marrgin和spacing是保存在Tileset中的兩個屬性,margin是瓦片圖的外邊距,而spacing則是瓦片圖的內邊距;currentRow、currentFrame、width和height用來控制從某行某列的尺寸爲(width, height)的矩形繪製到(x,y,width,height)上,即SDL中的srcRect;x、y、width、height用來控制繪製到哪裏,即SDL中的destRect

void TMXTiledMap::draw()
{
	//對偏移位置進行取反
	SDL_Point pos = { 0, 0 };
	//從哪開始繪製
	int startX = pos.x / _tileSize;
	int startY = pos.y / _tileSize;
	//繪製到哪 多繪製一個
	int endX = startX + _visibleWidth / _tileSize + 1;
	int endY = startY + _visibleHeight / _tileSize + 1;

	endX = endX > _mapRowTileNum ? _mapRowTileNum : endX;
	endY = endY > _mapColTileNum ? _mapColTileNum : endY;
	//只繪製屏幕
    for(int i = startY;i < endY;i++)
    {
        for(int j = startX;j < endX;j++)
        {
			int id = this->getTileGIDAt(j, i);
            //0代表無圖塊
            if(id == 0)
            {
                continue;
            }
            Tileset* tileset = getTilesetByID(id);
            id--;

            drawTile(tileset->name,tileset->margin,tileset->spacing
                     ,j * _tileSize,i * _tileSize
                     ,_tileSize,_tileSize
                     ,(id - (tileset->firstGirdID - 1))/tileset->numColumns
                     ,(id - (tileset->firstGirdID - 1))%tileset->numColumns);
        }
    }
}

draw函數用於完全繪製。

接着則是主函數:

#include<iostream>

#include "SDL.h"
#include "TMXTiledMap.h"

using namespace std;
//全局常量
const int FPS = 60;
const int DELAY_TIME = 1000/FPS;
//窗口和渲染器
SDL_Window* gWin = nullptr;
SDL_Renderer* gRen = nullptr;

bool init();
SDL_Point getScroll(SDL_Keycode keycode);

int main(int argc,char** argv)
{
	//地圖顯示
    TMXTiledMap* pTiledMap = nullptr;
    //
    Uint32 frameStart = 0, frameTime = 0;
    SDL_Event event;
    bool running = true;
	SDL_Keycode keycode = SDLK_UNKNOWN;
	//初始化SDL環境
	if (init())
	{
		cout << "初始化成功" << endl;
	}
	else
	{
		return -1;
	}
    pTiledMap = new TMXTiledMap("assets/map1.tmx",gRen, 640, 480);
    //循環
    while(running)
    {
        frameStart = SDL_GetTicks();

        SDL_RenderClear(gRen);
        //add code here..
        pTiledMap->draw();
        SDL_RenderPresent(gRen);
		//update


		//獲取事件
        while(SDL_PollEvent(&event))
        {
			switch (event.type)
			{
			case SDL_QUIT:
				running = false;
				break;
			case SDL_KEYDOWN:
				keycode = event.key.keysym.sym;
				break;
			case SDL_KEYUP:
				keycode = SDLK_UNKNOWN;
				break;
			default:
				break;
			}
        }
        frameTime = SDL_GetTicks() - frameStart;
        if (frameTime < DELAY_TIME)
        {
            SDL_Delay(int(DELAY_TIME - frameTime));
        }
    }
	//釋放內存
	delete pTiledMap;
	SDL_DestroyRenderer(gRen);
	SDL_DestroyWindow(gWin);
	SDL_Quit();

    return 0;
}

主函數則比較簡單,顯示創建了窗口和渲染器,然後創建了一個tiledMap對象,之後開始進入遊戲循環。

bool init()
{
	//初始化SDL
	if (SDL_Init(SDL_INIT_EVERYTHING) == 0)
    {
        if((gWin = SDL_CreateWindow("TileMapTest", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
			640, 480,SDL_WINDOW_SHOWN)) == NULL)
        {
            cout<<"error:"<<SDL_GetError()<<endl;
            return false;
        }
        else if((gRen = SDL_CreateRenderer(gWin, -1,
			SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE)) == NULL)
        {
            cout<<"error:"<<SDL_GetError()<<endl;
            return false;
        }

        SDL_SetRenderDrawColor(gRen,210,250,255,255);
    }

	return true;
}

運行結果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章