上一篇實現了瓦片地圖的繪製,但是單純地使用上面的代碼還是有些問題的,下面就來討論一下單純使用瓦片地圖的侷限性。
假設遊戲的分辨率爲960*720,瓦片地圖的大小也是960*720,瓦片大小爲32,那麼960/32 = 30, 720 / 32 = 22,即共有瓦片30*22=660個。一般的遊戲的FPS在60左右,即15ms刷新一次,那麼需要在這15ms之內最多要660次才能繪製出整個地圖,這還只是一個圖層的情況下;如果存在多個圖層的話,僅僅是繪製地圖就是一個很大的開銷。
卡馬克卷軸
對於使用到瓦片地圖的遊戲來說,如果地圖向右移動若干個像素,那麼屏幕右側則會出現新的內容;相反屏幕左側的部分就不再需要了,而屏幕中間的很大一部分都是不需要重新繪製的。顯然,如果每次都重新繪製所有的瓦片的話,有大部分區域都是和上一次的屏幕區域是相同的,如此造成了資源的浪費。這裏存在了一個思路,重用這兩次繪製的相同的部分,很容易想到創建一個略大於屏幕的緩衝區。
在圖1中,地圖向右移動,區域C是新出現的部分,區域A是被捨棄的部分,而區域B則是可以重用的部分。從上面不難看出,區域A的大小和區域C的大小是相同的,那麼如果我直接在區域A上繪製新的內容,再把區域B和更新後的區域A繪製到屏幕上,不就可以減少繪製次數了嗎?上面的思路就是卡馬克卷軸。
思路是有了,那麼具體該怎麼實現呢?
要解決下面三個問題:
- 刷新緩衝區的時機。
- 如何刷新緩衝區。
- 如何把緩衝區的內容繪製到屏幕上。
依次解決上面的問題。
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