完整版! 大一新手C语言数据结构实现简单贪吃蛇游戏

    技术2026-03-06  6

    大一新手C语言数据结构实现简单贪吃蛇游戏含源代码

    贪吃蛇1.前言简述2.编写语言及工具3.游戏的头文件代码(.h)3.1map.h3.2snake.h3.3Food.h 4.游戏的源文件代码(.c)4.1.地图模块(map.c)4.2.蛇模块(snake.c)4.3.食物模块(Food.c)4.4.主函数模块(main.c) 5.游戏中知识点分析5.1关于线程与进程5.2光标定位函数5.3链表实现蛇的创建和移动5.4MainLop实现键盘控制方向5.5随机食物的出现5.6Sleep函数控制速度 结尾

    贪吃蛇

    1.前言简述

    学习编程快一年了,偶然看到网上有做贪吃蛇的视频,于是便一时兴起,用两天时间跟着视频做了起来,在制作过程中也遇到不少问题和陌生的知识,因此通过想博客与大家分享,讨论。(游戏虽然做出来了,但笔者自己也有很多问题一知半解,如有解释不当,欢迎各位解答指正!)

    2.编写语言及工具

    语言:C语言 工具:Visual Studio 2019

    3.游戏的头文件代码(.h)

    注:整个游戏包含7个头文件(3个.h文件,4个.c文件)

    .h文件 中一般放的是同名.c文件中定义的变量、数组、函数的声明,声明后.c文件可以直接使用。.c文件一般放的是变量、数组、函数,无需再重复定义。使用.h文件和.c文件的原因主要是为了解决文件编译时重复声明的问题,相对来说使代码结构更清晰(改bug也更快)。

    3.1map.h

    #pragma once //这个pragma相当于编译指示,我是通过vs2019的类导向功能同时创建.h和.c文件自动出现的,不加上面这句也可以 enum { row = 26, col = 26 }; //初始化二维数组 void InitWall(); //输出边界墙 void DrawWall(); //设置场景中的字符 void SetWall(int x, int y, char key); //获取场景中的字符 char GetWall(int x, int y); //字符二维数组 char GameArray[row][col]; //移动光标打印字符 void PrintChar(int x,int y,char key);

    3.2snake.h

    #pragma once //#define _CRT_SECURE_NO_WARRINGS #include <stdio.h> #include <stdlib.h> #include <string.h> //定义结点结构体 struct Point { int x; int y; struct Point* next; }; //定义运动方向 enum EDiection { up='w',down='s',left='a',right='d' }; //初始化蛇 void InitSnake(); //销毁蛇 void DestroyPoint(); //添加结点 void AddPoint(int x, int y); //蛇头指针 struct Point* pHead; //删除结点 void DelPoint(); enum EDiection direction; //蛇头的坐标,这个决定了游戏是否继续,非常重要!!!!! int x; int y;

    3.3Food.h

    #pragma once void SetFood(); int FoodX; int FoodY;

    4.游戏的源文件代码(.c)

    4.1.地图模块(map.c)

    #include "map.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> //初始化二维数组 void InitWall() { for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { if (i == 0 || j == 0 || i == row - 1 || j == col - 1) { GameArray[i][j] = '*';//边界围墙 } else { GameArray[i][j] = ' ';//活动区域 } } } } //输出边界墙 void DrawWall() { for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { printf("%c ", GameArray[i][j]); } switch (i) { case 5:printf("贪吃蛇游戏"); break; case 9:printf("操作提示:"); break; case 11:printf("上: w"); break; case 13:printf("下: s"); break; case 15:printf("左: a"); break; case 17:printf("右: d"); break; default:break; } printf("\n"); } } //设置场景中的字符 void SetWall(int x, int y, char key) { GameArray[x][y] = key; } //获取场景中的字符 char GetWall(int x, int y) { return GameArray[x][y]; } //移动光标定位 void GotoXY(HANDLE hout,int x,int y) { COORD pos; pos.X = x; pos.Y = y; SetConsoleCursorPosition(hout,pos); } //移动光标打印字符 void PrintChar(int x, int y, char key) { HANDLE ss = GetStdHandle(STD_OUTPUT_HANDLE); GotoXY(ss, x, y); printf("%c", key); }

    4.2.蛇模块(snake.c)

    #include "snake.h" #include "map.h" #include <stdio.h> #include <stdlib.h> #include <string.h> //初始化蛇 void InitSnake() { pHead = NULL; direction = right; //数组的坐标 AddPoint(5,3); AddPoint(5,4); AddPoint(5,5); AddPoint(5,6); } //添加结点 void AddPoint(int x, int y) { //创建新的结点 struct Point* NewPoint = (struct Point*)malloc(sizeof(struct Point)); if (NewPoint == NULL) { //判断结点是否创建成功 return; } //新结点赋值对应的x,y坐标 NewPoint->x = x; NewPoint->y = y; NewPoint->next = NULL; if (pHead != NULL) { //pHead != NULL则为老蛇头存在,将其改变为身体 SetWall(pHead->x, pHead->y, '='); PrintChar(pHead->y * 2, pHead->x, '='); } //NewPoint作为新的蛇头pHead,老蛇头被NewPoint—>next指向 NewPoint->next = pHead; pHead = NewPoint; //数组的坐标为y为横坐标,x为纵坐标,往下x增加,往右是y增加 //光标的坐标为x为横坐标,y为纵坐标,往下y增加,往右是x增加 SetWall(pHead->x, pHead->y, '@'); PrintChar(pHead->y * 2, pHead->x, '@'); //销毁蛇 void DestroyPoint() { struct Point* cur = pHead; while (pHead != NULL) { cur = pHead->next; free(cur); pHead = cur; } } //删除结点 void DelPoint() { //两个结点以上才能删除 if (pHead == NULL || pHead->next == NULL) { return; } struct Point* pre = pHead; struct Point* cur = pHead->next; while (cur->next!=NULL) { pre = pre->next; cur = pre->next; } SetWall(cur->x, cur->y, ' '); PrintChar(cur->y * 2, cur->x, ' '); free(cur); cur = NULL; pre->next = NULL; } }

    4.3.食物模块(Food.c)

    #include <time.h> //设置食物 void SetFood() { srand((unsigned int)time(NULL)); while (1) { FoodX = rand() % (row - 2) + 1; FoodY = rand() % (col - 2) + 1; if (GetWall(FoodX, FoodY) == ' ') { SetWall(FoodX, FoodY,'$'); PrintChar(FoodY * 2, FoodX, '$'); break; } } }

    4.4.主函数模块(main.c)

    #include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> #include <process.h> #include "map.h" #include "snake.h" #include "Food.h" char ch = NULL; void MainLop() { char prekey = NULL; while (1) { ch =_getch(); //如果是蛇没有运动 if (ch == NULL && prekey == NULL) { continue; } //如果输入方向恰和蛇运动方向相反 if((ch == up && prekey == down)||(ch == down && prekey == up) ||(ch==left&&prekey==right)||(ch == right && prekey == left)) { ch = prekey; } else { prekey = ch; } switch (ch) { case 'w':direction = up; break; case 's':direction = down; break; case 'a':direction = left; break; case 'd':direction = right; break; default:break; } } } //初始化分数 int score = 0; //初始化速度 int vspace = 200; void main() { InitWall(); DrawWall(); SetFood(); InitSnake(); //开启线程 HANDLE h; h = (HANDLE)_beginthread(MainLop,0,NULL); while (1) { //开始蛇不会移动,按任意键启动游戏 if (ch == NULL) { continue; } //通过判断分数提升关卡难度 switch (score) { case 5:vspace = 100; break; case 10:vspace = 70; break; default: break; } Sleep(vspace); x = pHead->x; y = pHead->y; switch (direction) { case up:x--; break; case down:x++; break; case left:y--; break; case right:y++; break; default: break; } //若蛇头碰到墙或者自己身体游戏结束 if (GetWall(x, y) == '*' || GetWall(x, y) == '=') { break; } //若吃到食物 if (GetWall(x, y) == '$') { score++; AddPoint(x, y); //再设置新的食物 SetFood(); } else//正常移动 { AddPoint(x, y); DelPoint(); } } PrintChar(0, row, ' '); printf("最终得分为:\t%d分\n", score); system("pause"); }

    5.游戏中知识点分析

    前面只放了源代码没有解释,该部分主要针对上面的代码值得推敲的地方,再重新拿出来分析。同时在这部分代码分析中有些地方笔者解释的不够透彻,所以放上链接帮助大家深入了解学习。

    5.1关于线程与进程

    首先,本游戏要对线程进程有一定了解。 关于进程和线程的概念,可参照: 链接: C语言多线程编程-进程和线程的基本概念.

    在贪吃蛇游戏里,也采用了多线程编程,首先由main函数开启一个进程,然后在一个进程里再分出两个线程。 线程1:蛇的移动。 线程2:通过键盘控制蛇的方向 注:线程也不能多个同时执行,多线程程序也是由cpu轮流执行的,cpu在各线程里来回执行的速度很快,从用户角度上像是几个线程同时执行。

    5.2光标定位函数

    目的:确定蛇的坐标,为蛇在屏幕的显示和移动做准备。

    核心代码:

    //一定要导入该头文件! #include <Windows.h> void GotoXY(HANDLE hout,int x,int y) { COORD pos; pos.X = x; pos.Y = y; SetConsoleCursorPosition(hout,pos); } void PrintChar(int x, int y, char key) { HANDLE ss = GetStdHandle(STD_OUTPUT_HANDLE); GotoXY(ss, x, y); printf("%c", key); }

    代码分析:首先介绍两个结构体HANDLE和COORD

    COORD是描述位置的结构体,包含x, y坐标 typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD;

    HANDLE HANDLE是一种指向结构体的指针,同时也是处理对象的接口,也称句柄,你可以通过句柄来操作程序所涉及的对象。

    SetConsoleCursorPosition函数 功能:设置控制台光标坐标实现定位 参数:第一个参数类型为HANDLE,第二个参数类型为COORD

    GetStdHandle函数 功能:获取指定的标准设备的句柄 参数:有三种可能的参数取值 STD_INPUT_HANDLE—标准输入句柄

    STD_OUTPUT_HANDLE—标准输出句柄

    STD_ERROR_HANDLE—标准错误句柄

    注:GetStdHandle函数的返回值必须由HANDLE类型的变量接收。且标准输出均是通过屏幕

    综上小结 PrintChar函数: 参数:打印字符的x坐标、y坐标、打印的字符。 过程:PrintChar函数先定义了HANDLE类型的ss接收GetStdHandle函数的标准输出句柄。再调用GotoXY函数传入参数x、y坐标和HANDLE指针,GotoXY函数再调用SetConsoleCursorPosition函数确定打印位置坐标,就实现了蛇在屏幕上的打印。

    相关知识的学习来源:控制台API函数----HANDLE、SetConsoleCursorPosition、SetConsoleTextAttribute

    5.3链表实现蛇的创建和移动

    整个蛇的创建和移动都是通过链表实现的。

    //蛇由一个个结点组成,这里先定义结点 struct Point { int x; int y; struct Point* next; }; //蛇头的坐标,这个决定了游戏是否继续,非常重要!!!!! int x; int y; //蛇头指针 struct Point* pHead; 先将蛇打印在屏幕上 添加结点的函数上面有源代码就不列出了 //初始化蛇 void InitSnake() { pHead = NULL; direction = right; //数组的坐标 AddPoint(5,3); AddPoint(5,4); AddPoint(5,5); AddPoint(5,6); } 实现蛇的移动 实现蛇的的移动是在主函数中不断调用创建和销毁结点来实现 1.创建结点 //添加结点 void AddPoint(int x, int y) { //创建新的结点 struct Point* NewPoint = (struct Point*)malloc(sizeof(struct Point)); if (NewPoint == NULL) { //判断结点是否创建成功 return; } //新结点赋值对应的x,y坐标 NewPoint->x = x; NewPoint->y = y; NewPoint->next = NULL; if (pHead != NULL) { //pHead != NULL则为老蛇头存在,将其改变为身体 SetWall(pHead->x, pHead->y, '='); PrintChar(pHead->y * 2, pHead->x, '='); } //NewPoint作为新的蛇头pHead,老蛇头被NewPoint—>next指向 NewPoint->next = pHead; pHead = NewPoint; //数组的坐标为y为横坐标,x为纵坐标,往下x增加,往右是y增加 //光标的坐标为x为横坐标,y为纵坐标,往下y增加,往右是x增加 SetWall(pHead->x, pHead->y, '@'); PrintChar(pHead->y * 2, pHead->x, '@'); }

    2.删除结点

    //删除结点 void DelPoint() { //两个结点以上才能删除 if (pHead == NULL || pHead->next == NULL) { return; } struct Point* pre = pHead; struct Point* cur = pHead->next; while (cur->next!=NULL) { pre = pre->next; cur = pre->next; } SetWall(cur->x, cur->y, ' '); PrintChar(cur->y * 2, cur->x, ' '); free(cur); cur = NULL; pre->next = NULL; } 主函数中用调用添加和删除即可实现移动效果 //添加一个新结点并在尾部结点删除一个即可实现 AddPoint(x, y); DelPoint();

    注:该部分为数据结构的链表知识

    5.4MainLop实现键盘控制方向

    目的:通过键盘输入字符控制贪吃蛇的运动方向

    核心代码:

    考虑到输入模块只有上下左右键,因此定义为枚举类型,名为EDiection(snake.h) //在snake.h中定义枚举类型的上下左右 enum EDiection { up='w',down='s',left='a',right='d' }; //枚举类型的变量命名direction enum EDiection direction; 接下来在主函数中开启控制蛇方向的线程 //主函数文件要引入<process.h>文件 //开启线程 HANDLE h; h = (HANDLE)_beginthread(MainLop,0,NULL); _beginthread参数分别为:函数的入口地址,栈大小, 参数列表。接下来编写入口函数MainLop: (至于栈大小,参数列表为何是0和NULL,笔者还没查到,知道的朋友欢迎留言) void MainLop() { //prekey是记录上一次蛇运动的方向 char prekey = NULL; while (1) { ch =_getch(); //如果是蛇没有运动 if (ch == NULL && prekey == NULL) { continue; } //如果输入方向恰和蛇运动方向相反 if((ch == up && prekey == down)||(ch == down && prekey == up) ||(ch==left&&prekey==right)||(ch == right && prekey == left)) { ch = prekey; } else { prekey = ch; } //switch语句是通过键盘输入改变运动方向 switch (ch) { case 'w':direction = up; break; case 's':direction = down; break; case 'a':direction = left; break; case 'd':direction = right; break; default:break; } } }

    注:由于调试过程中存在bug:在蛇运动时按与蛇运动方向相同的键会结束游戏,而且刚开始游戏时蛇就自动跑了起来,最好添加个开始键,因此加入if-else语句,作用是将上一次运动的方向缓存在prekey指针里,和本次按键判断,具体可参照上述代码。

    5.5随机食物的出现

    #include <time.h> //设置食物 void SetFood() { srand((unsigned int)time(NULL)); while (1) { FoodX = rand() % (row - 2) + 1; FoodY = rand() % (col - 2) + 1; if (GetWall(FoodX, FoodY) == ' ') { SetWall(FoodX, FoodY,'$'); PrintChar(FoodY * 2, FoodX, '$'); break; } } }

    这里引入了一个随机数的生成器来随机设定x,y的坐标。 也可以参见这篇文章帮助理解。 链接: srand((unsigned)time(NULL))详解.

    5.6Sleep函数控制速度

    为了额外给游戏制造难度,关卡:“速度随蛇长增加而增加”。 引入了Sleep函数, 功能: 执行挂起一段时间,也就是等待一段时间在继续执行。主函数里每次调用Sleep即通过改变结点函数的速度控制蛇速度的作用。 注:使用Sleep函数需要引入windows.h头文件 核心代码:

    //初始化分数 int score = 0; //初始化速度 int vspace = 200; void main() { InitWall(); DrawWall(); SetFood(); InitSnake(); //开启线程 HANDLE h; h = (HANDLE)_beginthread(MainLop,0,NULL); while (1) { //开始蛇不会移动,按任意键启动游戏 if (ch == NULL) { continue; } //通过判断分数提升关卡难度 switch (score) { case 5:vspace = 100; break; case 10:vspace = 70; break; default: break; } //这里分数越高,vspace越高,速度越快 Sleep(vspace);

    对Sleep函数的详细解释: 链接: 【C/C++】Sleep函数的用法.

    结尾

    以上就是贪吃蛇游戏的全部实现过程了,大家有什么问题或者发现什么错误都欢迎评论区留言,如果喜欢就分享收藏点赞,如果能您能打赏,真的对我非常非常有帮助!感谢支持!

    Processed: 0.012, SQL: 10