C++实现不闪屏的字符游戏--贪吃蛇

C++实现不闪屏的字符游戏--贪吃蛇一 写在前面的废话 贪吃蛇游戏早在大一刚学编程的时候就写过了 虽然那时候有各种 bug 最近有个同学问我不闪屏的贪吃蛇怎么写 我学习了一位大佬的博客 动手做了一个不闪屏的版本 二 实现方法 首先我们要弄清楚闪屏的原因 因为贪吃蛇是一直在动的 我们就需要不停地输出 当然我们不能一幅图一幅图的输出

大家好,我是讯享网,很高兴认识大家。

一.写在前面的废话
贪吃蛇游戏早在大一刚学编程的时候就写过了,虽然那时候有各种bug…最近有个同学问我不闪屏的贪吃蛇怎么写,我学习了一位大佬的博客,动手做了一个不闪屏的版本。

二.实现方法
首先我们要弄清楚闪屏的原因。因为贪吃蛇是一直在动的,我们就需要不停地输出。当然我们不能一幅图一幅图的输出,也就是说我们要让贪吃蛇看起来在一个框框内移动。普遍的实现方法是,用清屏(system(“cls”))和输出(cout)交替进行。但是,如果我们的地图比较大,就可能出现比较明显的闪屏(尤其是下半部分地图,闪的人眼睛疼)。
比方说我们的地图边界是“#”,地图的第一个“#”和最后一个“#”的输出时间是有差别的,如果显示完所有“#”马上擦除,再来一次,则显示缓冲区不包含所有“#”的状态居多,这就导致了闪屏。
解决办法是,使用两个缓冲区,显示一个缓冲区的同时,将要输出的数据写入另一个缓冲区,这样交替进行就可以无缝衔接。
下面放代码和代码说明。

三. 代码实现

#include<iostream> #include<time.h> #include<conio.h> #include<Windows.h> #include<string.h> using namespace std; #define H 21 #define W 41//20行40列的地图 HANDLE hOutput, hOutBuf;//控制台屏幕缓冲区句柄 HANDLE *houtpoint;//显示指针 COORD coord = { 0,0 };//双缓冲处理显示 DWORD bytes = 0; bool showCircle = false;//判断显示哪个缓冲区 int snakeHeadX = 1; int snakeHeadY = 2;//蛇头'@'的初始坐标 char direction = 'd';//初始方向向右 bool gameRunning = true;//判断游戏是否结束 int foodX, foodY;//食物'$'的坐标 int snakeLength = 1;//蛇身初始长度为1 int foodEaten = 0;//吃掉的食物数,用来计算游戏难度和分数。 int score = 0; char scoreArray[1000];//分数 struct snakeBody {//蛇身结构体,存蛇身'*'的坐标 int x[H*W]; int y[H*W]; }snake; char Map[H][W]= { "", "#*@ #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "", }; void gameover() {//控制游戏结束 gameRunning = false; showCircle = !showCircle; if (showCircle) { houtpoint = &hOutput; } else { houtpoint = &hOutBuf; } memset(Map, 255, sizeof(Map));//将地图清零 for (int i = 0; i < H; ++i) { coord.X = 44;//以(45,5)为起点 coord.Y = i + 5; WriteConsoleOutputCharacterA(*houtpoint, (char*)Map[i], W, coord, &bytes); } //打印"Game Over!" coord.X = 44 + H / 2 - 3; coord.Y = 5; WriteConsoleOutputCharacterA(*houtpoint, "GAME OVER", 9, coord, &bytes); //设置新的缓冲区为活动显示缓冲 SetConsoleActiveScreenBuffer(*houtpoint); } void display() {//显示游戏画面,可以加上游戏等级,分数等。 if (gameRunning) { showCircle = !showCircle; if (showCircle) { houtpoint = &hOutput; } else { houtpoint = &hOutBuf; } coord.X = 51; coord.Y = 3; score = foodEaten * 100; sprintf_s(scoreArray, "Score:%d", score);//格式 WriteConsoleOutputCharacterA(*houtpoint, scoreArray, strlen(scoreArray), coord, &bytes); for (int i = 0; i < H; ++i) { coord.X = 44;//以(45,5)为起点 coord.Y = i + 5; WriteConsoleOutputCharacterA(*houtpoint, (char*)Map[i], W, coord, &bytes); } //设置新的缓冲区为活动显示缓冲 SetConsoleActiveScreenBuffer(*houtpoint); } Sleep(200-foodEaten*10);//吃的越多速度越快,增加难度 } void setMap() {//更新蛇、食物的坐标 Map[snakeHeadX][snakeHeadY] = '@'; for (int i = 0; i < snakeLength; ++i) { Map[snake.x[i]][snake.y[i]] = '*'; } Map[foodX][foodY] = '$'; } void spawnFood() {//设置食物坐标 while (1) { srand((unsigned)time(NULL)); //初始化随机数 foodX = rand() % 20; foodY = rand() % 40; if (Map[foodX][foodY] != '@'&&Map[foodX][foodY] != '*'&&Map[foodX][foodY] != '#')//不重合 break; } } void snakeMove() {//蛇移动 int pre_snakeHeadX = snakeHeadX;//蛇头的上一个坐标 int pre_snakeHeadY = snakeHeadY; if (_kbhit()) {//是否按下键盘 char tempControl = _getch(); switch (tempControl) { case 'w': if (direction == 's') break; direction = tempControl; break; case 'a': if (direction == 'd') break; direction = tempControl; break; case 's': if (direction == 'w') break; direction = tempControl; break; case 'd': if (direction == 'a') break; direction = tempControl; break; } } switch (direction) { case 'a': snakeHeadY--; break; case 'w': snakeHeadX--; break; case 's': snakeHeadX++; break; case 'd': snakeHeadY++; break; } //判断游戏是否结束 if (Map[snakeHeadX][snakeHeadY] == '#' || Map[snakeHeadX][snakeHeadY] == '*') gameover(); //判断是否吃到食物 if (Map[snakeHeadX][snakeHeadY] == '$') { foodEaten++; snake.x[snakeLength] = pre_snakeHeadX; snake.y[snakeLength] = pre_snakeHeadY; snakeLength++; spawnFood(); return; } //更新蛇身坐标 Map[snake.x[0]][snake.y[0]] = ' '; for (int i = 0; i < snakeLength - 1; ++i) { snake.x[i] = snake.x[i + 1]; snake.y[i] = snake.y[i + 1]; } snake.x[snakeLength - 1] = pre_snakeHeadX; snake.y[snakeLength - 1] = pre_snakeHeadY; } int main() { //创建新的控制台缓冲区 hOutBuf = CreateConsoleScreenBuffer( GENERIC_WRITE,//定义进程可以往缓冲区写数据 FILE_SHARE_WRITE,//定义缓冲区可共享写权限 NULL, CONSOLE_TEXTMODE_BUFFER, NULL ); hOutput = CreateConsoleScreenBuffer( GENERIC_WRITE,//定义进程可以往缓冲区写数据 FILE_SHARE_WRITE,//定义缓冲区可共享写权限 NULL, CONSOLE_TEXTMODE_BUFFER, NULL ); //隐藏两个缓冲区的光标 CONSOLE_CURSOR_INFO cci; cci.bVisible = 0; cci.dwSize = 1; SetConsoleCursorInfo(hOutput, &cci); SetConsoleCursorInfo(hOutBuf, &cci); //游戏开始 spawnFood(); snake.x[0] = 1; snake.y[0] = 1;//蛇身初始位置 setMap(); display(); while (gameRunning) { snakeMove(); setMap(); display(); } _getch();//等待游戏结束 } 

