|
8.4 优化加速游戏程序
上一节中的打砖块源代码介绍得很简略,没有详细讲解。读者只要耐心地阅读思考,并不难全部弄懂。本节想要讨论的是游戏程序的另一个方面:通过优化代码来加速游戏程序。
如果读者运行上一节的游戏程序,就会发现这个游戏程序能够正常运行,但是会明显地感觉到这个程序运行得并不流畅。问题出现在哪里呢?
仔细思考就会发现,主要原因是在主循环中每一次都绘制地图!虽然单独绘制一次地图并不需要花费太多时间,但是当程序非常频繁地绘制地图时,积累起来花费的时间就非常可观了,从而导致程序运行时并不流畅。应该对程序进行优化,以加快绘制地图的速度。
那么如何加快绘制地图的速度呢?关键的是:在绘制地图时,不要重复地绘制那些没有发生变化的内容,只需要绘制那些发生了变化的内容就可以了!显然,地图四周的围墙、没有被打中的砖块和大部分空白处都不需要重绘。需要重绘的是这些:(1)被打中的砖块需要修改绘制为空白;(2)弹球移动时的原位置和新位置需要重绘;(3)挡板移动时,只需要绘制左右两端发生变化的格点即可。
为此,需要改变程序中涉及到地图绘制的功能。需要写一个函数,用于绘制地图中的单个或部分格点。
在此函数中,需要先设计出地图格点与屏幕坐标的映射关系,然后在函数中才能把地图上的格点准确地定位到屏幕上。
在这个程序中,地图格点与屏幕坐标的映射关系相当简单:地图是绘制在屏幕的左上角,每个地图格点都绘制为一个全角字符,因此坐标为(x, y)的地图格点(即二维数组元素 map[y][x])对应到屏幕位置坐标 (x*2, y)。而在一般情况下,还需要把地图格点的二维坐标整体进行偏移。假设X方向的偏移量为 X0,Y 方向的偏移量为 Y0,于是坐标为 (x, y) 的地图格点就对应到屏幕坐标 (X0 + x*2, Y0 + y )。于是定义外部变量 X0 和 Y0,并写出绘制单个地图格点的函数如下:
int X0 = 10, Y0 = 0; //地图左上角坐标原点(地图整体偏移量)
void drawxy(int x, int y, char val) { //根据val的值绘制地图格点(x, y)
gotoxy(X0 + x * 2, Y0 + y);
if (val == ' ') //空白
printf(" ");
else if (val == '#') //围墙
printf("█");
else if (val == 'B') //砖块
printf("□");
else if (val == '=') //挡板
printf("▓");
else if (val == 'O') //弹球
printf("●");
else if (val == 'o') //失败时的弹球
printf("○");
else
printf("×");
}
(注意,在程序中作其它输出时,对于坐标也要作类似的处理:分别加上 X0 和 Y0 。)
上面函数的第三个参数是写成 val,而不是在函数中直接写成 map[y][x]。这样能使调用时有更大的自由度。有了这个函数之后,在程序中可以这样调用该函数:
drawxy(x, y, map[y][x]);
或
drawxy(x, y, ' ');
除此之外,有时候需要完整地绘制整个地图,所以写出 drawMap 函数如下:
void drawMap() { //绘制整个地图
int iy, ix;
for (iy = 0; iy < HT; iy++) { //行
for (ix = 0; ix < WD; ix++) { //列
drawxy(ix, iy, map[iy][ix]);
}
}
}
有了上面的 drawxy 函数之后,程序中只需要在创建新地图时(游戏刚开始时,以及一局胜利或失败之后)调用 drawMap 函数,其它地方都是通过调用 drawxy 函数来绘制部分地图元素即可。所以这样就大大减少了绘制地图所需要输出的内容。
例如,程序主循环开始部分要写成这样:
srand(time(0)); //初始化随机数种子
initMap(bricks); //初始化地图
initBarBall(); //初始化挡板和弹球
drawMap(); //绘制地图
while (true) { //游戏主循环
//drawMap(); //绘制地图
//showMsg(lives, bricks, score); //输出信息
if ((lives > 0 && bricks <= 0) || lives <= 0) { //胜利或失败
gotoxy(X0, Y0 + HT);
if (lives > 0)
cout << &#34;\n击碎所有砖块,您胜利了!&#34;;
else { //失败
cout << &#34;\n本局失败,还有砖块尚未击中。 &#34;;
score = 0; //本局失败,得分清0
}
if (wantMore()) {//是否继续
initMap(bricks); //初始化地图
initBarBall(); //初始化挡板和弹球
lives = 3;
drawMap(); //绘制地图
showMsg(lives, bricks, score, 2);
continue;
} else
break;
}
//既非胜利也非失败,则游戏正在进行
……
} //主循环结束
相应地,在挡板移动时需要调用 drawxy 函数来重新绘制挡板:
if ((key == 75 || key == &#39;a&#39; || key == &#39;A&#39;) && barx > 1) { //left
map[bary][barx + barwd] = &#39; &#39;; //空白
drawxy(barx + barwd, bary, &#39; &#39;);
barx--;
map[bary][barx] = &#39;=&#39;; //挡板
drawxy(barx, bary, &#39;=&#39;);
if (vx == 0 && vy == 0) { //开局时尚未发球,则球随之挡板移动
map[y][x] = &#39; &#39;;
drawxy(x, y, &#39; &#39;);
x--;
map[y][x] = &#39;O&#39;;
drawxy(x, y, &#39;O&#39;);
}
}
if ((key==77 || key==&#39;d&#39; || key==&#39;D&#39;) && (barx + barwd < WD - 2)) { //right
map[bary][barx] = &#39; &#39;; //空白
drawxy(barx, bary, &#39; &#39;);
barx++;
map[bary][barx + barwd] = &#39;=&#39;; //挡板
drawxy(barx + barwd, bary, &#39;=&#39;);
if (vx == 0 && vy == 0) { //开局时尚未发球,则球随之挡板移动
map[y][x] = &#39; &#39;;
drawxy(x, y, &#39; &#39;);
x++;
map[y][x] = &#39;O&#39;;
drawxy(x, y, &#39;O&#39;);
}
}
}
注意,挡板移动时根本不需要重绘整个挡板,而只需要绘制左右两个格点即可。
在弹球与砖块发生碰撞时,砖块需要重绘。例如判断横向运动与砖块相碰的代码改成这样(竖向或斜向碰撞也要相应修改。略):
if (map[y][x + vx] == &#39;B&#39;) { //横向运动方向为砖块
map[y][x + vx] = &#39; &#39;; //打碎砖块变为空白
drawxy(x + vx, y, &#39; &#39;); //重绘原有砖块位置为空白
hits++; //横向碰撞记数
vx = -vx; //横向反弹
cout << &#39;\a&#39;; //响铃
}
当弹球的坐标位置发生变化时,也要通过drawxy重绘弹球:
if (vx != 0 || vy != 0) {
map[y][x] = &#39; &#39;; //弹球原位置变为空
drawxy(x, y, &#39; &#39;); //清除原位置的弹球
}
if ((vy < 0 && y > 1) || (vy > 0 && y < HT - 1))
y = y + vy;
if ((vx < 0 && x > 1) || (vx > 0 && x < WD - 2))
x = x + vx;
if (vx != 0 || vy != 0) {
map[y][x] = &#39;O&#39;;
drawxy(x, y, &#39;O&#39;); //在新位置绘制弹球
}
由此可见,在程序中需要在多处大量地使用 drawxy 函数,使得代码变得有点复杂了,但是所做的这一切都是值得的!因为这样每次都只绘制很少量的地图元素,大大地减少了程序运行时的输出内容,可以优化程序的运行流畅度。
读者可以从本文作者的 Gitee 开源程序库中(https://gitee.com/devcpp/cgames)下载这个完整的源程序,文件名称为 cgame8(bricks)v2.cpp。
继续阅读:8.5 数据与地图分离 |
|