上一篇实现了瓦片地图的绘制,但是单纯地使用上面的代码还是有些问题的,下面就来讨论一下单纯使用瓦片地图的局限性。
假设游戏的分辨率为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