对于J2ME框架下的手机游戏程序的开发,其地图滚动的重绘有多种算法,由于手机性能的限制和开发周期等其他非技术条件,需要根据情况灵活选择所需的技术。但在及其苛刻条件下,如系统CPU资源不足,地图块尺寸较小等,会造成屏幕闪耀,帧数过低等情况,严重影响到游戏体验。在开发中如此类问题无法绕过以及避免(指通过修改策划方案,以及程序使用的技术框架),则需要考虑使用地图缓冲绘制技术,卡马克卷轴就是一种最经典的地图缓冲绘制技术。可有效的改善在地图绘制中的屏幕闪耀,帧数过低等情况。
English Abstract
For J2ME Mobile Phone Games under theframework of the development process, and its rolling redraw the map has avariety of algorithms, because of restrictions on mobile phone performance anddevelopment cycle and other non-technical conditions required under thecircumstances required the flexibility to choose technologies. However, in itsharsh conditions, such as system CPU resources are insufficient, and a smallerblock size, etc., will cause the screen shine, low frames, etc., seriously affectingthe gaming experience. At the development of such a category can not bypass theproblem and to avoid (referring to the adoption of amendments to planningprograms, as well as the technology used in the framework of the procedure),you need to consider the use of map rendering buffer, scroll Carmack is one ofthe most classic map buffer rendering. Can effectively improve the mapping ofthe screen shine, frames are too low and so on.
关键词
卡马克卷轴:一种经典的地图缓冲绘制技术。可有效的改善在地图绘制中的屏幕闪耀,帧数过低等情况。
Title:地图的贴片,指在地图绘制时,系统重绘的最小地图单元。一般为正方形,尺寸有8、16、24、32 Pixels等。
地图重绘:在游戏中由于角色移动造成的地图显示区域的改变,重新绘制地图的显示区域就称为地图重绘。
缓冲:在内存中建立一个区域,该区域等于或者略大于屏幕大小。在重绘时,首先在缓冲区上重绘,再一次性把缓冲区画到屏幕上。这种预先绘制的方式就是缓冲。
目录
卡马克卷轴是一种经典的地图缓冲绘制技术。可有效的改善在地图绘制中的屏幕闪耀,帧数过低等情况。可以使用在性能受到限制的手机上,以提高地图显示质量,防止屏幕闪耀,以及提高游戏时的帧数。
研究方法及过程如下:首先分析比较几种常见的地图绘制方法,指出其优劣,并引入地图缓冲的重绘问题。然后对卡马克卷轴的算法原理做具体描述以及分析。之后使用j2me技术平台实现卡马克卷轴算法的DEMO,用于技术演示。最后在实际开发的项目中,检测和评估卡马克卷轴的实际优化效果,并给出结论。
由于篇幅以及时间所限,对于卡马克卷轴的多种变形写法,没有做进一步的分析。
一、地图绘制的常用算法和优化
注:没有讨论MIDP2.0中的GAMEAPI下TiledLayer类画法
1.1无缝图片滚动画法
说明:最简单的一种画地图方法,无需使用数组,使用一张无缝的背景图片,在屏幕上绘制两次,以此来实现最简单的地图滚动效果,和图片的重复使用以节约资源。
示意图:红色虚线部分为屏幕,使用一个偏移量在屏幕中错开位置贴上两次图片,通过不断改变偏移量的大小来实现动画效果。
代码举例:
//imgBack图片对象
//posX图片在X轴方向上的偏移量
g.drawImage(imgBack,--posX,0, 0);
g.drawImage(imgBack, imgBack.getHeight()+posX,0, 0);
if(posX==-imgBack.getHeight())
posX=0;
优点与局限:此算法非常简单,由于是单张图片反复滚动生成的背景图片,所以对于美术人员的限制较少利于发挥,而已外观效果好。但因为不是地图Title组成的,资源复用率不高,只能用于生成不太复杂的地图。而且由于没有Title的存在,无法针对不同的Title计算碰撞。最终使得这种画法只能用于绘制简单屏幕背景图片,而无法用在有复杂物理碰撞的地图层。
1.2MIDP1.0的裁剪区画法
说明:使用二维数组保存地图信息,另外有一张图片素材,根据地图数组的不同下标,配合setClip(x,y,w,h)裁剪区方法,将对应的Title显示在正确的位置上。
示意图:红色虚线部分为屏幕,红色实线为裁剪区,通过读取地图数组,将相应的位置设置为裁剪区,并用将图片素材相对于裁剪区偏移一定x,y位置的方法,使得要绘制的Title正好对应出现在裁剪区中。
代码举例:
//cellSize一个title的尺寸大小,宽高同
//mapArray地图数组 ,保存地图信息
//leftTopX,leftTopY地图相对于屏幕位置的偏移量
//picColNum,图片素材的title列数
for(int i=0;i<mapArray.length;i++){
for(int j=0;j<mapArray[0]length;j++){
g.setClip(j*cellSize-leftTopX,
i*cellSize -leftTopY,
cellSize,
cellSize);
g.drawImage(imgMap,
j* cellSize -(mapArray[i][j]-1)%picColNum* cellSize -leftTopX,
i* cellSize -(mapArray[i][j]-1)/picColNum* cellSize -leftTopY,
0);
}
}
g.setClip(0,0,GameCanvas.WIDTH,GameCanvas.HEIGHT);
优点与局限:相对于前一种画法,图片资源的利用率提高了很多,可以绘制很复杂的地图。由于Title的存在,可以针对不同的Title计算碰撞,可以用于地图物理层的绘制。但是由于setClip裁剪区本身的局限,实际上每次绘制都多画了很多不会显示的内容(如上图),因此会造成系统资源的浪费。屏幕尺寸越大,cellSize越小,资源浪费就越大。
1.3MIDP2.0的图素切片画法
说明:在MIDP2.0的Image类中添加了createImage(Image image, int x,int y, int width, int height, int transform)方法,使用该方法可以非常方便的在内存中创建地图Title的切片缓冲区(Image对象数组)。使用切片缓冲区中的Title我们可以很方便的在屏幕中绘制地图元素。
示意图:红色虚线部分为屏幕,红色实线为当前绘制的title,通过读取地图数组,将内存缓冲区中相对应的Title绘制到屏幕上。
代码举例:
//省略了创建地图Title的切片缓冲区cellImage的方法
for (int i= 0; i < mapArray.length;i++) {
for (int j= 0; j < mapArray[0].length;j++) {
if(mapArray[i][j]>0)
g.drawImage(cellImage[mapArray[i][j]-1], j* cellSize -leftTopX,
i * cellSize -leftTopY,
0);
}
}
}
优点与局限:拥有1.0画法的所有优点,绘制地图十分方便。而且
createImage(Image image, int x,int y, int width, int height, int transform)方法所创建的图片为透明图片,在创建复杂的多重背景滚动地图时更加游刃有余。
1.4最常见的地图绘制优化——只绘制当前屏幕
上面的绘制方法都是画整个地图的,对于游戏中地图比屏幕大的情况,会造成很大的资源浪费。在实际开发中,常用的优化方法就是只绘制当前屏幕的地图Title。
代码举例:
//计算单元格起始位置下标
intiStart=leftTopY/ cellSize;
intjStart=leftTopX/ cellSize;
//计算单元格绘制宽度和高度
intiNum=GameCanvas.HEIGHT/ cellSize+1;
intjNum=GameCanvas.WIDTH/ cellSize+1;
//防止下标越界
if(iStart+iNum>=mapArray.length)
iNum=mapArray.length-1-iStart;
if(jStart+jNum>=mapArray[0].length)
jNum=mapArray[0].length-1-jStart;
1.5卡马克卷轴算法的引入
对于某些资源严重不足的手机,或者由于Title比较小,循环次数过多而造成画图时屏幕闪耀。就需要针对画图时相关算法做进一步的优化算法了。不论哪种优化算法,一个统一的思路就是尽量减少绘制的次数,从而减少系统资源的消耗。卡马克卷轴就是这样算法的一个经典例子。
对于横版游戏来说,如果角色向右侧移动,则地图向左侧滚动。由于角色每次移动若干个pixel(步长),因此地图中新画出的区域宽度也为若干个pixel,那么如果让系统重绘所有屏幕区域,很明显大部分区域都是和上一次屏幕区域相同的,如此造成成了资源的浪费。这里有一个思路——如果上一次绘制过的地图也能够部分重用到本次地图绘制上来就好了。那么很容易想到在内存中建立一个和屏幕一样大的(或略大的)Image缓冲区即可。
由上图可以看到,区域B为相同的地图区域,这个区域在下一次屏幕重绘时,可以被重新利用。区域A是在下一次屏幕重绘中不被采用的区域,这区域应当被舍弃,但是如果稍微留意一下的话,不难发现区域A和区域C的面积大小其实居然是一样的。
那么如果建立一个和屏幕大小相同的缓冲,在其被舍弃掉的绘制区域A中画上新的区域C,再把区域B和区域C拼合到屏幕上,是不是就能达到减少系统资源消耗的目的了呢?卡马克卷轴的基本原理正是如此。
上图显示了卡马克卷轴的最基本原理,首先在内存中建立一块和屏幕一样大小(或略大)的缓冲区。然后在本应由于地图移动而被舍弃掉的区域1上面绘制,由于地图滚动而出现的新地图区域。最后把两个区域按照地图的实际位置拼合到屏幕上。
2.2X&Y双轴滚动的卡马克卷轴
对于俯视游戏,或者有y轴卷动的游戏来说,单单一个方向的地图卷动并不够用。那么如果是出现两个方向的卷动会如何呢。不必担心,上面的思路算法一样能适应这种情况。
由上图可以看到,区域D为相同的地图区域,这个区域在下一次屏幕重绘时,可以被重新利用。区域ABC是在下一次屏幕重绘中不被采用的区域,可以在这个3个区域上绘制上下一次需要重绘的区域A’B’C’。再将绘制好的四个区域拼合到屏幕的对应位置。
上图显示了x&y双轴滚动的卡马克卷轴的基本绘制原理,需要特别注意的是:在缓冲区的绘制顺序和在屏幕上拼合的顺序是完全相反的。
地图卷轴会分成8个方向滚动,初看起来比较复杂。但左上、右上、左下、右下的四个方向又可以看成是x以及y两个方向合成的。而不论地图是在x轴卷动还是在y轴卷动,无论是正方向还是负方向,其原理是完全相同。如此,算法的研究就变为如何绘制在x轴方向的卷动,其他方向的以此类推即可。
卡马克卷轴缓冲画法的一般步骤如下:
1. 初始化所有地图数据,并且全屏绘制初始的地图
2. 若人物移动,则调用摄像机算法,修正地图偏移量
3. 地图偏移量不满足地图的边界条件,就重绘缓冲区
4. 重绘缓冲区
3.2类CarMapBuffer设计
字段定义
/** 缓冲区宽高,命名方式为:Carmack width or height */
private final int carWidth, carHeight;
/** 缓冲区宽的图块数,与高的图块数,命名方式为:Carmack title width or height */
private final int carTitleWidth, carTitleHeight;
/** 屏幕宽高命名方式为:screen width or height */
private final int scrWidth, scrHeight;
/** 缓冲切割线,命名方式为:Carmack x or y */
private int carx, cary;
/** 地图在缓冲区的X 、Y偏移量,命名方式为:map offset x or y */
private int mapOffx, mapOffy;
/** 缓冲区,命名方式为:Carmack buffer */
privateImage carBuffer;
/** 缓冲区画笔,命名方式为:Carmack Graphics */
privateGraphics carGp;
/** 缓冲区增大的大小(上下大小是一样的) *///使用这个量让缓冲区比屏幕大一些
private final int buffSize;
/** 图片宽度的所切割的图块数量。 */
private int imageTitleWidth;
// ***********************************************************
// interim 临时
/** 地图图片 */
privateImage mapImage;
/** 地图数组 */
private byte mapArray[][];
/** 图块大小,宽高一致 */
private int titleSize;
/** 图块的宽度数量,与高度数量 */
private int titleW, titleH;
/** 地图的宽高 *///偏移量的最远位置,用来修正mapOffx, mapOffy
private int mapLastx, mapLasty;
方法定义
CarMapBuffer(int, int, int, int)构造器
CarMapBuffer(int, int, int)构造器的代理
setMap(Image, byte[][])设置地图参数
initBuffer()初始化绘制地图
scroll(int, int)卷动地图算法
updateBuffer(int, int)绘制缓冲区
getIndexCarX()获得切割线所在的图块索引X
getIndexCarY()获得切割线所在的图块索引Y
getBufferCarX()获得切割线在Buffer中的X位置
getBufferCarY()获得切割线在Buffer中的Y位置
getIndexBuffLastX()获得缓冲区后面的X索引
getIndexBuffLastY()获得缓冲区后面的Y索引
getTitleHeight()获得当前要绘制的图块高度的数量
getTitelWidth()获得当前要绘制的图块宽度的数量
copyBufferX(int, int, int, int, int)由于x方向卷动造成的重绘
copyBufferY(int, int, int, int, int)由于y方向卷动造成的重绘
getMapX(int, int) 获得地图图片的X坐标偏移
getMapY(int, int) 获得地图图片的Y坐标偏移
paint(Graphics, int, int)将缓冲区的内容分成4块依次拼合到屏幕上
drawBuffer(Graphics, int, int)绘制缓冲区方法
drawRegion(Graphics, Image, int, int, int, int, int, int, int, int)封装的drawRegion()方法
getGraphics()获得缓冲区画笔
3.3步骤一的实现
初始化所有地图数据,并且全屏绘制初始的地图
3.4步骤二、三的实现
若人物移动,则调用摄像机算法,修正地图偏移量,若偏移量在[0,maplast]移动范围内移动,则有可能发生重绘
/**
* 卷轴滚动
* @param x X轴滚动
* @param y Y轴滚动
*/
public voidscroll(int x,int y) {
x += mapOffx;
y += mapOffy;
//*************************************************
//边界检测
if (x< 0 || y < 0)
return;
if (x> mapLastx) {
mapOffx = mapLastx;
return;
}
if (y> mapLasty) {
mapOffy = mapLasty;
return;
}
updateBuffer(x, y);//若在[0,maplast]移动范围内移动,则有可能发生重绘
3.5步骤四的实现
重绘缓冲区,地图的x方向卷动会造成列方向上的重绘(调用copyBufferX()方法),地图的y方向上的卷动会造成行方向上的重绘(调用copyBufferY()方法)。updateBuffer()方法用于针对不同的四个方向上的卷动进行copyBuffer()参数的初始化。
/**
* 更新缓冲区
* @param x 缓冲区新的地图X坐标
* @param y 缓冲区新的地图Y坐标
*/
private voidupdateBuffer(int x,int y) {
mapOffx =x;//确定了地图移动,那么记录新的偏移位置
mapOffy =y;
//右移 x轴正向
if (x> carx + buffSize) {//因为缓冲区比屏幕大了一些,所以还需要判断当前缓冲区的内容是否够用
intindexMapLastX = getIndexBuffLastX();
if(indexMapLastX < titleW) {
copyBufferX(indexMapLastX,getIndexCarY(), getTitleHeight(),
getBufferCarX(),getBufferCarY());
carx +=titleSize;
}
}
//左移 x轴负向
if (x< carx) {
carx -=titleSize;
copyBufferX(getIndexCarX(),getIndexCarY(), getTitleHeight(),
getBufferCarX(),getBufferCarY());
}
//下移 y轴正向
if (y> cary + buffSize) {
intindexMapLastY = getIndexBuffLastY();
if(indexMapLastY < titleH) {
copyBufferY(getIndexCarX(), indexMapLastY,getTitelWidth(),
getBufferCarX(),getBufferCarY());
cary +=titleSize;
}
}
//上移 y轴负向
if (y< cary) {
cary -=titleSize;
copyBufferY(getIndexCarX(),getIndexCarY(), getTitelWidth(),
getBufferCarX(),getBufferCarY());
}
}
重绘缓冲区的具体方法,该方法涉及到大量的坐标运算,而且由于卡马克点的存在经常会分成两个区域分两次进行重绘。见下图:
下面以x方向卷动为例举例
/**
* 由于x方向卷动造成的重绘
* @param indexMapx 重绘区域左上角的title的j号
* @param indexMapy 重绘区域左上角的title的i号
* @param titleHeight 重绘区域的高度title数,由这个量来控制两次重绘的区域高度
* @param destx 重绘区域在缓冲区的相对位置x
* @param desty 重绘区域在缓冲区的相对位置y
*/
private voidcopyBufferX(intindexMapx, intindexMapy, inttitleHeight,
intdestx, intdesty) {
intmapImagex, mapImagey, vy;
//绘制缓冲区下面部分 和地图的上面部分对应
for (int j= 0; j < titleHeight; j++) {
mapImagex = getMapX(indexMapy + j,indexMapx);
mapImagey = getMapY(indexMapy + j,indexMapx);
vy = j * titleSize +desty;
carGp.setClip(destx,vy, titleSize, titleSize);
carGp.drawImage(mapImage,destx - mapImagex, vy - mapImagey, 0);
}
//绘制缓冲区上面部分 和地图的下面部分对应
for (int k= titleHeight; k < carTitleHeight;k++) {
mapImagex = getMapX(indexMapy + k,indexMapx);
mapImagey = getMapY(indexMapy + k,indexMapx);
vy = (k - titleHeight) * titleSize;
carGp.setClip(destx,vy, titleSize, titleSize);
carGp.drawImage(mapImage,destx - mapImagex, vy - mapImagey, 0);
}
3.6步骤五的实现
将后台缓冲区的四个子区按照顺序画到屏幕上,这一步很简单,原理之前已经详细讲述过,不再赘述。
/**
* 绘画缓冲
* @param g 屏幕画笔
* @param x 绘画X点
* @param y 绘画Y点
*/
public voidpaint(Graphics g, int x,int y) {
//地图在缓冲中的坐标
inttempx = mapOffx % carWidth;
inttempy = mapOffy % carHeight;
//切割线右下角的宽与高
intrightWidth = carWidth -tempx;
intrightHeight = carHeight -tempy;
//画左上
drawRegion(g, carBuffer,tempx, tempy, rightWidth, rightHeight, 0, x,
y, 0);
//画右上
drawRegion(g, carBuffer,0, tempy, scrWidth -rightWidth, rightHeight,
0, x + rightWidth, y, 0);
//画左下
drawRegion(g, carBuffer,tempx, 0, rightWidth, scrHeight -rightHeight,
0, x, y + rightHeight, 0);
//画右下
drawRegion(g, carBuffer,0, 0, scrWidth -rightWidth, scrHeight
- rightHeight, 0, x +rightWidth, y + rightHeight, 0);
}
下面以手机游戏《FC恶魔城手机复刻版》为例实际检测一下卡马克卷轴的绘制优化效果。测试环境如下:
硬件环境: Intel(R)Pentium(R) Dual E2140 @ 1.60GHz 1.60GHz , 2.00G内存 , NVIDIA GeForce 8500 GT
软件环境:windowsXpSp2,eclipse3.2+eclipseME1.5插件,wtk2.2,jdk1.4.2,使用DefaultColorPhone模拟器
代码环境:Title大小8pixels,绘制区域Title数量30*12,普通绘制方法下,每帧绘制Title的循环次数30*12=360次;卡马克卷轴缓冲绘制方法下,每次更新缓冲区时,绘制Title的循环次数12~24次
测试内容以及测试方法为:让主角移动直到走过第一关的第一个小场景为结束。使用wtk的事件查看器和内存监控器来进行系统后台数据的分析。数据分为两个组,一个组使用卡马克卷轴优化算法,一个组使用普通的地图绘制算法。比较两组数据得出结论。
首先是卡马克卷轴绘制地图组的数据截图
然后是对照组普通绘制地图时的数据截图
从上面的数据可以看出,使用了卡马克卷轴算法后,绘制地图的方法所占用的CPU时间的比率大幅度下降,从55.83%下降到了9.04%,降幅达到46.79%,效果十分明显。两种情况下的方法调用次数则差不多,分别是184次和182次。
同样的,首先是卡马克卷轴绘制地图组的数据截图
然后是对照组普通绘制地图时的数据截图
从上面看出两者的最大内存消耗基本一致,但是由于卡马克卷轴涉及到很多坐标运算,所以在当前内存消耗中,有时候会多占用一些内存资源。
采用手机为nokia6288
两组都能正常运行。普通绘制下,地图卷动时屏幕暴闪,严重影响游戏感受。卡马克卷轴缓冲绘制下,地图卷动时屏幕仅有很轻微闪耀(据多方了解,应为6288手机的性能所限)。
结论
卡马克算法是在进行2D游戏地图卷动的算法中内存痕迹最小、效率适中的算法之一.其核心的思想就是把地图卷动过程中移出屏幕(不需要在显示的部分)所占用的buffer区域,绘制上新的需要图块,在往真实屏幕上绘制的时候,通过四次绘制buffer把完整的地图重现.
我们在实际的代码编写中按以下的方式进行.根据上面的基本思想,把地图分为四个块(十字形的将buffer划分为四块),用carx和cary来记录十字分区的中心坐标(相对于buffer的坐标,我把这个点叫卡马克分区点).当地图向右移动的时候这时把卡马克分区点的坐标X方向加上一个tile的width,然后在从现在的卡马克分区点的坐标Y开始绘制提取出来的tileID对应的图象,注意是从当前的卡马克分区点的坐标Y开始绘制,当超出carHeight时在从0开始绘制直到结束,这样就完成了在水平方向上的更新.还有就是在水平移动卡马克分区点的时候是在buffer中循环的,也就是从0到carWidth的一个循环过程,Y方向上完全一致.最后是绘制过程,也就是将四个分区绘制出来,口诀就是左变右上变下,掌握好卡马克算法对手游开发很有帮助的.
致谢
指导老师:老郑
j2megame.cn网站站长:爱飞翔
j2megame.cn论坛版友:hubluesky
参考文献
《DOOM启示录》
j2megame.cn论坛转载和原创的卡马克卷轴技术文章
参考代码:卡马克卷轴的演示代码(hubluesky提供)
摘自《DOOM启示录》第三章——侵犯版权的戴夫
当汤姆在卡马克身边坐下时,卡马克给他看了他正在做的东西,一种可以把动画应用于游戏背景的技术。计算机屏幕是由许多像素组成的,一组像素则可以构成一个图素(tile),美工们首先用像素来画出图素,然后再用创作好的图素来搭建整个世界,这就像是给厨房铺瓷砖(tile)。而卡马克刚找到了一种可以让这块瓷砖上的图像动起来的办法,“而且,”他对汤姆说,“我还可以在你们接触到某块图素的时候激活一些事件”。
“这个东西做起来简单么?”汤姆问道。
“当然,嗯”卡马克回答说,他只需要加一些代码来处理这个事件就行了。汤姆清楚地知道这会是多么惊人,因为《超级马里奥》之类的游戏靠的就是动画背景,而且,当玩家跳起来顶到某块闪烁着的砖头时,会触发一些事件,譬如落下若干金币。想到这里,汤姆来了精神。而卡马克要给他看的还不止这些。
卡马克在键盘上敲了几下,向汤姆展示了他的另一项成就:平滑的卷轴效果。这种由《防御者》和《超级马里奥》带来的技术,能够在玩家走到屏幕边缘的时候通过背景卷动让玩家体验到一个连续的世界。经过几个夜晚的尝试,卡马克终于在PC上也实现了这种效果。和往常一样,他用了一种独特的手段。很多人会一上来就试图走捷径,卡马克觉得这不是解决问题的方法。他一开始先试了最直接的途径,在整个屏幕上重新绘制图像,但行不通,因为那时的PC实在太慢了。然后他尝试着优化——可以通过多使用一些内存来换取性能的提高吗?他试了几次,发现这也是死路一条。
最终卡马克冷静了下来,从头开始考虑,“我要的是什么?——我要在玩家穿过游戏的时候图像能够平滑地移动。”他回想起他以前做的《墓窖》,在那个游戏里,当玩家走到屏幕上地牢边缘时,他会让背景做一次很大的调整,这种基于图素的卷动在当时是很常见的技术,而他现在要的是基于像素的卷动,哪怕玩家只移动了一点点,背景也要随之调整。问题是如此频繁地重绘远远超出了当时PC机的能力,但卡马克很快找到了突破口。
如果,卡马克想道,不是每次重绘整个屏幕,而是只重绘那些真正改变了的部分,会怎么样呢?那样的话,卷轴效果执行起来会快很多。他盯着屏幕,假想着一个由蓝天白云组成的背景,玩家跑向屏幕右边,如果它跑得足够远,那些云朵将会被它逐渐抛在身后,直到从屏幕上消失,尽管改变的只是一片白云,大部分蓝色的区域没有变化,但计算机不知道这些细节,它不知道其实有更快捷的办法,它只会笨拙地重绘每一个组成蓝天的像素,从屏幕的左上方开始,一个像素一个像素地往右,再一行行往下,直到完成整个屏幕。接下来卡马克就干了一件漂亮的事情,使得效率得到极大的提高。他写了一段代码,用来哄骗计算机,让它觉得譬如左数第七块图素其实是屏幕上的第一块,这样的话,卡马克就可以让计算机在他指定的地方开始绘制图像,计算机就可以直接绘制白云,而不需要再一个像素一个像素地填充蓝色的天空。为了确保玩家移动时感觉到平滑的效果,卡马克还想出了一个小技巧:让计算机在屏幕右边以外的地方画一块额外的天空,这其实是画在计算机的显存里,但当它们真正进入屏幕的时候,它就不再需要重新绘制,而是直接从显存里反映到屏幕上。卡马克把这个过程叫做“图素自动刷新”。