IE盒子

帖子
查看: 86|回复: 1

用C/C++编写小游戏:8.4 优化加速游戏程序

[复制链接]

4

主题

5

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2022-12-8 03:27:27 | 显示全部楼层 |阅读模式
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 << "\n击碎所有砖块,您胜利了!";
            else { //失败
                cout << "\n本局失败,还有砖块尚未击中。 ";
                score = 0;  //本局失败,得分清0
            }
            if (wantMore()) {//是否继续
                initMap(bricks);    //初始化地图
                initBarBall();      //初始化挡板和弹球
                lives = 3;
                drawMap();    //绘制地图
                showMsg(lives, bricks, score, 2);
                continue;
            } else
                break;
        }

        //既非胜利也非失败,则游戏正在进行
……
}  //主循环结束
相应地,在挡板移动时需要调用 drawxy 函数来重新绘制挡板:
if ((key == 75 || key == 'a' || key == 'A') && barx > 1) { //left
    map[bary][barx + barwd] = ' '; //空白
    drawxy(barx + barwd, bary, ' ');
    barx--;
    map[bary][barx] = '='; //挡板
    drawxy(barx, bary, '=');
    if (vx == 0 && vy == 0) { //开局时尚未发球,则球随之挡板移动
        map[y][x] = ' ';
        drawxy(x, y, ' ');
        x--;
        map[y][x] = 'O';
        drawxy(x, y, 'O');
    }
}
if ((key==77 || key=='d' || key=='D') && (barx + barwd < WD - 2)) { //right
    map[bary][barx] = ' '; //空白
    drawxy(barx, bary, ' ');
    barx++;
    map[bary][barx + barwd] = '='; //挡板
    drawxy(barx + barwd, bary, '=');
        if (vx == 0 && vy == 0) { //开局时尚未发球,则球随之挡板移动
           map[y][x] = ' ';
          drawxy(x, y, ' ');
          x++;
          map[y][x] = 'O';
          drawxy(x, y, 'O');
       }
    }
}
注意,挡板移动时根本不需要重绘整个挡板,而只需要绘制左右两个格点即可。
在弹球与砖块发生碰撞时,砖块需要重绘。例如判断横向运动与砖块相碰的代码改成这样(竖向或斜向碰撞也要相应修改。略):
if (map[y][x + vx] == 'B') {  //横向运动方向为砖块
    map[y][x + vx] = ' ';     //打碎砖块变为空白
    drawxy(x + vx, y, ' ');  //重绘原有砖块位置为空白
    hits++;  //横向碰撞记数
    vx = -vx;      //横向反弹
    cout << '\a';  //响铃
}
当弹球的坐标位置发生变化时,也要通过drawxy重绘弹球:
if (vx != 0 || vy != 0) {
    map[y][x] = ' '; //弹球原位置变为空
    drawxy(x, y, ' '); //清除原位置的弹球
}
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] = 'O';
    drawxy(x, y, 'O'); //在新位置绘制弹球
}
由此可见,在程序中需要在多处大量地使用 drawxy 函数,使得代码变得有点复杂了,但是所做的这一切都是值得的!因为这样每次都只绘制很少量的地图元素,大大地减少了程序运行时的输出内容,可以优化程序的运行流畅度。
读者可以从本文作者的 Gitee 开源程序库中(https://gitee.com/devcpp/cgames)下载这个完整的源程序,文件名称为 cgame8(bricks)v2.cpp。

继续阅读:8.5  数据与地图分离
回复

举报

3

主题

14

帖子

24

积分

新手上路

Rank: 1

积分
24
发表于 2025-3-28 08:50:02 | 显示全部楼层
我也来顶一下..
回复

举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表