C++贪吃蛇 最简单实现

在这里要实现一个简单的贪吃🐍程序。
所谓的最简单,指只有基本功能,界面傻瓜,屏幕易闪,🐍的数组静态分配,稳定性差(就是只能按上下左右,按键盘其他键之类很可能break)等,分别可以通过借用Qt界面编程或Windows API(本人暂无尝试),缓冲区和设立动态数组等解决。(过程中一一讨论问题)

按照面向过程的思想来想,流程就大概搭建边框—>开辟蛇的数组并画出蛇—>随机生成第一个食物—>保持读取更改蛇头方向指令,同时每隔一段时间让蛇按照某方向前进(通过蛇所在的坐标更新实现),同时更新显示食物位置,同时检查:蛇头是否碰到边界、碰到自己(这样游戏结束);是否碰到食物(这样就更新蛇长—>加分—>判断加等级、酌情加快蛇前进速度。)
按照面向对象(OOP)的思想,应该搞一个类似五子棋游戏中的chessboard类(就像前端,也不完全是),用来实现基础界面显示相关功能;还要有一个搞动作的类继承chessboard类并在它的基础上搞蛇的运动。将两个类(棋盘类和蛇类)的架构建成头文件,后者继承前者;在棋盘类中,要实现的有构建框架和分数显示、随机生成食物(这不属于蛇的动作,搞到棋盘类去算了)、蛇每动一次就用cls清空屏幕、重新显示当前结果(这时运行时屏幕乱闪的根源,很不好,时间有限将就了。通过多设缓冲区可以解决);上述框架写作:(宏定义22列 22行 中间包400空格)

#pragma once
#define H1 22           //宏定义
#define W 22           //宏定义
class chessboard       // 定义了棋盘类
{
public:
	char qp[H1][W];	   //定义数组表示棋盘
	int i, j, x1, y1;  //定义棋盘用到的变量
	chessboard();  //定义构造函数,用于初始化棋盘
	void chessboard2();
	void food();       //定义成员函数,用于随机生成食物
	void prt(int grade, int score, int gamespeed); //更新屏幕显示:棋盘显示  等级 分数  游戏速度
};

