C语言实现简易贪吃蛇(详细版)(1)
目录
欢迎来到本博客!今天我们将会一起来学习如何使用C语言实现一个简易版的贪吃蛇游戏!
在本博客中,我们将从头开始一步步地实现这个游戏,包括贪吃蛇的移动、食物的生成、碰撞检测等等。
如果你对C语言、游戏开发或者贪吃蛇游戏有兴趣,那么就跟着我一起来探索吧!
本博客的技术要点有C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。
下面详细介绍如何实现贪吃蛇。(注意终端要使用Windows控制台主机)
想要实现贪吃蛇我们需要考虑到:
- 制作地图
- 制作蛇的身体
- 蛇的状态
- 按键控制方向
- 制作食物
- 记录分数
- 蛇的加速减速
- 游戏的结束条件
我将贪吃蛇分为基础部分、主体部分、和末尾部分三部分进行介绍。
本篇介绍基础部分
这一部分要介绍的内容如目录。
为了方便管理,这里我们将与蛇有关的分数,状态,方向,加速,减速等信息封装为一个结构体
-
头文件声明:定义游戏所需的常量、数据结构和函数声明
#pragma once #include <stdlib.h> #include <stdio.h> #include <windows.h> #include <stdbool.h> #include <wchar.h> #include <locale.h> #include <time.h> #define BODY L'●' #define FOOD L'★' //判断按键是否被按下 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0) //默认蛇身长度 #define Length 5 //蛇的状态 enum STATUS { OK, //正常 KILL_BY_WALL, //撞墙 KILL_BY_SELF, //撞到自己 END_NORMAL //正常退出 }; //蛇的移动 enum DIR{ UP = 1, DOWN, LEFT, RIGHT }; //蛇体 typedef struct body { int x; int y; struct body* next; }body, * Body; //游戏主体 typedef struct Snake { Body _pSnake;//指向蛇头的指针 Body _pFood;//指向食物节点的指针 enum DIR _dir;//蛇的方向 enum STATUS _status;//游戏的状态 int _food_weight;//一个食物的分数 int _score; //总成绩 int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢 }Snake, * pSnake; //长 56 //宽 27 //定位光标 void SetPos(short x, short y); void GameStart(Snake* ps); void WelcomeToGame(); void CreateMap(); //创建蛇身 void CreateBody(Snake** ps); //创建食物 void CreateFood(Snake* ps); //蛇的移动 void SnakeMove(Snake* ps); //游戏暂停 void Pause(); //游戏运行 void GameRun(pSnake ps); //游戏结束条件 void GameOver(pSnake ps);
刚开始大家可能没看懂,不过别急,接下来会进行详细介绍
-
Win32 API 使用:利用 Win32 API 创建游戏窗口,并处理用户输入等交互操作
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32API 也就是Microsoft Windows32位平台的应⽤程序编程接。
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用 cmd命令来设置控制台窗口的长宽 :设置控制台窗口的大小,30行,100列,使用 title命令 来设置窗口的名字。
这些能在控制台窗口执行的命令,也可以调⽤C语⾔函数system来执行#include <stdio.h> int main() { system("mode con cols=100 lines=30"); //设置窗口名称 system("title 贪吃蛇"); getchar(); return 0; }
效果:
-
GetStdHandle函数
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle); //列: HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
-
GetConsoleCursorInfo函数
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo ); //PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO结构的指针, 该结构接收有关主机游标(光标)的信息 ````
-
CONSOLE_CURSOR_INFO函数
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible,游标的可⻅性。如果光标可见,则此成员为 TRUE。
-
SetConsoleCursorInfo函数
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo ); //列; //获取标准输出的句柄(⽤来标识不同设备的数值) HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //隐藏光标操作(创建变量) CONSOLE_CURSOR_INFO CursorInfo; //获取控制台光标信息 GetConsoleCursorInfo(hOutput, &CursorInfo); //隐藏控制台光标 CursorInfo.bVisible = false; //设置控制台光标状态 SetConsoleCursorInfo(hOutput, &CursorInfo);
-
控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系
(0,0) 的原点位于缓冲区的顶部左侧单元格。typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD; //给坐标赋值 COORD pos = { 10, 15 };
-
SetConsoleCursorPosition函数
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标 信息放在COORD类型的pos中,调
⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos ); //例: COORD pos = { 10, 5}; HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);
-
GetAsyncKeyState函数
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState( int vKey );
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明当前按键的状态是按下,如果最⾼是0,说明当前按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1//定义宏 #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 1) ? 1 : 0 )
参考: 虚拟键码 (Winuser.h) - Win32 apps
熟悉这些函数以后我们就可以正式开始了!
-
-
制作游戏开始界面
创建初始化游戏的函数进行封装
void GameStart(Snake* ps) { system("mode con cols=100 lines=30"); system("title 贪吃蛇"); HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false; // 设置光标不可见 SetConsoleCursorInfo(hOutput, &CursorInfo); //1. 打印环境界面和功能介绍 WelcomeToGame(); //2. 绘制地图 CreateMap(); //等等...... }
-
打印游戏介绍界面:在开始界面提供一个选项,让玩家了解游戏规则和操作说明
//定位光标函数 void SetPos(short x, short y) { COORD pos = { x, y }; HACCEL hehe = NULL; hehe = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hehe, pos); } void WelcomeToGame() { //第一页 //定位光标 SetPos(40, 14); //打印 printf("欢迎来到贪吃蛇小游戏\n"); //定位光标 SetPos(42, 20); //暂停 system("pause"); //清屏 system("cls"); //第二页 SetPos(25, 14); printf("用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n"); SetPos(42, 20); system("pause"); system("cls"); //第三页 SetPos(39, 15); printf("加速能够得到更高的分数\n"); SetPos(42, 20); system("pause"); system("cls"); }
效果展示:
打印游戏介绍界面
-
打印游戏地图:在游戏界面打印贪吃蛇的初始地图,包括蛇、食物和边界等元素
注意:
头文件<locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样行为的部分。
在标准中,依赖地区的部分有以下几项:
• 数字量的格式
• 货币量的格式
• 字符集
• ⽇期和时间的表⽰形式
–类项–
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部
分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,
指定⼀个类项:
• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
每个类项的详细说明,请参考
setlocale函数char* setlocale (int category, const char* locale);
– setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
– setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参
数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和" "(本地模式)。
在任意程序执⾏开始,都会隐藏式执⾏调⽤:setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
当程序运⾏起来后想改变地区就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。简而言之: 要打印特殊宽字符墙体 “□” 就要将程序适应本地环境
//适应本地环境 setlocale(LC_ALL, " ");
-
制作墙体:
-
函数名称: CreateMap
- 函数功能: 创建墙体
- 输入参数: 无
- 返回值: 无
效果:void CreateMap() { //上 int i = 0; for (i = 0; i < 29; i++) { //打印宽字符固定格式,记住就好。 wprintf(L"%lc", L'□'); } //下 SetPos(0, 26); for (i = 0; i < 29; i++) { wprintf(L"%lc", L'□'); } //左 for (i = 1; i <= 25; i++) { SetPos(0, i); wprintf(L"%lc", L'□'); } //右 for (i = 1; i <= 25; i++) { SetPos(56, i); wprintf(L"%lc", L'□'); } getchar(); }
-
制作蛇身:
-
函数名称: CreateBody
- 函数功能: 创建贪吃蛇的身体,并初始化蛇的相关属性。
- 输入参数: Snake** ps - 指向贪吃蛇结构体指针的指针,用于更新蛇的信息。
- 返回值: 无
void CreateBody(Snake** ps) { // 循环创建蛇的每个身体节点 for (int i = 0; i < Length; i++) { // 为当前身体节点分配内存空间 Body P = (Body)malloc(sizeof(body)); // 设置当前节点的位置坐标 P->x = X + i * 2; P->y = Y; P->next = NULL; // 将当前节点添加到蛇身链表中 if ((*ps)->_pSnake == NULL) { // 如果蛇身链表为空,则将当前节点设为蛇身链表的头节点 (*ps)->_pSnake = P; } else { // 否则将当前节点插入到蛇身链表的头部 P->next = (*ps)->_pSnake; (*ps)->_pSnake = P; } } // 遍历蛇身链表,将蛇身节点显示在游戏界面上 Body T = (*ps)->_pSnake; while (T) { SetPos(T->x, T->y); wprintf(L"%lc", BODY); T = T->next; } // 初始化蛇的其他属性 (*ps)->_dir = RIGHT; // 蛇的初始移动方向为向右 (*ps)->_sleep_time = 200; // 设置蛇的移动速度 (*ps)->_status = OK; // 设置蛇的状态为正常 (*ps)->_food_weight = 10; // 设置初始食物的重量 }
-
制作食物:
-
函数名称: CreateFood
- 函数功能: 在游戏界面上生成食物,并确保食物不与蛇身重叠。
- 输入参数: Snake* ps - 指向贪吃蛇结构体的指针,用于获取蛇的信息。
- 返回值: 无
效果:void CreateFood(Snake* ps) { int y = 0; // 食物的纵坐标 int x = 0; // 食物的横坐标 int i = 0; // 用于判断食物位置是否与蛇身冲突的标志 // 使用当前时间作为随机数种子 srand(time(NULL)); // 在随机位置生成食物,直到食物位置不与蛇身冲突为止 do { i = 0; // 重置冲突标志 // 随机生成食物的纵坐标 y = rand() % 23 + 2; // 随机生成食物的横坐标,确保为偶数 do { x = rand() % 55 + 1; } while (x % 2 != 0); // 检查食物位置是否与蛇身冲突 Body B = ps->_pSnake; while (B) { if (B->x == x && B->y == y) { i = 1; // 若冲突,则设置标志为1 break; } B = B->next; } } while (i); // 若发现冲突,则重新生成食物位置 // 分配内存空间,创建食物节点 Body F = (Body)malloc(sizeof(body)); ps->_pFood = F; ps->_pFood->next = NULL; ps->_pFood->x = x; ps->_pFood->y = y; // 在游戏界面上显示食物 SetPos(x, y); wprintf(L"%lc", FOOD); }
-
-
那么关于贪吃蛇第一部分内容到这里结束了,在下篇博客我们会介绍主体部分:
蛇的移动:根据用户输入控制蛇的移动方向,并更新游戏地图。
食物生成:在地图上随机生成食物,并确保不与蛇身重叠。
碰撞检测:检测蛇头是否与边界或蛇身相撞,以及是否吃到了食物。
分数计算:根据蛇吃到的食物数量计算玩家的得分。
等等…
如有什么问题或疑问请大家在评论区指出~
谢谢大家的观看!