SDL遊戲開發之四-卡馬克卷軸

上一篇實現了瓦片地圖的繪製,但是單純地使用上面的代碼還是有些問題的,下面就來討論一下單純使用瓦片地圖的侷限性。

假設遊戲的分辨率爲960*720,瓦片地圖的大小也是960*720,瓦片大小爲32,那麼960/32 = 30, 720 / 32 = 22,即共有瓦片30*22=660個。一般的遊戲的FPS在60左右,即15ms刷新一次,那麼需要在這15ms之內最多要660次才能繪製出整個地圖,這還只是一個圖層的情況下;如果存在多個圖層的話,僅僅是繪製地圖就是一個很大的開銷。

 

卡馬克卷軸

對於使用到瓦片地圖的遊戲來說,如果地圖向右移動若干個像素,那麼屏幕右側則會出現新的內容;相反屏幕左側的部分就不再需要了,而屏幕中間的很大一部分都是不需要重新繪製的。顯然,如果每次都重新繪製所有的瓦片的話,有大部分區域都是和上一次的屏幕區域是相同的,如此造成了資源的浪費。這裏存在了一個思路,重用這兩次繪製的相同的部分,很容易想到創建一個略大於屏幕的緩衝區。

圖1-地圖向右滾動(圖片來源於網絡)

 在圖1中,地圖向右移動,區域C是新出現的部分,區域A是被捨棄的部分,而區域B則是可以重用的部分。從上面不難看出,區域A的大小和區域C的大小是相同的,那麼如果我直接在區域A上繪製新的內容,再把區域B和更新後的區域A繪製到屏幕上,不就可以減少繪製次數了嗎?上面的思路就是卡馬克卷軸。

思路是有了,那麼具體該怎麼實現呢?

要解決下面三個問題:

  1. 刷新緩衝區的時機。
  2. 如何刷新緩衝區。
  3. 如何把緩衝區的內容繪製到屏幕上。

依次解決上面的問題。

1.刷新緩衝區的時機

在地圖發生移動超過一個tileSize的時候,就需要刷新緩衝區。

在我個人看來,卡馬克卷軸的真正思想在於引入了“切割線”。以圖1爲例,在初始狀態下切割線carmarkX = 0,假設每次移動不超過tileSize的大小。在地圖向右移動超過一個tileSize的時候,區域A就廢棄,右側將會出現新的一列地圖,此時直接把新增的內容繪製到carmarkX所在的那一列(那一列就是切割線,即carmarkX所在的那一列),然後在拼接的時候,把更新後的區域A繪製到區域C即可。

這就是之前說的爲什麼要創建一個略大於屏幕的緩衝區,假如要創建一個和屏幕一樣大的緩衝區的話,當地圖右移的時候,只有移動超過一個tileSize的時候,纔會刷新緩衝區。圖一右移時,左側不再需要,則在左側繪製出現的新內容,而又因爲刷新是在移動超過一個tileSize的時候纔會進行,所以當移動少於一個tileSize時,最右側顯示的是最左側的內容(切割線的大小是tileSize的整數倍)。如下圖:

 

下面的兩個問題還是在代碼中說明。

在TMXTiledMap類的基礎上新增卡馬克卷軸的功能。

//TMXTiledMap.h
public:
	void fastDraw(int x, int y);
	void scroll(int x, int y);
  private:	
	void drawRegion(int srcX, int srcY, int width, int height, int destX, int destY);
	void updateBuffer(int x, int y);
	//卡馬克繪圖,再調用前應該設置_buffer爲target
	void carmarkDraw(int id, int destX, int destY);

	void copyBufferX(int indexMapX, int indexMapY, int tileColNum, int destX, int destY);
	void copyBufferY(int indexMapX, int indexMapY, int tileRowNum, int destX, int destY);

	//獲得切割線所在的圖塊索引
	int getIndexCarmarkX() const;
	int getIndexCarmarkY() const;

	//獲得切割線的在緩衝區的位置
	int getBufferCarmarkX() const;
	int getBufferCarmarkY() const;

	//獲取緩衝區後面的索引
	int getIndexBufLastX() const;
	int getIndexBufLastY() const;

	//獲得當前緩衝區去掉切割線的圖塊個數
	int getCarTileRowNum() const;
	int getCarTileColNum() const;
 private:

	//緩衝區大小尺寸 buffer width|height
	int _bufferWidth;
	int _bufferHeight;

	//緩衝區圖塊個數 buffer row|col tile num
	int _bufferRowTileNum;
	int _bufferColTileNum;

	//緩衝區增加的額外大小
	int _extraSize;

	//緩衝區
	SDL_Texture* _buffer;

	//地圖尺寸 - 緩衝區尺寸
	int _deltaWidth;
	int _deltaHeight;

	//地圖在緩衝區的X、Y的偏移量,限制在[0, deltaWidth|deltaHeight]
	int _offsetX;
	int _offsetY;

	//緩衝區切割線 必定是tileSize的整數倍
	int _carmarkX;
	int _carmarkY;
};