讯享网

四. 代码说明
1.几乎所有变量都是全局变量,也有用类实现的,但是我觉得全局变量更简洁一些。
2.字符的含义分别是:#代表墙,撞上就game over;@代表蛇头;*代表蛇身,初始长度为1;$代表食物;可走的路为’ '。
3.我在声明地图的时候就做了初始化,这样每次设置地图只需对蛇和食物的坐标作修改。正如图所示,游戏开始时贪吃蛇的蛇身长度为1,蛇头向右边前进。
4.整个程序由 控制游戏结束、打印地图、设置地图、产生食物、蛇移动5个模块组成,分别写成5个函数。
5.我的x坐标是放在二维数组前一位的,y坐标是放在二维数组后一位的,也就是从上往下数是x坐标,从左往右数是y坐标。
6.打印分数的时候用到了sprintf函数,作用是把int型的score格式化后存入字符串scoreArray中。顺便一提,如果用vs运行代码,应当写成sprintf_s。
7.蛇身的坐标变化过程是这样的:我们首先用一个结构体snake(含有数组x[]和y[])来保存蛇身的坐标,用一个整型变量snakeLength保存蛇身的长度。当蛇吃了一个食物(蛇头的位置变成了食物的位置),蛇身长度要加一,我们将新增加的蛇身加到蛇头的上一个位置,这样其他的蛇身坐标就不用变化。新的蛇身坐标存入数组:


讯享网

讯享网snake.x[snakeLength] = pre_snakeHeadX; snake.y[snakeLength] = pre_snakeHeadY; snakeLength++; 

也就是说,新加入的蛇身坐标排在数组的后面。那么,怎么实现蛇身的移动呢?我们考虑后一个蛇身的坐标变成前一个蛇身的坐标,最前面的蛇身的坐标变成蛇头的上一个坐标:

 for (int i = 0; i < snakeLength - 1; ++i) { snake.x[i] = snake.x[i + 1]; snake.y[i] = snake.y[i + 1]; } snake.x[snakeLength - 1] = pre_snakeHeadX; snake.y[snakeLength - 1] = pre_snakeHeadY; 

五. 增加游戏趣味性的设计
贪吃蛇的形状可以更好看。比如在本博客开始提到的大佬,他利用ASCII做了个很好看的贪吃蛇游戏。
在本贪吃蛇程序中,我的设计是吃的食物越多,贪吃蛇的速度越快,每吃一个食物速度就比初始速度快5%(通过减少Sleep的时间实现)。我还显示了分数,一个食物100分。之前我有考虑增加排行榜,初步设想是采用文件读写,文件保留最高的10个分数以及时间,实现方法应该不难,但由于作业多(可能是懒吧)我还没有动手去实现…
如果大家有什么问题或者建议,欢迎提出!

小讯
上一篇 2025-02-07 13:42
下一篇 2025-02-19 12:01

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/128534.html