前言
第一次写博客的小白来报道了,希望各位大佬手下留情,有不对的地方请多多指教|ू・ω・` ) 这个C语言贪吃蛇的小程序是我的一门课的作业,花了一天的时间才把这个程序弄出来,第一次体会到写BUG的快感(ಥ_ಥ) ,最后想想第一次写了这么多代码,不如把我做的过程记录下来。
环境
VS2019(千万别用Clion!!!)
思路概述
运行效果
代码框架
具体代码(附超详细注释,我自己第一次写的所有思路都写了下来o(一︿一+)o)
建议看完代码先瞅瞅代码后面写的,都是血的教训啊ε(┬┬﹏┬┬)3
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#define DOWN_WALL 20//纵向长度
#define RIGHT_WALL 60//横向长度
typedef struct s_snack//定义结构体蛇的每一节身子,创建链表
{
int x;//x坐标
int y;//y坐标
struct s_snack* next;//蛇的下一节身子
}Snake;
void InitSnake();
void Welcome();
void SetPos(int x, int y);
void DrawBorder();
void Point();
int Move();
int HitWall();
int BiteMyself();
void Food();
int check();
void PlayGame();
void free_snack(Snake* head);
void HideCursor();
int main() {
//system("chcp 65001 > nul");//Clion中解决utf-8编码的方法
HideCursor(); //消除光标(照着抄就完事了,不加就是你的蛇有点丑而已)
Welcome(); //欢迎界面
DrawBorder(); //画方框
InitSnake(); //初始化蛇
Point(); //游戏提示
//游戏界面准备完成
Food(); //随机生成food
PlayGame(); //游戏运行
//free_snack(head); //释放内存
SetPos(5, 23);
return 0;
}
void SetPos(int x, int y)//控制光标的位置
{
/* COORD是Windows API中定义的一种结构体
* typedef struct _COORD
* {
* SHORT X;
* SHORT Y;
* } COORD;
*
*/
COORD pos = { x, y };
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//返回标准输出设备的句柄;获得输出屏幕缓冲区的句柄
SetConsoleCursorPosition(output, pos);//BOOL SetConsoleCursorPosition(HANDLE hOut,COORD pos);
}
void Welcome()//欢迎界面
{
SetPos(25, 6);//定义的移动光标的函数
printf("---------这是一条贪吃蛇---------");
SetPos(25, 9);
printf(" 作者:枫林晚霞");
SetPos(25, 11);
printf("游戏规则:o(≧口≦)o");
SetPos(25, 13);
printf("1、不能撞墙、不能咬到自己");
SetPos(25, 15);
printf("2、本蛇暂时不想支持自动变速,按下W加速,S减速");
SetPos(25, 17);
printf("2、按空格暂停/继续游戏");
SetPos(25, 19);
printf("3、按下Esc退出游戏");
SetPos(25, 21);
printf("(友情提示:请先把输入法切成英文ヽ(ー_ー)ノ)");
printf("\n");
SetPos(25, 23);
system("pause");//暂停
system("cls");//清屏
}
void DrawBorder()//绘制横向DOWN_WALL长度 纵向RIGHT_WALL长度的边框
{
int i;
for (i = 0; i < RIGHT_WALL + 2; i += 2) { //绘制上下边框,(注意: 一段蛇身占用 x 2个单位, y 1个单位 )
SetPos(i, 0);
printf("■");
SetPos(i, DOWN_WALL);
printf("■");
}
for (i = 1; i < DOWN_WALL; i++) { //绘制左右边框
SetPos(0, i);
printf("■");
SetPos(RIGHT_WALL , i);
printf("■");
}
printf("\n\n");
}
void InitSnake()//初始化蛇身,位置(20,15) 注意:结束时head为蛇头,在HitWall函数中会使用
{
int i = 0;
end = (Snake*)malloc(sizeof(Snake)); //动态分配内存,用来存放蛇的身子(不加的话蛇不会变长)
end->x = 10;
end->y = 5;
end->next = NULL;//定义尾部指针,next为空
for (i = 0; i < 3; i++)//开始蛇为3节
{
head = (Snake*)malloc(sizeof(Snake));//定义蛇头
head->x = 10 + 2 * i;
head->y = 5;
head->next = end;//定义蛇身,使next为上一节身体
end = head;//将上一节身体赋值为本节,为下一节准备
}
//此时end为蛇头的head
while (end->next != NULL)
{
SetPos(end->x, end->y);//将光标指向end结构体中的x和y
printf("■");//输出一节蛇身体
end = end->next;//将end的值从蛇头向下一节蛇身传递
}
}
void Point()//游戏旁的提示
{
SetPos(RIGHT_WALL+3, 3);
printf(" 得分:%d ", score);
SetPos(RIGHT_WALL+3, 5);
printf(" 速度:%d毫秒/每次)", speed);
SetPos(RIGHT_WALL+3, 7);
printf(" 最低速600毫秒 最高速10毫秒");
SetPos(RIGHT_WALL + 3, 13);
printf("不能撞墙/咬到自己");
SetPos(RIGHT_WALL + 3, 15);
printf("按空格暂停/继续");
}
void PlayGame()//判断函数,将键盘操作存入游戏进程
{
int feedback = 0;//定义蛇每次移动后的状态反馈变量
key = 'R';//蛇的初始方向为向右
while (1)
{
if ((GetAsyncKeyState(VK_UP) && 0x8000) && key != 'D')//GetAsyncKeyState(键值 && 0x8000)是为了防止其他按键干扰
{
key = 'U'; //注意:是键入向右且原方向不为向左
}
else if ((GetAsyncKeyState(VK_DOWN) && 0x8000) && key != 'U')//https://blog.csdn.net/six_sex/article/details/8653509 (关于此函数的解释)
{
key = 'D';
}
else if ((GetAsyncKeyState(VK_RIGHT) && 0x8000) && key != 'L')
{
key = 'R';
}
else if ((GetAsyncKeyState(VK_LEFT) && 0x8000) && key != 'R')//以上四个判断为判断键入上下左右四个键
{
key = 'L';
}
else if (GetAsyncKeyState(VK_SPACE) && 0x8000)//当键入空格时的处理方法
{
while (1)
{
Sleep(100);//消抖
if ((GetAsyncKeyState(VK_SPACE) && 0x8000))//当再次按下空格,跳出循环,游戏继续
break;
}
}
else if ((GetAsyncKeyState(VK_ESCAPE) && 0x8000))//当按下Esc,退出游戏
{
return;
}
else if ((GetAsyncKeyState('S') && 0x8000) && speed <= 600)//'W'和'S'分别为提升10毫秒速度和降低10毫秒速度
{
speed += 10;
SetPos(RIGHT_WALL + 3, 5);
printf(" 速度:%d毫秒/每次)", speed);
}
else if ((GetAsyncKeyState('W') && 0x8000) && speed > 10)//最大速度为10毫秒动一次,最小速度为600毫秒动一次
{
speed -= 10;
SetPos(RIGHT_WALL + 3, 5);
printf(" 速度:%d毫秒/每次)", speed);
}
feedback = Move();//使反馈系数等于移动函数的返回值,在这里蛇动了Move()函数。
if (feedback == 1)//当移动函数返回1时,游戏结束
{
SetPos(15, DOWN_WALL + 2);
printf("请按Esc退出\n");
while (1) {
if (GetAsyncKeyState(VK_ESCAPE) && 0x8000)
break;
}
}
}
}
int Move()//蛇的移动和边长函数
{
if ((key != 'U') && (key != 'D') && (key != 'R') && (key != 'L'))//当key为上下左右以外的值时不做移动
{
return 0;
}
if (HitWall() || BiteMyself())//判断是否撞墙或者咬到自己,两个函数均为int型,返回有1则Move函数直接返回1
{
return 1;
}
p = (Snake*)malloc(sizeof(Snake));//定义辅助指针
p->next = head;//指向蛇头
switch (key)//对上下左右进行移动
{
case 'U':
p->y = head->y - 1;
p->x = head->x;
break;
case 'D':
p->y = head->y + 1;
p->x = head->x;
break;
case 'R':
p->x = head->x + 2;//方块占两个格
p->y = head->y;
break;
case 'L':
p->x = head->x - 2;
p->y = head->y;
break;
}
head = p;//在贪吃蛇的头部添加一个称为新的头 ,相当于是贪吃蛇移动一格
//如果 移动的一格刚好是食物的位置,新增的称为蛇头,不用去掉蛇尾
//如果 移动的一格不是食物的位置,新增的称为蛇头,去掉蛇尾,就是贪吃蛇移动一格的效果
/*printf("■");
Sleep(speed);*/
//如果直接将这两句放在外面,而不是在if里面,就会造成蛇在吃苹果时
//苹果先消失蛇长不变,然后下一步再增长一节,这个是程序执行的顺序的问题
//而且,如果写在外面当按暂停时会造成少一格的问题
if (p->x == foodx && p->y == foody)//当p与食物重合时
{
Food();//重新生成食物
score += 10;//加分
SetPos(RIGHT_WALL + 3, 3);
printf(" 得分:%d ", score);
SetPos(p->x, p->y);
printf("■");//只增加蛇头,不减去蛇尾
Sleep(speed);
}
else {
q = p;
while (p->next->next != NULL)
{
p = p->next;
}
SetPos(q->x, q->y);
printf("■");
SetPos(p->x, p->y);//否则增加蛇头,将蛇尾输出“ ”
printf(" ");//两个空格
free(p->next);
p->next = NULL;
p = head;//释放蛇尾的空间
Sleep(speed);
}
return 0;
}
int HitWall()//如果蛇头的x,y坐标等于墙的坐标,则返回1,输出“游戏结束”
{
if (head->x == 0 || head->y == 0 || head->x == RIGHT_WALL - 2 || head->y == DOWN_WALL)//或!
{
SetPos(15, DOWN_WALL+1);
printf("游戏结束!你撞到墙了( _ _)ノ|\n");
SetPos(15, DOWN_WALL+1);//令 ‘按任意键继续...’居中显示
return 1;
}
return 0;
}
int BiteMyself()//比较head和每一个p的x,y,如果相同,返回1,游戏结束
{
p = head;//令辅助指针p为等于head
while (p->next != NULL)//从head开始遍历整个蛇身,直到p->为NULL,即为蛇尾
{
p = p->next;
if (head->x == p->x && head->y == p->y)
{
SetPos(15, DOWN_WALL + 1);
printf("游戏结束!你咬到你自己了\n");
SetPos(15, DOWN_WALL + 1);//令 “请按任意键继续”居中显示
return 1;
}
}
return 0;
}
void Food()//在这里本来是用p指针的,但是当我加上ifood=1时,蛇吃食物的过程出错,应该是指针空间分配的问题,把指针换成f即可
{
srand(time(NULL));//播下随机数种子
while (1)
{
foody = rand() % (DOWN_WALL - 1) + 1;
foodx = rand() % (RIGHT_WALL - 4) + 3;//将随机数赋给食物坐标,范围为(1,边框-1)
if (foodx % 2 == 0)//食物横坐标必须为偶数,因为方框为两格
{
break;
}
}
while (ifood != -1) //循环检查函数,当check()返回0时继续运行,直接返回check值不行
{
check();
}
SetPos(foodx, foody);
printf("☆");//显示食物
/*SetPos(RIGHT_WALL + 3, 16);
printf("%d,%d", foodx, foody);*/
ifood = 1;//重置判断条件,否则只能跳过食物第一次在蛇身上的情况
}
int check()//检查食物的生成是否在蛇身上(为了避免用goto函数)
{
f = head;
while (f->next != NULL)//从蛇头开始检测,直到蛇尾
{
if (foodx == f->x && foody == f->y)//当检测出食物在蛇的某一节上时,重新赋值,并且重新循环
{
srand(time(NULL));
while (1)
{
foody = rand() % (DOWN_WALL - 1) + 1;
foodx = rand() % (RIGHT_WALL - 4) + 2;//将随机数赋给食物坐标,范围为(1,边框-1)
if (foodx % 2 == 0)//食物横坐标必须为偶数,因为方框为两格
{
break;
}
}
ifood = 1;
return 0;
}
f = f->next;
}
if (f->next == NULL)//如果循环到蛇尾依旧没有返回,则返回0,令ifood为-1,跳出循环
ifood = -1;
return 0;
}
void free_snack(Snake* head)//释放资源后老是报错,所以主函数我也就没加上去,就把代码放这了
{
if (head == NULL || head->next == NULL)
{
return;
}
while ((p = head) != NULL)
{
free(p);
head = head->next;
}
}
void HideCursor()//隐藏光标
{
CONSOLE_CURSOR_INFO cursor_info = { 1, 0 };
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}
本代码参考:https://blog.csdn.net/nanfeibuyi/article/details/82724532
注意:
1.编译器一定要用对,如果用的是老版Devc++,它的编码格式不是UTF-8,如果用CLion,那么这个代码,我感觉可以不用写了,那个不是cmd的结果输出把整个框架全乱了!!!(▼ヘ▼#)如果有大佬知道怎么调成控制台输出可以在评论说一下,我用的VS,至于VC++没用过,受不了那个界面,太丑了o(´^`)o。
2.代码中的■为两个单位,凡是进行横向操作的必须是将其乘以2,比如:食物生成的x坐标必须是偶数,在设为填充空格时需要填充两个“ ”。
3.Void函数可以有return 直接写reutrn;(不知道是否标准,有争议)
4.GetAsyncKeyState函数为无缓冲输入,即输入范围为整个程序,无论在任何位置键入参数值,均触发函数。
5.要时刻清楚指针指向的空间。
6.调试模式真的好用ヽ( ̄▽ ̄)ノ
结语
有想要源代码小伙伴可以私信我,这个代码块好烦啊,中间好像不能有空格吧,粘的累死我了。因为第一次写博客,所以不太熟,以后会慢慢写一点。