TMXTiledMap新增了很多函數和屬性,這些都是爲了實現卡馬克卷軸而準備的。

TMXTiledMap::TMXTiledMap(const std::string& tmxPath,SDL_Renderer*ren, int width, int height)
{
	//打開地圖文件
	bool ret = this->initWithFile(tmxPath);

	//稍微使得緩衝區大點
	_extraSize = _tileSize;

	//緩衝區要稍微比屏幕的尺寸大一些,並且能被tileSize整除
	int temp = 0;
	while (temp < _visibleWidth)
		temp += _tileSize;
	_bufferWidth = temp + _extraSize;

	temp = 0;
	while (temp < _visibleHeight)
		temp += _tileSize;
	_bufferHeight = temp + _extraSize;

	//緩衝區圖塊個數
	_bufferRowTileNum = _bufferWidth / _tileSize;
	_bufferColTileNum = _bufferHeight / _tileSize;

	//創建緩衝區
	_buffer = SDL_CreateTexture(_pRenderer, SDL_PIXELFORMAT_RGB444, SDL_TEXTUREACCESS_TARGET, _bufferWidth, _bufferHeight);

	//地圖變量初始化
	_deltaWidth = _mapRowTileNum * _tileSize - _visibleWidth;
	_deltaHeight = _mapColTileNum * _tileSize - _visibleHeight;

	//渲染到緩衝區
	SDL_SetRenderTarget(_pRenderer, _buffer);
	SDL_RenderClear(_pRenderer);
	//完全繪製
	this->draw();
	SDL_SetRenderTarget(_pRenderer, nullptr);
}

TMXTiledMap類的構造函數新增了對緩衝區的管理的功能,首先要保證緩衝區可以被tileSize整除,其次緩衝區要比屏幕打上_extraSize(原因上面已經說明),_deltaWidth和_deltaHeight的值爲地圖的尺寸 - 屏幕的尺寸,他們的大小決定了切割線的最大值

由於用到了緩衝區,所以在初始時需要先把當前的內容完全繪製到緩衝區。

void TMXTiledMap::scroll(int x, int y)
{
	x += _offsetX;
	y += _offsetY;

	if (x < 0 || y < 0)
		return;

	//緩衝區的偏移
	if (x > _deltaWidth) 
	{
		_offsetX = _deltaWidth;
		return;
	}
	if (y > _deltaHeight)
	{
		_offsetY = _deltaHeight;
		return;
	}
	//更新緩衝區
	this->updateBuffer(x, y);
}

scroll方法用來控制地圖的移動,如果當前移動合法的話,則會調用updateBuffer來更新緩衝區。

2.如何更新緩衝區

void TMXTiledMap::updateBuffer(int x, int y)
{
	_offsetX = x;
	_offsetY = y;

	//右移
	if (x > _carmarkX + _extraSize)
	{
		int indexMapLastX = getIndexBufLastX();
		//不會越界
		if (indexMapLastX < _mapRowTileNum)
		{
			copyBufferX(indexMapLastX, getIndexCarmarkY(),
				getCarTileColNum(),
				getBufferCarmarkX(), getBufferCarmarkY());
			_carmarkX += _tileSize;
		}
	}

	//左移
	if (x < _carmarkX)
	{
		_carmarkX -= _tileSize;
		copyBufferX(getIndexCarmarkX(), getIndexCarmarkY(),
			getCarTileColNum(),
			getBufferCarmarkX(), getBufferCarmarkY());
	}

	//下移
	if (y > _carmarkY + _extraSize)
	{
		int indexMapLastY = getIndexBufLastY();

		if (indexMapLastY < _mapColTileNum)
		{
			copyBufferY(getIndexCarmarkX(), indexMapLastY,
				getCarTileRowNum(),
				getBufferCarmarkX(), getBufferCarmarkY());
			_carmarkY += _tileSize;
		}
	}

	//上移
	if (y < _carmarkY)
	{
		_carmarkY -= _tileSize;
		copyBufferY(getIndexCarmarkX(), getIndexCarmarkY(),
			getCarTileRowNum(),
			getBufferCarmarkX(), getBufferCarmarkY());
	}
}

