贪吃蛇的实现,基于windows操作系统

news/2025/2/21 8:02:14/

前言:

贪吃蛇从学习到真正实现花了9天实现,第一二天第一次学习,第三四五天第二次学习,第六七八天一边实现一边思考,才完成了贪吃蛇的代码。实现了贪吃蛇以后已经接近过年,我想自己再根据掌握的知识制作烟花燃烧绽放的场景。贪吃蛇的移动和烟花的移动原理是一样的,贪吃蛇的头插删尾,使我能够处理烟花消失的部分。在我花两天时间写完500行的烟花代码后,对贪吃蛇的实现原理也更加了解了。然后再写下这篇文章。

写作过程中遇到的问题:有很多,但最多的是,写着写着不知道当前要实现什么。思路不清晰。然后去看正确的代码。以及对指针的掌握不够,要使用的函数比较生疏,令人烦躁,静不下心去理解等等。之后通过各种途径一一克服了。

一、分析和规划贪吃蛇的思路

1.一个已经懂得贪吃蛇怎样写的人和一个从没写过贪吃蛇怎么写,第一次上手的思维是不一样的。但是第一部应该都需要了解自己要实现哪些功能。然后划分为不同阶段逐个完成。

2.根据想要制作的成品模样,画出草图和X-mind思维导图。

                                                          成图1,欢迎页面一

                                                        成图2,欢迎页面二

                                                      成图3,游戏页面

     根据贪吃蛇游戏框,绘出墙体食物和蛇。

接下来是X-mind思维导图,写出整个程序的脉络。然后一一落实。

二、落实想法

1.建立三个文件。snake.c  snake.h  test.c.

2.贪吃蛇有两个结构体。

一个是蛇身,它是由一个个结点组成。

这个结点里放蛇身的坐标和下一个结点的指针,记住下一个结点的位置。放坐标是因为蛇在移动的时候要打印蛇身,蛇身的位置是依靠坐标来确定的,这样打印的时候就能找到蛇身的位置了。

这个蛇身由单向不带头不循环链表----单链表构成。

创建一个结构体,以及一个指向结构体的指针并将它们重命名。

typedef struct SnakeNode
{int x;int y;struct SnakeNode * next;
}SnakeNode, *pSnakeNode;

另一个用来维护蛇。如思维导图中写的思路所示。

typedef struct Snake
{pSnakeNode sn;  //指向蛇头的指针pSnakeNode pfood;//指向食物的指针,本质上和蛇身没什么区别,只是它是单个的,没有链。int score;   //分数,每次吃食物要涨粉int foodweight;//当前每吃一个食物增加的分数enum DIRECTION;//方向enum STATUS;//状态int sleeptime;//速度
}Snake,*psnake;//重命名

3.三个文件代码附上

snake.h

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#pragma once
#include <locale.h>
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <wchar.h>
#include <math.h>#define WALL L'□'
#define SNAKE L'●'
#define FOOD L'★'//监测按键
#define KEY_STATE(vkey)   ((GetAsyncKeyState(vkey)&0x1)?(1):(0))
//蛇头方向
enum DIRICTION
{UP=1,DOWN,LEFT,RIGHT
};
//游戏状态
enum STATUS
{OK=1,KILL_BY_SELF,KILL_BY_WALL,ESC
};//蛇身的结点
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//蛇的维护
typedef struct Snake
{pSnakeNode sn;//蛇身,是一个结构体指针pSnakeNode pfood;//也是一个结构体指针,它是一个坐标enum DIRICTION Dir;//蛇的方向enum STATUS Status;//蛇的状态int Score;//游戏当前得分int Foodweight;//食物的分数int Sleeptime;//走一步睡眠时间,和蛇速相关
}Snake,*pSnake;//游戏开始前的准备工作
void GameStart(pSnake snake);//玩游戏
void GameRun(pSnake snake);
//
游戏结束
//void GameEnd();

