哎呀,快到期末啦,学校给了一个写贪吃蛇的作业QAQ,查了好多资料才把这个写出来,但是正是因为这个实战让我学到了,很多新的东西,今天来分享给大家。
项目实现:
1、定义一个蛇的结构体用于存储蛇的位置,并且将蛇身的每一部分连接起来。
2、定义一个食物的结构体,用于存储食物的位置
3、绘制地图(包括地图边界,蛇和食物的绘制)
4、蛇的移动
5、食物的生成
6、各种判定机制(蛇是否吃到食物,食物的生成是否合法,游戏是否结束。。。)
7、菜单部分
8、将以上函数利用主函数形成逻辑关系
话费不多直接上头文件
//#ifdef _TOP_H
//#define _TOP_H
#include<iostream>
#include<Windows.h>
#include<time.h>
#include<conio.h>
#include<stdlib.h>
using namespace std;
//创建蛇的结构体,需要记录蛇的位置,还需要将蛇身连接起来
struct Snake {
int x;
int y;
Snake* pre;
Snake* next;
};
//创建食物的结构体,记录食物位置即可
struct Food {
int x;
int y;
};
//绘制场景
void Drawmap(Food* food,Snake *head);
//放置食物
void creatfood(struct Food* food, int m, char move);
//蛇的移动
void movethesnake(Snake* rear, Snake* head, char move);
//读取按键
char setbutton(char move);
//检查食物与蛇的位置是否重合
int iffoodandsnake(Food* food, Snake* head);
//检测蛇是否吃到食物
int ifSnakemeetFood(Snake* head, Food* food);
//将蛇身增长一段
Snake* addSnakeLong(Snake* rear, char move);
//各种菜单
void menue2();
void menue0();
void menue3(int m);
void menue1();
void menue4(int score, int max);
//模式1中的结束条件
int ifend(Snake* head);
//模式二中当蛇碰到墙壁会从另一边穿出的规则实现
void nonewall(Snake* head);
//模式二的结束条件
int newif(Snake* head);
//#endif
”哎,博主,你就整个头文件,这谁能理解啊”,别着急接下来我会为大家一一讲解每一部分
绘制地图
#include"top.h"
const int L = 20;
const int W = 50;
char map[L][W];
void Drawmap(Food *food,Snake *head) {
//清屏操作,每次开始时,将上一次的图像清空
system("cls");
for (int i = 0; i < W; i++) {
cout << "-";
}
cout << endl;
for (int i = 0; i < L; i++) {
cout << "|";
for (int j = 0; j < W; j++) {
//使用flag减少程序处理时间,如果该位置绘制了蛇的部分就直接跳到下一个部分,不需要绘制其他东西
int flag = 0;
Snake* tmp=head;
while (tmp) {
if (tmp->x == i && tmp->y == j) {
if (tmp == head)
cout << "O";
if(tmp!=head) cout << "o";
flag = 1;
break;
}
tmp = tmp->next;
}
if (flag == 0) {
if (i == food->x && j == food->y) {
cout << "*";
continue;
}
cout << " ";
}
}
cout << "|" << endl;;
}
for (int i = 0; i < W; i++) {
cout << "-";
}
cout << endl;
}
(由于这里我不会在头文件创建宏定义,所以我就只能在每个需要用到的函数头顶上重新声明了T-T)首先每次绘制场景前我们都会情况清理掉上一次场景绘制的情况,保证页面的整洁,所以使用了一个system(“cls”)的清屏操作,然后利用“-”和·“|”绘制边界,在绘制每一行的过程中利用flag来判断是否有蛇身的绘制,如果有,绘制完蛇身后就直接跳到下一个格子的绘制,如果没有蛇身的绘制就检测在该点位置是否有食物存在,有就将食物绘制出来,然后跳到下一个格子的绘制。如果这一格既没有蛇身也没有食物,那就绘制” “一个空格表示此地为空
总而言之绘制的优先级为 边界>蛇身>食物>空格
蛇的移动
#include"top.h"
void movethesnake(Snake *rear,Snake *head,char move) {
Snake* tmp=rear;
//从蛇尾到蛇头的前一段,身体移动到上一段的位置
while (tmp!=head) {
tmp->x = tmp->pre->x;
tmp->y = tmp->pre->y;
tmp = tmp->pre;
}
//再将蛇头移动到按键对应的位置
if (move == 'a') {
head->y -= 1;
}
if (move == 'w') {
head->x -= 1;
}
if (move == 's') {
head->x += 1;
}
if (move == 'd') {
head->y += 1;
}
}
//按键检测,当输入有效按键时才返回
char setbutton(char move) {
char c = _getch();
if (c == 27) {
return 'x';
}
if (c == 'a') {
if (move == 'd')return move;
move = 'a';
return move;
}
if (c == 'w') {
if (move == 's')return move;
move = 'w';
return move;
}
if (c == 's') {
if (move == 'w')return move;
move = 's';
return move;
}
if (c == 'd') {
if (move == 'a')return move;
move = 'd';
return move;
}
//如果输入的不是有效按键就放回上一次的按键
return move;
}
蛇的移动分为两个过程,首先接收键盘输入的指令,再根据指令调整蛇的每一个部分的位置。
首先来实现对于键盘指令的接收,这里就有新知识辣,由于我以前学习的输入方法必须要按下enter进行确认才能输入,这显然不满足玩贪吃蛇游戏的要求,并且输入的数据会显示在屏幕上也并不美观。在查阅资料的过程中我发现了_kbhit()和_getch()(VS里必须要前面的那个下划线),他们都包含在include<conio.h>里,前一个函数可以实现检测到键盘是否有输入的操作(若无输入则返回0),后一个是一个输入的函数,可以立刻接收你的输入不用确认,并且不会显示在屏幕上所以我们在主函数利用_kbhit()来检测键盘是否有输入,如果没有输入,移动的方向就按照上一次的移动方向处理,如果有输入,就检测输入的合法性是否为”a,s,d,w,esc中的一种“如果输入违法,也按照上一次的移动方向处理
接下来来谈谈关于蛇的移动,其实我们可以发现,键盘上接收的所有指令都是控制头的移动对不对,那么身体的其他部分如何移动呢?我们小时候都玩过一个开火车的游戏吧,当火车头移动之后,后一个同学是不是应该也向前一步,除了火车头以外的每个同学都会走到他们前一个同学之前所占的位置。没错蛇的身体移动也是如此,我们从蛇尾开始,将蛇的前一部分(这时结构体里定义的记录前一个蛇身的信息就帮大忙了)的位置(x轴,y轴)作为该部分的位置,最后再让蛇头根据移动的指令移动即可。
食物的生成
#include"top.h"
const int L = 20;
const int W = 50;
const int Woring = 5;
void creatfood(struct Food *food,int m,char move) {
//当食物与蛇身的重合次数超过最大次数时,直接在蛇头的移动方向之前放置食物
if (m == Woring) {
if (move == 'a') {
food->y -= 1;
}
if (move == 'w') {
food->x -= 1;
}
if (move == 's') {
food->x += 1;
}
if (move == 'd') {
food->y += 1;
}
return;
}
food->x = rand() % L;
food->y = rand() % W;
}
敲黑板辣新知识又来辣,食物的生成无疑是一个随机数的生成过程,虽然我以前学习过随机数的生成rand(),但是这个随机数,在每次运行程序的时候产生数字都是相同的,后来我了结到了原来rand()是依托srand()括号里的种子来生成的随机数,就像玩《我的世界》一样,相同的种子会生成一个完全相同的地图”博主又在乱讲明明基岩版就不能用java版的种子“(小声),去!臭杠精别打岔。那么,我们能否有办法让每次运行时srand()里产生的种子都不同呢?我们可以使用系统的时间来作为种子,生成随机数。我们可以在主函数声明其种子srand(time(NULL)),这样每次运行时系统的时间不同生成的随机数也会不同
ok,解决完理论上的问题,我们直接操作,这里有一个小细节,为了将食物的生成范围控制在我们的边界内(因为我们只绘制了边界内的内容,如果食物生成在边界外,我们根本看不到),所以在对应x的随机数后要对L取余,保证该随机数小于L(这里我测试了很多次,好像随机数的生成都是正数,因为我的食物从来没刷新在边界外所以应该不用对其正负进行操作),y的操作同理。
但是随着我们蛇身的越来越长,食物的刷新很可能会刷在蛇的身体内(这肯定是不合理的毕竟白嫖就能吃到食物),所以为了防止食物在多次刷新后任然找不到合适的刷新位置,所以我们规定了一个Woring,当刷新次数超过Woring时,直接选定一块空的地方将食物放上去(我这里是根据蛇的移动方向来直接放在他移动方向的前一格,大家也可以有自己的想法)
各种判定
#include"top.h"
const int L = 20;
const int W = 50;
//蛇是否吃到食物的判定
int ifSnakemeetFood(Snake* head, Food* food) {
//当蛇头与食物位置重合时返回1
if (head->x == food->x && head->y == food->y)
return 1;
else return 0;
}
//食物是否生成在蛇的内部的判定
int iffoodandsnake(Food *food,Snake *head) {
Snake* tmp = head;
//从头到尾检测食物是否生成在蛇的身体内
while (tmp) {
if (tmp->x == food->x && tmp->y == food->y) return 0;
tmp=tmp->next;
}
return 1;
}
//游戏是否结束的判定
int ifend(Snake *head) {
Snake* tmp=head->next;
//当蛇的头与墙重合时游戏结束
if (head->x == -1 || head->x == L||head->y==-1||head->y==W)
return 1;
while (tmp) {
//当蛇头与蛇身向碰时游戏结束
if (head->x == tmp->x && head->y == tmp->y)
return 1;
tmp = tmp->next;
}
return 0;
}
//模式二的结束条件:当蛇头碰到蛇身时
int newif(Snake* head) {
Snake* tmp = head->next;
while (tmp) {
if (head->x == tmp->x && head->y == tmp->y)
return 1;
tmp = tmp->next;
}
return 0;
}
//当蛇头碰到边界时,将蛇头的位置直接重新定义在另一端
void nonewall(Snake *head) {
if (head->x == -1) {
head->x = L;
return;
}
if (head->x == L) {
head->x = 0;
return;
}
if (head->y == -1) {
head->y = W;
return;
}
if (head->y == W) {
head->y = 0;
return;
}
}
判定的代码相对比较简单只需要确定蛇的头部或者身体与食物或者边界的坐标是否重合即可,注意后面两个函数是针对模式二的特殊规则,大家可以先不看注释,根据代码猜猜看模式二是什么模式
菜单
#include"top.h"
void menue0() {
cout << "\t\t*********贪吃蛇*********" << endl ;
cout << "\t\t请选择游戏模式 1:普通模式\t2:无墙模式" << endl;
cout << "\t\t*********************" << endl;
}
void menue1() {
system("cls");
cout << "\t\t真的要退出吗QAQ" << endl;
cout << "\t\t确认--1"<<endl;
}
void menue2() {
for (int i = 0; i < 3; i++) {
system("cls");
if (i == 0)
cout << "贪" << endl;
if (i == 1) {
cout << endl;
cout << "\t吃" << endl;
}
if (i == 2) {
cout << endl;
cout << endl;
cout << "\t\t蛇" << endl;
}
Sleep(1000);
}
}
void menue3(int m){
system("cls");
cout << endl << endl << endl << "\t\t\t不服气?再来一把?当前得分"<<m << endl;
cout << "\t\t\t1--那就再战\t2--算了我不行"<<endl;
}
void menue4(int score,int max) {
cout << "\t\t当前得分:" << score<<"\t 最高得分:" << max << endl;
}
新知识又来辣,请大家看到menue2()的倒数最后,Sleep(括号里写停顿时间ms)是用于控制屏幕刷新率的函数,像我们平时写cout写不管多少行,运行的时候就直接全部显示出来了,而不是一行一行的显示,(这肯定不够酷,狠人说话都要慢慢说,咱们也要慢慢输出)这是因为系统默认的刷新时间是很短暂的,没刷新一次出现一个cout后的语句,其实他是一行一行的出现的,但是刷新太快我们捕捉不到,所以我们可以在那些cout后面输入Sleep()来使其停顿一下,让他别刷新那么快,大家可以试试下面一段代码加Sleep()和不加Sleep()的区别。
#include<Windows.h>
#include<iostream>
using namespace std;
int main(){
cout<<"1"<<endl;
Sleep(1000);
cout<<"\t2"<<endl;
Sleep(1000);
cout<<"\t\t3"<<endl;
Sleep(1000);
cout<<"\t\t\t4"<<endl;
Sleep(1000);
cout<<"\t\t\t\t5"<<endl;
return 0;
}
游戏的逻辑
#include"top.h"
const int L = 20;
const int W = 50;
//食物生成位置与蛇身位置重叠的最大次数
const int Woring = 5;
int main() {
int again=1;
menue2();
//改变模式
int change;
//根据系统的时间生成种子,确保每次开始游戏rand()生成的随机数不同
srand(time(NULL));
int yin=0;
while (1) {
system("cls");
menue0();
cin >> change;
//小彩蛋输入的时候多次违规输入即可触发;
if (change == 1 || change == 2)
break;
if (yin == 0) {
cout << "请规范输入" << endl;
system("pause");
}
if (yin == 1) {
cout << "请规范输入!!!" << endl;
system("pause");
}
if (yin == 2) {
cout << "?你很会输入吗" << endl;
system("pause");
}
if (yin == 3) {
cout << "好好好我算是看出来了你非得找出点什么,好了给你个隐藏模式吧(宝宝模式)"<<endl;
system("pause");
change = 1;
break;
}
yin++;
}
system("cls");
int max = 0;
while (again == 1) {
//创建和初始化食物,蛇,按键
char move = 'd';
Food food = { 1,1 };
Snake snake, * head, * rear;
head = &snake;
snake.x = 5;
snake.y = 5;
snake.next = NULL;
snake.pre = NULL;
rear = head;
int score = 0;
while (1) {
int m = 1;
Drawmap(&food, head);
menue4(score,max);
//一下两个if均为模式不同而改变的结束条件
if (change == 1) {
if (ifend(head)) {
menue3(score);
cin >> again;
break;
}
}
if (change == 2) {
nonewall(head);
if (newif(head)) {
menue3(score);
cin >> again;
break;
}
}
//当键盘有输入时才改变蛇的移动方向
if (_kbhit())move = setbutton(move);
//做了一个菜单,确认是否退出,防止误触
if (move == 'x') {
int chance;
menue1();
cin >> chance;
if (chance == 1)
return 0;
move = 'd';
}
movethesnake(rear, head, move);
//当蛇吃到食物后:蛇身增长,创造新的食物,分数+10
if (ifSnakemeetFood(head, &food)) {
rear = addSnakeLong(rear, move);
creatfood(&food, m, move);
score += 10;
}
if (yin != 3)
//屏幕刷新率
Sleep(300);
if (yin == 3)
Sleep(1000);
}
if (max < score)
max = score;
}
return 0;
}
(上面那一长段if是我设计的小彩蛋嘻嘻,大家可以忽略掉,不加对游戏也没什么影响)
首先每轮游戏开始前必须要进行初始化(while(again==1)到while(1)中间的所有代码)这点非常重要,因为你第一轮绘制场景的时候蛇头和食物都是要出现的,没有初始化意味着他们就没有坐标也就不会出现在场景上,更别提移动了(虚空移动是吧)
ok接下来就是游戏的逻辑实现了首先将m(食物与蛇身的重叠次数)重新赋值为1,接着绘制场景和记录得分的版块,再根据选择的模式change来用不同的规则判断当前游戏是否结束,然后开始接收键盘上是否有输入以及输入是否有效,如果输入了”Esc“则暂停游戏询问是否确认退出,防止误触;在根据move(移动信号)来确定蛇的下一个位置,然后根据蛇的下一个位置判断蛇头是否吃到了食物,如果吃到了食物就进行以下操作:将蛇身增长,创造一个新的食物,score(得分)+10;最后使其在Sleep处强制停顿0.3s,让人有时间通过场景判断局势。游戏结束使如果score大于max(最高分)则将当前score记录为最高分。
OK,代码方面讲解完成了,大家可以思考一下如果想要增加蛇的移速该怎么做,(我的代码里其实有涉及到一点),如果将Sleep()停顿的时间设计的很短会怎么样。
以上即为我对于贪吃蛇实战的全部感悟以及想法,如有问题,欢迎指正。