右移的情況在上面已經分析過了,當右移時,如果x > _carmark + _extraSize時,先繪製(即繪製x=0的那列),之後切割線右移一個tileSize;當地圖左移超過一個tileSize的時候,此時的x < _carmarkX成立,先讓_carmarkX -= _tileSize;即切割線先左移,然後重繪。假設此時地圖僅僅右移了一個tileSize,此時的carmarkX = _tileSize,重繪的區域在x軸爲0的列,而在左移後,carmarkX = 0,更新的還是橫軸爲0的列。這就是切割線在更新緩衝區的作用。

int TMXTiledMap::getIndexCarmarkX() const
{
	return _carmarkX / _tileSize;
}

int TMXTiledMap::getIndexCarmarkY() const
{
	return _carmarkY / _tileSize;
}

int TMXTiledMap::getBufferCarmarkX() const
{
	return _carmarkX % _bufferWidth;
}

int TMXTiledMap::getBufferCarmarkY() const
{
	return _carmarkY % _bufferHeight;
}

int TMXTiledMap::getIndexBufLastX() const
{
	return (_carmarkX + _bufferWidth) / _tileSize;
}

int TMXTiledMap::getIndexBufLastY() const
{
	return (_carmarkY + _bufferHeight) / _tileSize;
}

int TMXTiledMap::getCarTileRowNum() const
{
	return (_bufferWidth - _carmarkX % _bufferWidth) / _tileSize;
}

int TMXTiledMap::getCarTileColNum() const
{
	return (_bufferHeight - _carmarkY % _bufferHeight) / _tileSize;
}

以上的幾個函數都是在updateBuffer()中用到的。getIndexBufLastX()和getIndexBufLastY()主要用於確定當前要繪製地圖的哪一部分。

x軸移動影響的是一列(不一定是整列);y軸移動影響的是一行(同樣不一定是整行)。

getCarTileRowNum()和getCarTileColNum()則用於控制x、y移動是更新的列和行數。

void TMXTiledMap::copyBufferX(int indexMapX, int indexMapY, int tileColNum, int destX, int destY)
{
	int vy = 0;
	SDL_SetRenderTarget(_pRenderer, _buffer);
	//局部刷新
	//拷貝地圖上面到緩衝區的下面??
	SDL_Rect rect = {destX, 0, _tileSize, _tileSize * _bufferColTileNum};
	SDL_RenderFillRect(_pRenderer, &rect);

	for (int j = 0; j < tileColNum; j++)
	{
		vy = j * _tileSize + destY;
		int id = this->getTileGIDAt(indexMapX, indexMapY + j);
		//繪製
		this->carmarkDraw(id, destX, vy);
	}
	//拷貝地圖到緩衝區的上面
	for (int k = tileColNum; k < _bufferColTileNum; k++)
	{
		vy = (k - tileColNum) * _tileSize;
		int id = this->getTileGIDAt(indexMapX, indexMapY + k);

		this->carmarkDraw(id, destX, vy);
	}
	SDL_SetRenderTarget(_pRenderer, nullptr);
}
void TMXTiledMap::copyBufferY(int indexMapX, int indexMapY, int tileRowNum, int destX, int destY)
{
	int vx = 0;
	SDL_SetRenderTarget(_pRenderer, _buffer);
	//局部刷新
	//拷貝地圖上面到緩衝區的下面??
	SDL_Rect rect = {0, destY, _tileSize * _bufferRowTileNum, _tileSize};
	SDL_RenderFillRect(_pRenderer, &rect);

	//拷貝地圖左邊到緩衝的右邊
	for (int i = 0; i < tileRowNum; i++)
	{
		vx = i * _tileSize + destX;
		int id = this->getTileGIDAt(indexMapX + i, indexMapY);

		this->carmarkDraw(id, vx, destY);
	}
	//拷貝地圖右邊到緩衝區的左邊
	for (int k = tileRowNum; k < _bufferRowTileNum; k++)
	{
		vx = (k - tileRowNum) * _tileSize;
		int id = this->getTileGIDAt(indexMapX + k, indexMapY);

		this->carmarkDraw(id, vx, destY);
	}
	SDL_SetRenderTarget(_pRenderer, nullptr);
}

上面的兩個函數代碼類似,以copyBufferX()爲例,先是設置當前的緩衝區爲渲染目標,接着是通過SDL_RenderFillRect局部刷新,這裏沒有使用SDL_RenderClear()是因爲這個函數是全部刷訊。

然後下面的兩個函數則是繪製,至於爲什麼分爲兩個循環,我個人也不太理解,不過好像是因爲卡馬克點的存在,希望哪個大佬可以解惑。