snake.c

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"//把光标移动到想要的位置
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}void Welcometogame()
{SetPos(38, 13);printf("欢迎来到贪吃蛇小游戏\n");SetPos(60, 25);system("pause");system("cls");//打印游戏说明SetPos(25, 12);printf("用↑.↓.←.→分别控制蛇的移动,F3为加速,F4为减速\n");SetPos(25, 13);printf("加速能得到更高的分数。\n");SetPos(60, 25);system("pause");system("cls");
}//初始化蛇身
void InitSnake(pSnake snake)
{for (int i = 0; i < 5; i++){pSnakeNode p = (pSnakeNode)malloc(sizeof(SnakeNode));if (p == NULL){perror("malloc failed!\n");exit(1);}p->x = 20+2*i;p->y = 6;p->next = NULL;//头插法if (snake->sn==NULL){snake->sn = p;}else{p->next = snake->sn;snake->sn = p;}//打印蛇身,用循环/*if (p){SetPos(p->x, p->y);wprintf(L"%lc", SNAKE);}*/while (p){SetPos(p->x, p->y);wprintf(L"%lc", SNAKE);p = p->next;}}//其他信息初始化snake->Dir = RIGHT;snake->Foodweight = 10;snake->pfood = NULL;snake->Score = 0;snake->Sleeptime = 200;snake->Status = OK;}void CreatMap()
{int i = 0;for (i = 0; i < 57; i += 2){wprintf(L"%lc", WALL);}for (i = 0; i <= 26; i++){SetPos(56, i);wprintf(L"%lc\n", WALL);}SetPos(0, 26);for (i = 0; i < 57; i += 2){wprintf(L"%lc", WALL);}SetPos(0, 1);for (i = 0; i <= 25; i++){wprintf(L"%lc\n", WALL);}
}void CreatFood(pSnake snake)
{int x = 0;int y = 0;
again:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//不能在蛇身上pSnakeNode cur = snake->sn;while (cur){if (cur->x != x || cur->y != y){cur = cur->next;}else{goto again;}}//申请食物的结点pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("malloc failed!\n");exit(1);}pFood->x = x;pFood->y = y;snake->pfood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);}
//游戏开始前的准备工作
void GameStart(pSnake snake)
{//设置一下控制台大小 system("mode con cols=100 lines=30");//更改控制台名字system("title 贪吃蛇");//隐藏光标HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO Cursorinfo;GetConsoleCursorInfo(handle, &Cursorinfo);Cursorinfo.bVisible = false;SetConsoleCursorInfo(handle, &Cursorinfo);//打印欢迎界面Welcometogame();//绘制地图CreatMap();//初始化蛇身InitSnake(snake);//打印食物CreatFood(snake);}void PrintHelpInfo()
{SetPos(61, 16);printf("1.不能撞墙,不能咬到自己\n");SetPos(61, 17);printf("2.用↑.↓.←.→分别控制蛇的移动\n");SetPos(61, 18);printf("3.F3为加速,F4为减速\n");SetPos(61, 19);printf("4.加速可以获得更多分数\n");SetPos(80, 22);printf("制作者:真白");
}void EatFood(pSnake snake, pSnakeNode pnext)
{//吃食物,则头插pnext->next = snake->sn;//食物的下一个结点连接蛇头snake->sn = pnext;//把食物的结点给蛇头//打印蛇身pSnakeNode cur = snake->sn;//创建一个cur指针来循环while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", SNAKE);cur = cur->next;}//分数变化snake->Score += snake->Foodweight;//释放旧的食物结点free(snake->pfood);//创建新的食物结点CreatFood(snake);
}void NotEatFood(pSnake snake,pSnakeNode pnext)
{//正常走,头插,删尾pnext->next = snake->sn;snake->sn = pnext;//删尾,创建一个指针循环pSnakeNode cur = snake->sn;while (cur->next->next){SetPos(cur->x, cur->y);//先设置坐标再打印wprintf(L"%lc", SNAKE);//打印蛇身cur = cur->next;}SetPos(cur->next->x, cur->next->y);//先设置坐标再打印尾处的空白printf("  ");free(cur->next);//释放尾结点cur->next = NULL;
}void IsItFood(pSnake snake, pSnakeNode pnext)
{if (pnext->x == snake->pfood->x && pnext->y == snake->pfood->y){//是食物,吃掉EatFood(snake,pnext);}else{//不是食物,不吃NotEatFood(snake,pnext);}
}void KillByWall(pSnake snake, pSnakeNode pnext)
{//下一个结点的位置是不是墙的坐标if (pnext->x == 0 || pnext->y == 0 || pnext->x == 56 || pnext->y==26){snake->Status = KILL_BY_WALL;}
}
void KillBySelf(pSnake snake, pSnakeNode pnext)
{//下一个结点是不是蛇身pSnakeNode cur = snake->sn->next;while (cur){if (pnext->x != cur->x || pnext->y != cur->y){cur = cur->next;}else {snake->Status = KILL_BY_SELF;break;}}}void SleepTime(pSnake snake)
{Sleep(snake->Sleeptime);
}
void Snakemove(pSnake snake)
{//创建蛇的下一个位置的结点pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pnext == NULL){perror("Snakemove:: malloc");return;}pnext->next = NULL;//只需要这个结点,不需要它下一个结点的信息//安排pnext的坐标switch (snake->Dir){case UP: pnext->x = snake->sn->x;pnext->y = snake->sn->y - 1;//如果按了上,下一个坐标的位置就在蛇头的上面break;case DOWN: pnext->x = snake->sn->x;pnext->y = snake->sn->y + 1;//同上,下面同上break;case LEFT:pnext->x = snake->sn->x - 2;pnext->y = snake->sn->y;break;case RIGHT:pnext->x = snake->sn->x + 2;pnext->y = snake->sn->y;break;}//判断下一个结点是否是食物IsItFood(snake, pnext);//判断下一个节点是否撞墙KillByWall(snake,pnext);//判断下一个节点是否咬到自己KillBySelf(snake,pnext);}
void Pause()
{while (1){Sleep(200);if (KEY_STATE(VK_SPACE) == 1){break;}}
}void F3(pSnake snake)
{//休眠时间限制,5档 200,170,140,110,80if (snake->Sleeptime <= 200 && snake->Sleeptime >= 110){snake->Sleeptime -= 30;snake->Foodweight += 2;}
}void F4(pSnake snake)
{if (snake->Sleeptime >= 80 && snake->Sleeptime <= 170){snake->Sleeptime += 30;if (snake->Foodweight >= 4){snake->Foodweight -= 2;}}
}
//玩游戏
void GameRun(pSnake snake)
{//打印帮助信息PrintHelpInfo();do{//当前分数情况SetPos(60, 10);printf("得分:%d  ", snake->Score);printf("每个食物得分:%02d\n", snake->Foodweight);//监测当前按键情况if (KEY_STATE(VK_UP) == 1 && snake->Dir != DOWN){snake->Dir = UP; }else if(KEY_STATE(VK_DOWN) == 1&& snake->Dir != UP){snake->Dir = DOWN;}else if (KEY_STATE(VK_LEFT) == 1&& snake->Dir != RIGHT){snake->Dir = LEFT;}else if (KEY_STATE(VK_RIGHT) == 1&& snake->Dir != LEFT){snake->Dir = RIGHT;}else if (KEY_STATE(VK_ESCAPE) == 1){snake->Status = ESC;break;}else if (KEY_STATE(VK_SPACE) == 1){Pause();}else if (KEY_STATE(VK_F3) == 1){F3(snake);}else if (KEY_STATE(VK_F4) == 1){F4(snake);}//蛇的移动Snakemove(snake);//移动一个位置,休眠一下SleepTime(snake);} while(snake->Status==OK);}void GameEnd(pSnake snake)
{SetPos(15, 12);switch (snake->Status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_WALL:printf("很遗憾,你撞墙了,游戏结束\n");break;case KILL_BY_SELF:printf("很遗憾,你咬到自己了,游戏结束\n");break;}pSnakeNode cur = snake->sn;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(snake->pfood);snake = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"void test()
{int ch = 0;do{Snake s1 = { 0 };GameStart(&s1);GameRun(&s1);GameEnd(&s1);SetPos(15, 14);printf("再玩一把吗?Y/N :");ch = getchar();getchar();} while (ch == 'Y' || ch == 'y');}//
int main()
{setlocale(LC_ALL, "");//本地化,头文件<locale.h>test();SetPos(0, 27);return 0;}

三、整个贪吃蛇实现的难点

1.windows系统提供的API的一些接口和功能。

在学C语言的时候没有接触,所以要了解一下需要掌握的函数用法。

如何修改控制台的大小?

通过包含windows.h的库函数,可以使用windows命令提示符的一些命令。来达到修改控制台大小的效果。在程序结束前,都是这个大小。system("mode con cols 100 lines 30")

同样的,修改控制台的标题也是利用windows命令提示符的一些命令来修改。

如何隐藏光标?

首先是要获得控制台的句柄。句柄就相当于控制台的钥匙,获取句柄,就是获取这个控制台的信息,(这些信息使得这个控制台与其他控制台相区别)我们得到的这个控制台的句柄,只能操作这个控制台的独特信息,不能修改其他控制台的独特信息。提供的函数是GetStdHandle,它有一个参数,但可以选择填入的参数有三个,为了获取句柄,填入的是STD_OUTPUT_HANDLE。这个函数返回的类型是HANDLE。因此也要创建一个HANDLE 类型的变量来接收返回的句柄。

    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); 
    CONSOLE_CURSOR_INFO Cursorinfo;      创造一个光标的变量,前面是类型,后面是变量,这是在下一句获取光标信息要用到。提前创建一个变量。
    GetConsoleCursorInfo(handle, &Cursorinfo);    传本控制台的句柄和一个光标变量的地址。传入这两个变量以后,就能获取本控制台的光标信息。本控制台的光标信息,会被复制到Cursorinfo这个变量上。
    Cursorinfo.bVisible = false; 这个Cursorinfo是一个结构体变量,它有两个成员,一个是光标占一个坐标的比例(0-100)。第二个是是否可见。把bVisible设置为ture就是可见,false就是不可见。通过这一步,就把光标设置为隐藏。
    SetConsoleCursorInfo(handle, &Cursorinfo);这个函数是设置光标信息。传入本控制台的句柄,再把设置好的光标信息传进去。从而改变本控制台的光标。

如何设置坐标位置?

首先,什么是坐标?

以此图为例,x坐标是横坐标,从左向右延申,y坐标是纵坐标,从上至下延申,第一个位置是原点。

如何设置坐标位置:

void SetPos(int x, int y)  只要传入坐标的位置,就可以把光标移到想要的地方。把他封装成一个函数
{
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    获取本控制台的句柄,因为把光标移到指定坐标(的函数)也需要本控制台的句柄(作为它的参数)。
    COORD pos = { x,y }; 这是一个坐标的结构体,COORD是一个结构体类型,里面是坐标的参数。这一步是设置pos作为一个结构体坐标变量,成员初始化为x,y。
    SetConsoleCursorPosition(handle, pos);这是一个设置光标位置的函数,需要传两个参数,一个是本控制台句柄,一个是指定的坐标信息。
}

通过这些代码就可以把光标移动到指定位置,然后在指定位置打印出想要的信息。

2.绘制地图

地图的墙是宽字符,打印宽字符要先本地化。使用的函数是wprintf,w表示wide的意思。

3.初始化蛇身

初始化蛇身需要做到申请蛇的结点,每个结点的坐标关系是怎样的,在于你想要蛇一开始在地图的哪个位置出现。蛇的结点之间的连接采用头插法。把蛇的结点都创建出来以后,还要打印一遍蛇身。最后再初始化蛇的状态、速度、食物分数、分数等信息。

4.打印食物

食物的特征,本质上是一个结点。坐标要随机生成,不能生成在墙上,x坐标得是2的倍数,因为和蛇对称,和墙也要对称。坐标不能在蛇的身上。设置好相关条件就可以创建结点了。然后把它初始化为已经生成的坐标。

5.蛇的移动

蛇是走一步移动一步,它要监测是否有按上下左右,如果按了,蛇头就要转变方向。蛇的移动本质上是下一个结点的位置在哪。所以要安排下一个结点的位置。下一个结点的位置有多种可能,撞墙,撞自己,吃食物和正常进行。

如果是吃食物,就是头插。

如果是正常走,那么就是头插以后再删尾,这个过程还要打印一遍蛇身。那么尾巴部分的结点被释放以后,在尾结点的原坐标上打印两个空格来代替。这个过程在地图上显示就是蛇走了一格。

蛇移动的速度越快,休眠的时间越短。所以速度方面设置休眠时间就可以了。这个休眠时间Sleep的函数也是包含windows的库函数来实现的。

撞墙和撞自己都需要修改游戏状态。修改的游戏状态就在于停止游戏。所以外面要套个循环。游戏只在状态是OK的时候进行,其他情况都分别打印出对应的信息。

6.暂停功能如何实现?

暂停的功能可以通过死循环,一直在睡眠。只有重新按了空格键,再跳出循环继续运行。

7.最后的收尾

收尾部分主要是游戏玩了一把game over以后,因为各种原因结束而打印不同信息。打印完了要把蛇的结点依次释放。然后再把食物释放,把传来的维护蛇的指针置为空。

如果想设置再来一把的消息,可以在test.c文件里进行。

要注意两个getchar。第一个getchar用来读取信息,通过一个变量来接收,用于判断玩家到底要不要开下一把。第二个getchar用来接收读取回车字符,但是没有变量接收它,也就是它不产生实际作用。因为它的目的只是用来吸收回车,使这个回车键不至于影响到下一次的输入判定。

8.贪吃蛇的结构体维护

贪吃蛇有两个结构体,第一个结构体是蛇身,第二个是蛇的各种信息,里面也包含了蛇身。

那么就是创建一条贪吃蛇的结构体,来玩这个贪吃蛇游戏。传的参数就是这个贪吃蛇的结构体的地址,因为传地址,才能改变贪吃蛇的值。这里的值包括状态,蛇的结点的指针,蛇的方向,当前分数等等。蛇的结点的头指针是常常需要改变的,因为它要不断移动。如果传的是结点指针,那么要传二级指针。但是本次情况中蛇的结点的头指针在贪吃蛇结构体里只是一个值,这里既然传了贪吃蛇的结构体地址,那么就能随便改变蛇的结点的头指针了。这个是要注意的关于指针的细节。以便在进行与贪吃蛇相似的项目中能够复用。


http://www.ppmy.cn/news/1350682.html

相关文章

Swift Combine 管道 从入门到精通三

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二 1. 用弹珠图描述管道 函数响应式编程的管道可能难以理解。 发布者生成和发送数据&#xff0c;操作符对该数据做出响应并有可能更改它&#xff0c;订阅者请求并接收这些数据。 这…

LocalAI 部署(主要针对 mac m2 启动)

LocalAI 部署 介绍 LocalAI 是免费的开源 OpenAI 替代方案。 LocalAI 充当 REST API 的直接替代品&#xff0c;与本地推理的 OpenAI API 规范兼容。 它无需 GPU&#xff0c;还有多种用途集成&#xff0c;允许您使用消费级硬件在本地或本地运行 LLM、生成图像、音频等等&#…

Linux系统安全——iptables相关总结

在使用iptables时注意要先关闭firewalld&#xff08;systemctl stop firewalld.service&#xff09; 1.查看iptables规则 iptables -vnL 选项含义-v查看时显示更多详细信息-n所有字段以数字形式显示-L查看规则列表 例&#xff0c;拒绝来自192.168.241.22的源地址 直接丢弃 …

2.13作业

数组练习 1、选择题 1.1、若有定义语句&#xff1a;int a[3][6]; &#xff0c;按在内存中的存放顺序&#xff0c;a 数组的第10个元素是D A&#xff09;a[0][4] B) a[1][3] C)a[0][3] D)a[1][4] 1.2、有数组 int a[5] {10&#xff0c;20&#xff0c;30&#xff0c;40&#xf…

AlmaLinux右键菜单(基于GNOME桌面)

文章目录 前言前提说明在文件上右键在文件夹上右键 前言 在使用VSCode的过程中&#xff0c;AlmaLinux没能像Windows一样在右键菜单上显示打开方式&#xff0c;所以找了一下解决方案&#xff0c;罗列出来 前提说明 虽然说无论是media还是StackOverflow都推荐使用这条命令&…

牛客错题整理——C语言(实时更新)

1.以下程序的运行结果是&#xff08;&#xff09; #include <stdio.h> int main() { int sum, pad,pAd; sum pad 5; pAd sum, pAd, pad; printf("%d\n",pAd); }答案为7 由于赋值运算符的优先级高于逗号表达式&#xff0c;因此pAd sum, pAd, pad;等价于(…

论文阅读-One for All : 动态多租户边缘云平台的统一工作负载预测

论文名称&#xff1a;One for All: Unified Workload Prediction for Dynamic Multi-tenant Edge Cloud Platforms 摘要 多租户边缘云平台中的工作负载预测对于高效的应用部署和资源供给至关重要。然而&#xff0c;在多租户边缘云平台中&#xff0c;异构的应用模式、可变的基…

Junit常用注解

注解是方法的“标签” 说明每个方法的“职责” Q:总共有那些注解? 参见官方的API文档 0.常用主机及其特点 BeforeClass 只会执行一次必须用static修饰常用来初始化测试需要的变量 Before 会执行多次&#xff08;只要写一次&#xff09;在每个Test执行执行之前执行可以和…