上一篇实现了瓦片地图的绘制,但是单纯地使用上面的代码还是有些问题的,下面就来讨论一下单纯使用瓦片地图的局限性。

假设游戏的分辨率为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为targetvoid 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|heightint _bufferWidth;int _bufferHeight;//缓冲区图块个数 buffer row|col tile numint _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);//updateSDL_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

SDL游戏开发之四-卡马克卷轴相关推荐

  1. (转)卡马克卷轴算法研究

    卡马克卷轴算法研究 中文摘要 对于J2ME框架下的手机游戏程序的开发,其地图滚动的重绘有多种算法,由于手机性能的限制和开发周期等其他非技术条件,需要根据情况灵活选择所需的技术.但在及其苛刻条件下,如系 ...

  2. android 双缓冲地图,卡马克卷轴算法的研究地图双缓冲.doc

    卡马克卷轴算法的研究地图双缓冲 第 PAGE 12 页 共 NUMPAGES 25 页 卡马克卷轴算法研究摘要与关键词 卡马克卷轴算法研究 中文摘要 对于J2ME框架下的手机游戏程序的开发,其地图滚动 ...

  3. 3D游戏之父 电玩游戏奇才约翰·卡马克

    约翰·D·卡马克二世(John D. Carmack II,1970年8月20日-),是美国著名的电玩游戏设计开发者.著名的游戏设计公司id Software的创始人之一,id是一家专门开发电子游戏. ...

  4. SDL游戏开发之一-SDL的简介

    本教程为一个长系列,旨在于从零开始边学习SDL边开发游戏. 一.什么是SDL? SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用C语言写成.SDL提 ...

  5. SDL游戏开发系列第一话:Hello SDL

      各位读者朋友大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com.从今天起博主将带领大家一起走进SDL游戏开发的世界,如果说此前的Unity3D游戏开 ...

  6. 3D 游戏之父卡马克再创业:“我自己出得起 2000 万美元,但花投资人的钱会更有责任心”...

    整理 | 苏宓 出品 | CSDN(ID:CSDNnews) John Carmack,一代传奇游戏程序员,被誉为 3D 游戏之父. 近日,他在 Twitter 上透露,将再次进行创业,成立了一家新公 ...

  7. 乔布斯往事:游戏之神卡马克眼中的“英雄和傻瓜”

    本文的作者约翰·卡马克(John Carmack)是著名的计算机游戏图形学之父,2010 年的 GDC 终身成就奖获得者.作为天才的程序员,他几乎一手完成了早期 3D 游戏引擎的核心基础,FPS 类游 ...

  8. SDL游戏开发之三-瓦片地图

    一.瓦片地图 1)瓦片地图简介 瓦片地图(Tiled Map),又称为瓷砖地图,是在游戏开发中经常使用到的技术,它是由少量的尺寸相同的.小的瓦片图片拼接而成的很大的地图.相对于使用一张张图片来绘制地图 ...

  9. 【iOS-cocos2d-X 游戏开发之四】Cocos2dX创建Android NDK新项目并编译导入Eclipse中正常运行!...

    首先还没有配置好环境并正常运行Cocos2dx自带的test.android例子的童鞋先把环境都搭建好吧:[iOS-cocos2d-X 游戏开发之三]Mac下配置Android NDK环境并搭建Coc ...

最新文章

  1. 算法设计思想(4)— 分治法
  2. 开源MyBatis分页插件,省时省力
  3. python知识:稀疏矩阵转换成密度矩阵
  4. 八.linux系统文件属性知识
  5. ThinkPHP V5 漏洞利用
  6. 卷积的感受野计算及特征图尺寸计算
  7. JavaWeb项目生成PDF文件添加水印图片并导出
  8. MATLAB——imhist函数
  9. MarkDown学习手册
  10. 数独九宫格专家级解题思路
  11. VC dll依赖性查看工具depends
  12. Python问题:NotImplementedError: The confidence keyword argument is only available if OpenCV is install
  13. 《地球概论》(第3版)笔记 第四章 地球运动的地理意义
  14. 注意 怎么选择车险附加险?避免“这也不赔那也不赔”
  15. java 文件拷贝保留原来的属性_Java常用属性拷贝工具类使用总结
  16. Android Qcom USB Driver学习(八)
  17. 安化云台山第二届星空帐篷音乐节盛大启动
  18. reverse和reversed函数的总结
  19. Python通过MQTT协议上传物联网数据给ThingsBoard
  20. Kotlin入门第四节

热门文章

  1. 手把手带你撸一个校园APP(五):新闻中心模块
  2. 2018-7-16 2-1 分别由signed 和unsigned 限定的 char,short,int,long类型变量的取值范围
  3. MCS-51单片机C语言程序注释,精通MCS-51单片机C语言编程
  4. 怎么写一个php脚本_php脚本怎么写
  5. Oculus内下游戏报错,OVR40779122解决办法
  6. Entering emergency mode. Exit the shell to continue.。。。
  7. Python代码画小猪佩奇--turtle绘图
  8. [Leetcode学习-java]Additive Number
  9. 初次配置zookeeper——Invalid config, exiting abnormally
  10. 苹果或将为iPhone 13全系配备LiDAR