chessboard类构造函数,就是画出基本的框架(四周用#圈成一圈中间是空格);food函数生成随机数、取余并减一(数组下标从0开始)。并且得到的数上棋盘应该是空格(食物不能安在蛇体上);用$表示食物。每隔规定的间隔时间(由game speed确定),系统调用一次prt。由于qp数组定义在chessboard类中,所以可以直接使用;而score、grade、game speed是它的继承类中定义的,所以调用时需传参。观察这个prt,发现它是从上往下一行行地刷的。所以,靠下的行的显示延迟时间大点,所以乱闪屏,比较晃眼。

#include<conio.h>
#include<windows.h>
#include"chessboard.h"
#include<time.h>
#include<iostream>
using namespace std;

chessboard::chessboard()
{
	for (i = 1; i <= H1 - 2; i++)
	{
		for (j = 1; j <= W - 2; j++)
		{
			qp[i][j] = ' ';//棋盘中间置零
		}
	}
	for (i = 0; i <= H1 - 1; i++)
	{
		qp[0][i] = qp[H1 - 1][i] = '#';//棋盘两边置#
	}
	for (i = 1; i <= H1 - 2; i++)
	{
		qp[i][0] = qp[i][W - 1] = '#';//棋盘两边置#
	}
	food(); //每次构造函数调用成员函数food生成食物
}

void chessboard::food()
{
	srand(time(0));      // 随机时间  srand生成随机数,需要输入随机种子,种子相同则随机数相同;为保证生成的随机数具有随机性应选取不同的随机种子,一般选取系统当前时间最为随机种子,因为系统时间在变化
						  //time(0)回去当前系统时间
	do
	{
		x1 = rand() % W - 1; // 随机位置
		y1 = rand() % H1 - 1;
	} while (qp[x1][y1] != ' '); //保证生成的食物在空白处
	qp[x1][y1] = '$'; //设置食物形状
}
void chessboard::prt(int grade, int score, int gamespeed)
{
	system("cls");//调用系统命令行cls清空屏幕,与cmd中命令行一致                                      //取消这一行                          //取消这一行
	cout << endl; //换行
	for (i = 0; i < H1; i++) //显示结果
	{
		cout << "\t";
		for (j = 0; j < W; j++)
			cout << qp[i][j] << ' ';
		if (i == 0) cout << "\tGrade:" << grade;
		if (i == 2) cout << "\tScore:" << score;
		if (i == 4) cout << "\tAutomatic forward";
		if (i == 5) cout << "\ttime interval:" << gamespeed << "ms";
		cout << endl;
	}
}

而🐍类实现蛇的移动。在蛇类,应该实现剩下所有内容:开辟蛇的数组并画出蛇、保持着准备读取更改蛇头方向的指令、如果没那指令就每隔一段时间让蛇按照某方向前进(通过蛇所在的坐标更新实现)、检查蛇头是否碰到边界、碰到自己(这样游戏结束);是否碰到食物(这样就更新蛇长—>加分—>判断加等级、酌情加快蛇前进速度。)

蛇头用@表示,蛇身用*表示。游戏伊始,蛇在左上角往右动,蛇长为4(那个定义的length不是蛇长);随机生成食物(一个位置);上下左右键控制蛇头摆动方向;蛇碰到食物,蛇长加一;蛇头一旦碰到边界或自身,游戏结束。大概如此。

下述程序,head是蛇头(初始化为3),tail是蛇尾(初始化为0);grade存放当前等级,score存放当前分数,game speed存放蛇前进一单位的时间间隔,time over控制自动向下运行还是手动更新运行。(说人话是手先按了上下左右time over就等于1,程序继续运行;没按上下左右等了一段间隔后,这时(clock() - start == gamespeed),程序跳出while继续运行)direction存放蛇头的方向,olddirection存放蛇尾的方向。

#pragma once
#include"chessboard.h"
#include<time.h>
#include<conio.h>
class snake:chessboard// 类继承
{
public:
	int zb[2][100]; //存放贪吃蛇坐标
	long start; //计时
	int head, tail, grade, score, gamespeed, length, timeover, x, y; //游戏内使用的量,描述在代码块外
	char direction;    //贪吃蛇前进方向,ASCII码数字显示方式
	char oldDirection; //方向更新时用到的临时方向
	snake(); //构造函数
	void move();//成员函数,表征贪吃蛇移动动作,每500ms更新一次
};

构造函数应该产生刚开始的蛇形。(左上角、朝右走、长度为4)方便看,这里专门用个cpp文件写构造函数。
注意,321倒计时写在这个程序里面。可以想象后面的主程序可以一开始就只构建一个蛇类(头文件、前面的类、(重载)函数等等都继承进去了),在自动调用构造函数时,先执行321倒计时,再调用其他的继承类把棋盘刻上去、把蛇画上去、让它动起来。321倒计时,就是每暂停1000个时间片(1ms)从3到1更新一次数字。

而后,需要定义贪吃蛇的起始位置。让qp[1][1]到qp[1][3](棋盘空表的左上角,qp上面已有,是棋盘数组)显示成*表示蛇身,qp[1][4]表示用@表示蛇头。这样在屏幕上就会显示出来第一个状态的贪吃蛇。另一方面,存放贪吃蛇坐标的数组也应该更新。

#include<iostream>
#include"snake.h"

using namespace std;
snake::snake()
{
	cout << "\n\n\t\tThe game is about to begin!" << endl;
	for (i = 3; i >= 0; i--) // 321倒计时
	{
		start = clock();     //成员变量开始计时
		while (clock() - start <= 1000); // 暂停1000个系统时间片,1秒
		system("cls");
		if (i > 0)
			cout << "\n\n\t\tCountdown:" << i << endl;
	}
	for (i = 1; i <= 3; i++) // 定义贪吃蛇起始棋盘位置
	{
		qp[1][i] = '*';//贪吃蛇身体
	}
	qp[1][4] = '@';     //贪吃蛇头
	for (i = 0; i < 4; i++) //初始化贪吃蛇的坐标
	{
		zb[0][i] = 1;
		zb[1][i] = i + 1;
	}
}

move函数可以认为是整个程序的核心、最不好实现的部分。

其中用到的变量在定义Snake类中时已描述。

#include<time.h>
#include"snake.h"
#include<Windows.h>
#include<iostream>
#include<conio.h>
using namespace std;
void snake::move()
{
	score = 0;
	head = 3, tail = 0;
	grade = 1, length = 4;
	gamespeed = 500; // 500时间片0.5秒走一步
	direction = 77; // 起始方向->
	oldDirection = direction;
	int breakFlag = 0; //按键异常标志。

cppwhile ((timeover = (clock() - start <= gamespeed)) && !_kbhit());也已有描述,也有注释。breakFlag—按键异常标志,当按了与当前蛇的走向相反方向的键,这个值就从0设成1。case 72:if (oldDirection == 80) { breakFlag = 1; break; }x = zb[0][head] - 1; y = zb[1][head]; oldDirection = direction; break;表示按了上按键,如果蛇本来按向下走的,结束这个switch语句,进入按键异常处理;否则(蛇头在向上\左\右方向前进),蛇头立即继续按原方向前进。72上,80下,75左,77右。下面三行的代码与之同理。至于怎么通过zb数组+1 -1的变化引起相应输出数组qp显示成蛇应该有的形状,到这段代码最后再解释。(简单讲,这里贪吃蛇想改成向下移动,就将蛇头(@符号)纵坐标zb[0][head]比原来减去1,而横坐标zb[1][head]不变)。这个程序head的更新方式有致命缺陷,一会讨论。

	while (1)
	{
		timeover = 1;
		start = clock(); //计时开始
		while ((timeover = (clock() - start <= gamespeed)) && !_kbhit()); //等待gamespeed时间自动前进或者有按键输入前进
		//当等待时间超过gamespeed,(timeover = (clock() - start <= gamespeed)为假退出while循环,程序往下运行,此时timeover=0,表示自动前进
		//当有按键输入,_kbhit()真,!_kbhit()为假,程序往下运行,此时timeover=1,表示按键按下手动前进
		if (timeover)
		{
			_getch();//输入为“上下左右”时,需要获取两次才能得到正确的ASC码 
			direction = _getch();//后去输入方向
			//此写法当输入其他按键时程序会等待下个按键,为暂停效果
		}
		switch (direction) //根据方向更新贪吃蛇坐标
		{
			//与当前前进方向相反按键认为按键异常
		case 72:if (oldDirection == 80) { breakFlag = 1; break; }x = zb[0][head] - 1; y = zb[1][head]; oldDirection = direction; break;//坐标更新
		case 80: if (oldDirection == 72) { breakFlag = 1; break; }x = zb[0][head] + 1; y = zb[1][head]; oldDirection = direction; break;//坐标更新
		case 75: if (oldDirection == 77) { breakFlag = 1; break; }x = zb[0][head]; y = zb[1][head] - 1; oldDirection = direction; break;//坐标更新
		case 77: if (oldDirection == 75) { breakFlag = 1; break; }x = zb[0][head]; y = zb[1][head] + 1; oldDirection = direction; break;//坐标更新
		default: breakFlag = 1;//超出上下左右范围的按键认为按键异常
		}

在按键异常处理的方式与上述方式完全相同(实际上fagns可以换一种写法去掉),这里如果按键方向与已知方向相反,就直接再往原来的方向前进一个单位,与上述分析过程相同。

		if (breakFlag) //按键异常处理
		{
			switch (oldDirection) {//保持原有方向继续前进
			case 72: x = zb[0][head] - 1; y = zb[1][head]; direction = oldDirection; break;//坐标更新
			case 80: x = zb[0][head] + 1; y = zb[1][head]; direction = oldDirection; break;//坐标更新
			case 75: x = zb[0][head]; y = zb[1][head] - 1; direction = oldDirection; break;//坐标更新
			case 77: x = zb[0][head]; y = zb[1][head] + 1; direction = oldDirection; break;//坐标更新
			}
			breakFlag = 0; //恢复标志位
		}

而后的几行代码比较简单,判断是否撞墙,就是判断更新后的蛇头是否碰到了边框(蛇头横或纵坐标是0或21是撞墙的标志),判断是否碰到了自己,就是判断更新后的蛇头位置qp中存放着* (当然,这个*不能是恰好随机生成的那些个),也就是(qp[x][y] != ' ' && !(x == x1 && y == y1))这两种情况都是gameover。



		if (x == 0 || x == 21 || y == 0 || y == 21) // 判断是否撞墙
		{
			cout << "\tGame over!" << endl; break;
		}
		if (qp[x][y] != ' ' && !(x == x1 && y == y1)) // 吃自己判断
		{
			cout << "\tGame over!" << endl; break;
		}

game没over就判断🐍是不是吃了新食物。吃了新食物就加分数、对间隔时间进行判断。判断逻辑比较简单:吃了就加100分;length不是蛇长参数而是加等级的参数,初始化是4,到了8以后加等级,此后每新搞定8个加一个等级(如图,第一遍吃4个就到等级2,以后每吃8个升一级。对应的升级分数是400 1200 2000 2800等等)。每升一级,蛇往前一步的时间间隔减少50ms。如果厉害的玩到200ms,那就不再减了。

		if (x == x1 && y == y1) // 吃到彩蛋
		{
			length++;
			score = score + 100;
			if (length >= 8)
			{
				length -= 8;
				grade++;
				if (gamespeed >= 200)
					gamespeed = 550 - grade * 50; //游戏难度等级设置,加速
			}

在这里插入图片描述
在这里插入图片描述

最后,在棋盘(画布)上搞出来新生成的蛇。这算是最难的部分。上面已经搞到(以向下为例)x = zb[0][head] - 1; y = zb[1][head],那显然蛇头的横坐标(这里要横着看)是x,纵坐标是y,先把qp(显示数组)中的[x][y]设成@;如果🐍刚刚吞了一个食物,那么tail的值不需要更新、屏幕上的尾巴也不需要更新。只更新head的值。由于数组横纵坐标分别只有100个空间,如果有人真有耐心搞到了长度为100的🐍,会出现混乱的情况。这里我把参数100都调成15来看看怎么回事:
在这里插入图片描述
发现到满15后tail的值就无法更新,🐍的尾部不跟随它的前进而消失;而再获得一个食物后,出现了断头🐍的情况(如上图)。可见,对其影响是巨大的。


			qp[x][y] = '@';
			qp[zb[0][head]][zb[1][head]] = '*';
			head = (head + 1) % 100;
			zb[0][head] = x;
			zb[1][head] = y;
			food();//重新生成食物
			prt(grade, score, gamespeed); //更新屏幕显示
		}
		else
		{
			qp[zb[0][tail]][zb[1][tail]] = ' ';
			tail = (tail + 1) % 100;
			qp[zb[0][head]][zb[1][head]] = '*';
			head = (head + 1) % 100;
			zb[0][head] = x;
			zb[1][head] = y;
			qp[zb[0][head]][zb[1][head]] = '@';
			prt(grade, score, gamespeed);//更新屏幕显示
		}
	}
}

主函数直接带进来snake.h创建实例、调用move、从io getchar就搞出来了。

#include<iostream>
#include"snake.h"

using namespace std;   //引用c++命名空间“std”
int main()
{
	// chessboard cb;
	snake s;
	s.move();
	getchar();
	//printf("%c%c%c%c\n", 72, 80, 75, 77);
	//getchar();
}

总体代码如下:
chessboard.h

#pragma once
#define H1 22           //宏定义
#define W 22           //宏定义
class chessboard       // 定义了棋盘类
{
public:
	char qp[H1][W];	   //定义数组表示棋盘,核心思想是不同的数组内容表示不同的含义
	int i, j, x1, y1;  //定义棋盘用到的变量
	chessboard();  //定义类“chessboard”的构造函数,用于初始化棋盘
	void chessboard2();
	void food();       //定义类“chessboard”的成员函数“food”,用于随机生成食物
	void prt(int grade, int score, int gamespeed); // //更新屏幕显示:棋盘显示等级 分数和游戏速度
};

snake.h

#pragma once
#include"chessboard.h"
#include<time.h>
#include<conio.h>
class snake:chessboard// 类继承
{
public:
	int zb[2][100]; //用于存放贪吃蛇的坐标
	long start; //用于计时
	int head, tail, grade, score, gamespeed, length, timeover, x, y; //游戏内使用的标量
	char direction;    //贪吃蛇前进方向,ASCII码数字显示方式
	char oldDirection; //方向更新是用到的临时方向
	snake(); //构造函数
	void move();//成员函数,表征贪吃蛇移动动作,每500ms更新一次
};

chessboard_food_prt.cpp

#include<conio.h>
#include<windows.h>
#include"chessboard.h"
#include<time.h>
#include<iostream>
using namespace std;

chessboard::chessboard()
{
	for (i = 1; i <= H1 - 2; i++)
	{
		for (j = 1; j <= W - 2; j++)
		{
			qp[i][j] = ' ';//棋盘中间置零
		}
	}
	for (i = 0; i <= H1 - 1; i++)
	{
		qp[0][i] = qp[H1 - 1][i] = '#';//棋盘两边置#
	}
	for (i = 1; i <= H1 - 2; i++)
	{
		qp[i][0] = qp[i][W - 1] = '#';//棋盘两边置#
	}
	food(); //每次构造函数调用成员函数food生成食物
}

void chessboard::food()
{
	srand(time(0));      // 随机时间  srand生成随机数,需要输入随机种子,种子相同则随机数相同;为保证生成的随机数具有随机性应选取不同的随机种子,一般选取系统当前时间最为随机种子,因为系统时间在变化
						  //time(0)回去当前系统时间
	do
	{
		x1 = rand() % W - 2 + 1; // 随机位置
		y1 = rand() % H1 - 2 + 1;
	} while (qp[x1][y1] != ' '); //保证生成的食物在空白处
	qp[x1][y1] = '$'; //设置食物形状
}
void chessboard::prt(int grade, int score, int gamespeed)
{
	system("cls");//调用系统命令行cls清空屏幕,与cmd中命令行一致                                      //取消这一行                          //取消这一行
	cout << endl; //换行
	for (i = 0; i < H1; i++) //显示结果
	{
		cout << "\t";
		for (j = 0; j < W; j++)
			cout << qp[i][j] << ' ';
		if (i == 0) cout << "\tGrade:" << grade;
		if (i == 2) cout << "\tScore:" << score;
		if (i == 4) cout << "\tAutomatic forward";
		if (i == 5) cout << "\ttime interval:" << gamespeed << "ms";
		cout << endl;
	}
}

snake.cpp

#include<iostream>
#include"snake.h"

using namespace std;
snake::snake()
{
	cout << "\n\n\t\tThe game is about to begin!" << endl;
	for (i = 3; i >= 0; i--) // 321倒计时
	{
		start = clock();     //成员变量开始计时
		while (clock() - start <= 1000); // 暂停1000个系统时间片,1秒
		system("cls");
		if (i > 0)
			cout << "\n\n\t\tCountdown:" << i << endl;
	}
	for (i = 1; i <= 3; i++) // 定义贪吃蛇起始棋盘位置
	{
		qp[1][i] = '*';//贪吃蛇身体
	}
	qp[1][4] = '@';     //贪吃蛇头
	for (i = 0; i < 4; i++) //初始化贪吃蛇的坐标
	{
		zb[0][i] = 1;
		zb[1][i] = i + 1;
	}
}

move.cpp

#include<time.h>
#include"snake.h"
#include<Windows.h>
#include<iostream>
#include<conio.h>
using namespace std;
void snake::move()
{
	score = 0;
	head = 3, tail = 0;
	grade = 1, length = 4;
	gamespeed = 500; // 500时间片0.5秒走一步
	direction = 77; // 起始方向-》
	oldDirection = direction;
	int breakFlag = 0; //按键异常标志
	while (1)
	{
		timeover = 1;
		start = clock(); //计时开始
		while ((timeover = (clock() - start <= gamespeed)) && !_kbhit()); //等待gamespeed时间自动前进或者有按键输入前进
		//当等待时间超过gamespeed,(timeover = (clock() - start <= gamespeed)为假退出while循环,程序往下运行,此时timeover=0,表示自动前进
		//当有按键输入,_kbhit()位真,!_kbhit()为假,程序往下运行,此时timeover=1,表示按键按下手动前进
		if (timeover)
		{
			_getch();//输入为“上下左右”时,需要获取两次才能得到正确的ASC码 
			direction = _getch();//后去输入方向
			//此写法当输入其他按键时程序会等待下个按键,为暂停效果
		}
		switch (direction) //根据方向贪吃蛇的坐标更新 
		{
			//与当前前进方向相反按键认为按键异常
		case 72:if (oldDirection == 80) { breakFlag = 1; break; }x = zb[0][head] - 1; y = zb[1][head]; oldDirection = direction; break;//坐标更新
		case 80: if (oldDirection == 72) { breakFlag = 1; break; }x = zb[0][head] + 1; y = zb[1][head]; oldDirection = direction; break;//坐标更新
		case 75: if (oldDirection == 77) { breakFlag = 1; break; }x = zb[0][head]; y = zb[1][head] - 1; oldDirection = direction; break;//坐标更新
		case 77: if (oldDirection == 75) { breakFlag = 1; break; }x = zb[0][head]; y = zb[1][head] + 1; oldDirection = direction; break;//坐标更新
		default: breakFlag = 1;//超出上下左右范围的按键认为按键异常
		}

		if (breakFlag) //按键异常处理
		{
			switch (oldDirection) {//保持原有方向继续前进
			case 72: x = zb[0][head] - 1; y = zb[1][head]; direction = oldDirection; break;//坐标更新
			case 80: x = zb[0][head] + 1; y = zb[1][head]; direction = oldDirection; break;//坐标更新
			case 75: x = zb[0][head]; y = zb[1][head] - 1; direction = oldDirection; break;//坐标更新
			case 77: x = zb[0][head]; y = zb[1][head] + 1; direction = oldDirection; break;//坐标更新
			}
			breakFlag = 0; //恢复标志位
		}
		if (x == 0 || x == 21 || y == 0 || y == 21) // 判断是否撞墙
		{
			cout << "\tGame over!" << endl; break;
		}
		if (qp[x][y] != ' ' && !(x == x1 && y == y1)) // 吃自己判断
		{
			cout << "\tGame over!" << endl; break;
		}
		if (x == x1 && y == y1) // 吃到彩蛋
		{
			length++;
			score = score + 100;
			if (length >= 8)
			{
				length -= 8;
				grade++;
				if (gamespeed >= 200)
					gamespeed = 550 - grade * 50; //游戏难度等级设置,加速
			}
			qp[x][y] = '@';
			qp[zb[0][head]][zb[1][head]] = '*';
			head = (head + 1) % 100;
			zb[0][head] = x;
			zb[1][head] = y;
			food();//重新生成食物
			prt(grade, score, gamespeed); //更新屏幕显示
		}
		else
		{
			qp[zb[0][tail]][zb[1][tail]] = ' ';
			tail = (tail + 1) % 100;
			qp[zb[0][head]][zb[1][head]] = '*';
			head = (head + 1) % 100;
			zb[0][head] = x;
			zb[1][head] = y;
			qp[zb[0][head]][zb[1][head]] = '@';
			prt(grade, score, gamespeed);//更新屏幕显示
		}
	}
}

main.cpp

#include<iostream>
#include"snake.h"

using namespace std;   //引用c++命名空间“std”
int main()
{
	// chessboard cb;
	snake s;
	s.move();
	getchar();
	//printf("%c%c%c%c\n", 72, 80, 75, 77);
	//getchar();
}

总体运行效果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值