void TMXTiledMap::carmarkDraw(int id, int destX, int destY)
{
	//0代表無圖塊
    if(id == 0)
    {
		return;
    }
    Tileset* tileset = getTilesetByID(id);
    id--;

    drawTile(tileset->name,tileset->margin,tileset->spacing
             ,destX, destY
             ,_tileSize,_tileSize
             ,(id - (tileset->firstGirdID - 1))/tileset->numColumns
             ,(id - (tileset->firstGirdID - 1))%tileset->numColumns);
}

這個函數只是簡單的封裝了一下drawTile。

3.如何把緩衝區的內容繪製到屏幕

void TMXTiledMap::fastDraw(int x, int y)
{
	int tempX = _offsetX % _bufferWidth;
	int tempY = _offsetY % _bufferHeight;

	//切割右下角的寬與高
	int rightWidth = _bufferWidth - tempX;
	int rightHeight = _bufferHeight - tempY;

	//繪製左上角
	drawRegion(tempX, tempY, rightWidth, rightHeight, x, y);

	//繪製右上角
	drawRegion(0, tempY, _visibleWidth - rightWidth, rightHeight, x + rightWidth, y);

	//繪製左下角
	drawRegion(tempX, 0, rightWidth, _visibleHeight - rightHeight, x, y + rightHeight);

	//繪製右下角
	drawRegion(0, 0, _visibleWidth - rightWidth, _visibleHeight - rightHeight, x + rightWidth, y + rightHeight);
}

fastDraw函數中分4次進行繪製,這個也不太理解。。。

void TMXTiledMap::drawRegion(int srcX, int srcY, int width, int height, int destX, int destY)
{
	//寬高度檢測
	if (width <= 0 || height <= 0)
		return;

	//超出屏幕檢測
	width = width > _visibleWidth ? _visibleWidth : width;
	height = height > _visibleHeight ? _visibleHeight : height;

	//渲染
	SDL_Rect srcRect = { srcX, srcY, width, height };
	SDL_Rect destRect = { destX, destY, width, height};
	SDL_RenderCopy(_pRenderer, _buffer, &srcRect, &destRect);
}

drawRegion()則相對較爲簡單,它主要就是真正的繪製,把緩衝區的部分內容繪製到屏幕的相應位置上。

最後則是main.cpp的更新

SDL_Point getScroll(SDL_Keycode keycode)
{
	SDL_Point speed = { 0, 0 };
	if (keycode != SDLK_UNKNOWN)
	{
		switch (keycode)
		{
		case SDLK_w:
		case SDLK_UP:
		case SDLK_KP_8:
			speed.y = -5;
			break;
		case SDLK_s:
		case SDLK_DOWN:
		case SDLK_KP_2:
			speed.y = 5;
			break;
		case SDLK_a:
		case SDLK_LEFT:
		case SDLK_KP_4:
			speed.x = -5;
			break;
		case SDLK_d:
		case SDLK_RIGHT:
		case SDLK_KP_6:
			speed.x = 5;
			break;
		case SDLK_KP_3:
			speed.x = 5;
			speed.y = 5;
			break;
		case SDLK_KP_7:
			speed.x = -5;
			speed.y = -5;
			break;
		case SDLK_KP_9:
			speed.x = 5;
			speed.y = -5;
			break;
		case SDLK_KP_1:
			speed.x = -5;
			speed.y = 5;
			break;
		default:
			break;
		}
	}
	return speed;
}

首先新建一個getScroll()函數用於處理按鍵。

   //循環
    while(running)
    {
        frameStart = SDL_GetTicks();

        SDL_RenderClear(gRen);
        //add code here..
        //pTiledMap->draw();
		pTiledMap->fastDraw(0, 0);
        SDL_RenderPresent(gRen);
		//update
		SDL_Point speed = getScroll(keycode);
		pTiledMap->scroll(speed.x, speed.y);

		//獲取事件
        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的繪製函數由draw改爲fastDraw(0, 0)即可。

注:嚴格意義上來說,fastDraw()中的x和y一般不應該爲0,這是因爲當主角移動的時候,一幀移動的距離一般小於圖塊寬高時,則會造成緩衝區的偏移,此時應該記錄上次和這次的偏差,然後作爲參數傳給fastDraw即可。

除此之外,由於當前的代碼的設計問題,目前的地圖只能逐漸滾動,而無法跳躍式移動。

注:

網上的卡馬克教程大多是比較古老的Java ME的教程,有點只是給出了思想而沒有給出具體的代碼,前幾天在逛csdn的時候獲得了卡馬克卷軸的java me的完整代碼,調試了一段時間後就把java代碼改成了c++和SDL代碼。

java me代碼:https://download.csdn.net/download/bull521/11147023

參考文檔:http://www.360doc.com/content/15/0722/14/8279768_486644348.shtml

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