分享一个简易的小黑窗贪吃蛇,一共就两百行代码左右(包含注释),很适合初学者巩固语法来练练手.
如果后续需要其他功能也可以再添加.
先小小展示一下:
源码在文末免费领取.
使用工具:
VS2019(不是用VS的也可以直接找出cpp和h文件复制到你们用的IDE,甚至是记事本都可以)
闲话(可跳过):
为什么要写贪吃蛇呢?首先是从这学期开始下定决心要走C++路线了,因此把C++又复习了一遍,基础语法又学了一遍,还写了C++primer的笔记(有专栏).
想着要来个项目练练手,但是目前就会个C++的基础语法和STL,甚至连QT都不熟练,然后我又想起了把我领进门的学长在我刚学C语言的时候就布置了一个"任务",学完C语言之后写个贪吃蛇出来 .
因此在复习完C++后我决定就写个小黑窗贪吃蛇出来,QT版本的以后也要做出来(等我再好好学学QT),然后大概是花了一个下午把贪吃蛇给写出来了,包括注释也就两百行左右,自己留着也是孤芳自赏,因此在此分享出来.
蛇:
在写代码之前一定要构思好怎么写,绝不能想到一点写一点,否则以后加功能或是找bug的时候会很痛苦(深有体会)
贪吃蛇最主要的就是蛇,那么先给蛇写个类,类中需要的属性有
蛇的长度
蛇的头部
蛇的身子
蛇的运动方向
蛇应该要有的动作(函数):
沿着方向移动
改变方向
因此可以写出蛇类的大体框架:
class snack{friend class UI; //将UI界面设为友元,使得界面可以接触到蛇的私有属性
private:vector<int>head = vector<int>(2); //存放头的坐标vector<vector<int>>body; //存放身子的坐标char direction; //运动方向int size = 1; //身长public:snack(int x=25, int y=10); void crawl(); //爬行bool changeDirection(char newDirection); //改变方向
界面:
刚才说贪吃蛇最主要的是蛇,其实最主要的是界面(至少在本项目中)
界面需要有边框,需要把蛇装起来,需要生成食物......
UI需要的属性有:
长
宽
蛇
得分
食物
界面
需要有的动作(函数):
展示画面
移动蛇
检测玩家操作
生成食物
检查蛇的移动是否合规
由上可以写出UI类的大体框架
class UI {friend void getDirection(UI& u);
private:int Width; //宽int Height; //高int score=0; //得分snack role; //蛇std::default_random_engine e; //用于生成随机数,随机生成食物vector<int>food = vector<int>(2); //存放食物坐标vector<vector<char>>UI_Cache; //界面缓存,直接打印出来即可
public:UI(int width = 50, int height = 20);void show(); void moveSnack(); //移动蛇bool check(char c); //检查输入字符void getDirection(); //获取玩家操作void createFood(); //生成食物void checkRole(); //检查蛇的移动是否合规void run(); //运行
};
生成界面
那么框架有了,我们只需要把里面的功能实现就行,那么界面该怎么生成呢?
我们直接把界面缓存打印出来,界面缓存是个二维vector,刚好是界面的长*宽,因此我们需要把二维vector的边界修改成游戏的边界,这一点可以在UI的构造函数里完成,说到构造函数,我们很多功能都可以在构造函数里完成,例如初始化蛇,初始化食物......
如下所示:
UI::UI(int width, int height) :Width(width), Height(height) {UI_Cache.resize(height, vector<char>(width, ' ')); //将画面大小重置role = snack(width/2,height/2); //初始化蛇,传入参数使蛇在画面中间e.seed(time(0)); //给随机数引擎设置随机数种子//绘制边框for (int i = 0; i < height; i++) UI_Cache[i][0] = UI_Cache[i][width - 1] = '|';for (int j = 0; j < width; j++) UI_Cache[0][j] = UI_Cache[height - 1][j] = '-';UI_Cache[0][0] = UI_Cache[0][width - 1] = UI_Cache[height - 1][0] = UI_Cache[height - 1][width - 1] = '*';//打印开始界面for (int i = 0; i < Height / 2-1; i++) cout << endl; //使得welcome在界面中央for (int j = 0; j < Width / 2; j++)cout << " ";cout << "welcome" << endl;//初始化食物createFood();Sleep(1500);system("cls");
}
蛇的移动
界面有了之后,我们应该让蛇移动了,但是在游戏的一开始应该让蛇停在画面中间,直到玩家开始操作之后再开始移动,我们该怎么移动蛇呢?
蛇分为头部和身子,蛇的每次移动实际上是移动头部,而身子的大部分是不变的,我们可以找到规律,每次移动,身子的最后一节都会没有,而原本头所在的位置会变成最开始的一节身子,那么我们可以在蛇移动的时候,删去存放蛇身子的vetcor的最后一个(pop_back()),然后再把头插入到存放蛇身子的vector的第一位(insert(body.begin(),head));
void snack::crawl() { //爬行,将头部变成第一节身子,然后将身子的末尾去掉.if (direction == ' ') return; //开局先不动,方向默认是空字符,可以直接返回.body.insert(body.begin(), head);body.pop_back();//调增新头部的位置if (direction == 'w') head[1]--; //向上,则是将y轴的值减一else if (direction == 's') head[1]++; //向下,则是将y轴的值加一else if (direction == 'a') head[0]--; //向左,则是将x轴的值减一else if (direction == 'd') head[0]++; //向右,则是将x轴的值加一
}
说到移动,就不能忘记方向,因此我们需要有调整蛇方向的函数,要注意的是,蛇不能掉头(180°转弯),只能90°转.
bool snack::changeDirection(char newDirection) {//调整移动方向//这里需要做个小判断,比如正在向左走就不能转到右,不能直接180°转弯if (newDirection == 'w' && direction == 's') return false;if (newDirection == 's' && direction == 'w') return false;if (newDirection == 'a' && direction == 'd') return false;if (newDirection == 'd' && direction == 'a') return false;direction = newDirection;return true;
}
方向是需要玩家输入的,因此我们需要实现玩家输入的函数,本来我是想做成多线程的,这样子扫描玩家输入比较流畅,但是我查了半个下午,调试了四分之一个下午都没能搞定,因此还是就做成固定查询了.(如果有搞定了多线程扫描输入的好兄弟,可以评论告诉我)
#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)bool UI::check(char c) {//检测某个按键是否按下if (KEY_DOWN(c)) return true;return false;
}void UI::getDirection() {if (check('Q')) { //按q则退出cout << "game is over!" << endl;cout << "your score is " << score << endl;exit(0);}//操纵蛇else if (check('A') || check(37)) role.changeDirection('a');else if (check('S') || check(40)) role.changeDirection('s');else if (check('D') || check(39)) role.changeDirection('d');else if (check('W') || check(38)) role.changeDirection('w');
}
当然,我们移动蛇只是改变了蛇的属性,我们还需要在UI界面实现更新蛇坐标的功能:
void UI::moveSnack() {//将蛇的头部和身子分别映射到UI的缓存中,这里可以修改蛇头部和身子的字符cout << role.head[0] << ' ' << role.head[1] << endl;UI_Cache[role.head[1]][role.head[0]] = '@';for (auto b : role.body) {UI_Cache[b[1]][b[0]] = '*';}
}
如果直接向上面那样的话会有一个问题,就是蛇会一直变长,这是因为在UI界面的缓存中,我们仍然缓存着上一次的蛇身子的坐标,而我们这里只是在UI缓存中将蛇的身体的坐标赋值成了特定的符合,而没有删除上一次的缓存,这就导致了因为移动而被我们删除的最后一节身子仍然留在缓存中,所以我们还需要清除缓存,但是如果因此而重置缓存的话,那么会影响运行效率,因此我们仅仅将移动前的身子最后一节的坐标映射在UI缓存中的位置置位空字符即可:
UI_Cache[role.body[role.size - 1][1]][role.body[role.size - 1][0]] = ' ';
食物
接下来就剩食物了,生成食物使用随机数引擎,在界面宽高的范围内生成随机的x,y坐标,还需要检查,如果生成在了蛇的位置上则需要重新生成.
void UI::createFood() { //生成食物std::uniform_int_distribution<unsigned> u(1,Width-2); //设置随机数生成范围int x, y;while (1) {x = u(e), y = u(e) % (Height - 2); if (x <= 0 || y <= 0 || x >= Width - 1 || y >= Height - 1) continue; //如果生成的食物坐标不在边界内则重新生成.if (x == role.head[0] && y == role.head[1]) continue; //如果生成的食物坐标和蛇头一致则重新生成.for (auto a : role.body) {if (y == a[1] && x == a[0]) continue; //如果生成的食物坐标与蛇身一致则重新生成.}break;}food[0] = x, food[1] = y;UI_Cache[y][x] = '$'; //这里可以更改食物的字符
}
我们还需要检测蛇是否吃到了食物,以及是否吃到了自己和是否撞墙.
撞到墙或吃到自己是毫无疑问结束游戏,而吃到了食物,我们则需要将玩家的得分增加,并且增长蛇的长度,我们可以将食物的位置变成新的蛇头位置,并且将老蛇头的坐标插入到蛇身体里,这样就完成了蛇身体的增长.
不要忘记生成新的食物:
void UI::checkRole() { //检查移动是否合法int x = role.head[0], y = role.head[1];if (x <= 0 || y <= 0 || x >= Width-1 || y >= Height-1) { //碰到边界则失败cout << "you are lose" << endl;cout << "your score is " << score << endl;exit(0);}for (auto a : role.body) { //碰到自己的身体则失败if (a[0] == x && a[1] == y) {cout << "you are lose" << endl;cout << "your score is " << score << endl;exit(0);}}if (x == food[0] && y == food[1]) { //吃到食物则分数增加,蛇身体长度增加.score++; role.size++;role.body.insert(role.body.begin(),role.head);role.head[0] = food[0], role.head[1] = food[1]; //将食物位置直接变成新蛇头的位置,达到身体增长的效果.createFood(); //重新生成食物}
}
代码领取:
免费领取完整代码可以关注我的公众号:折途想要敲代码,回复关键字"贪吃蛇"即可.
也可以直接在CSDN上下载,我也已经上传到CSDN了.