目录
前言
1. 游戏背景
2. 游戏效果演示
3. 目标
4. 项目定位
5. 技术要点
6. Win 32 API 介绍
6.1 Win32 API
6.2 控制台程序(Console)
6.3 控制台屏幕上的坐标COORD
6.4 GetStdHandle
6.5 GetConsoleCursorInfo
6.5.1 CONSOLE_CURSOR_INFO
6.6 SetConsoleCursorInfo
6.7 SetConsoleCursorPosition
6.8 GetAsyncKeyState
7. 贪吃蛇游戏设计与分析
7.1 地图
7.1.1 本地化
7.1.2 类项
7.1.3 setlocale 函数
7.1.4 宽字符的打印
7.1.5 地图坐标
7.2 蛇身和食物
7.3 数据结构设计
7.4 游戏流程设计
8. 核心逻辑实现分析
8.1 游戏主逻辑
8.2 游戏开始
8.2.1 打印欢迎界面
8.2.2 创建地图
8.2.3 初始化蛇身
8.2.4 创建第一个食物
8.3 游戏运行
8.3.1 KEY_PRESS
8.3.2 PrintHelpInfo
8.3.3 蛇身移动
8.3.3.1 NextIsFood
8.3.3.2 EatFood
8.3.3.3 NoFood
8.3.3.4 KillByWall
8.3.3.5 KillBySelf
8.4 游戏结束
9. 参考代码
总结
前言
1976年,Gremlin平台推出了一款经典街机游戏Blockade。游戏中,两名玩家分别控制一个角色在屏幕上移动,所经之处砌起围栏。角色只能向左、右方向90度转弯,游戏目标保证让对方先撞上屏幕或围栏。 听起来有点复杂,其实就是下面这个样子:
基本上就是两条每走一步都会长大的贪吃蛇比谁后完蛋,玩家要做的就是避免撞上障碍物和越来越长的身体。更多照片、视频可以看 GamesDBase 的介绍。
Blockade 很受欢迎,类似的游戏先后出现在 Atari 2600、TRS-80、苹果 2 等早期游戏机、计算机上。但真正让这种游戏形式红遍全球的还是21年后随诺基亚手机走向世界的贪吃蛇游戏——Snake。
1. 游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学生的编程能力和逻辑能力。
2. 游戏效果演示
贪吃蛇
3. 目标
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇实现基本的功能:
- 贪吃蛇地图绘制
- 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
- 退出游戏
4. 项目定位
- 趣味性强
- 对C语言语法做一个基本巩固
- 启发游戏开发的兴趣
- 项目适合C语言学完,有一定的代码能力,初步接触数据结构中的链表的学者
5. 技术要点
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
6. Win 32 API 介绍
6.1 Win32 API
本次实现贪吃蛇会使用到的一些Win32 API知识,需要学习
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的。由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
6.2 控制台程序(Console)
新版WIN 11操作系统提供的终端如上图,此控制台有些功能无法完成,此项目也无法实现,需要修改
选择Windows控制台主机并保存
修改成功!
也可以修改回去
右击鼠标选择属性
平常运行起来的黑框程序其实就是控制台程序,可以使用cmd命令来设置控制台窗口的长宽,设置控制台窗口的大小,如:30行,100列
1 mode con cols=100 lines=30
参考:mode | Microsoft Learn
也可以通过命令设置控制台窗口的名字:
1 title 贪吃蛇
参考:title | Microsoft Learn
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
#include<stdio.h>
#include<stdlib.h>int main()
{//设置控制台窗口的长度:设置控制台窗口的大小,30行,100列system("mode con cols=100 lines=30");//设置cmd窗口名称system("title 贪吃蛇");//getchar();system("pause");//pause暂停return 0;
}
运行结果
使用getchar函数观察效果或system函数都可以
6.3 控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格
1 typedef struct _COORD {
2 SHORT X;
3 SHORT Y;
4 } COORD,* PCOORD;
给坐标赋值:(使用时包含头文件windows.h)
1 COORD pos = { 10, 15 };
6.4 GetStdHandle
参考:GetStdHandle 函数 - Windows Console | Microsoft Learn
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
1 HANDLE GetStdHandle(DWORD nStdHandle);
实例:
1 HANDLE hOutput = NULL;
2
3 //获取标准输出的句柄(用来标识不同设备的数值)
4 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
6.5 GetConsoleCursorInfo
参考:GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
1 1 BOOL WINAPI GetConsoleCursorInfo (
2 HANDLE hConsoleOutput,
3 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
4 );
实例:
1 HANDLE hOutput = NULL;
2 //获取标准输出的句柄(用来标识不同设备的数值)
3 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
4
5 CONSOLE_CURSOR_INFO CursorInfo;
6 GetConsoleCursorInfo(hOutput, & CursorInfo);//获取控制台光标信息
6.5.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
1 typedef struct _CONSOLE_CURSOR_INFO {
2 DWORD dwSize; BOOL bVisible;
3 } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
1 CursorInfo.bVisible = false; //隐藏控制台光标
6.6 SetConsoleCursorInfo
参考:SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
设置指定控制台屏幕缓冲区的光标的大小和可见性。
1 BOOL WINAPI SetConsoleCursorInfo(
2 HANDLE hConsoleOutput,
3 const CONSOLE_CURSOR_INFO * lpConsoleCursorInfo
4 );
实例:
1 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2
3 //影藏光标操作
4 CONSOLE_CURSOR_INFO CursorInfo;
5 GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
6 CursorInfo.bVisible = false; //隐藏控制台光标
7 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
6.7 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
1 BOOL WINAPI SetConsoleCursorPosition(
2 HANDLE hConsoleOutput,
3 COORD pos
4 );
实例:
1 COORD pos = { 10, 5 };
2 HANDLE hOutput = NULL;
3 //获取标准输出的句柄(用来标识不同设备的数值)
4 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
5 //设置标准输出上光标的位置为pos
6 SetConsoleCursorPosition(hOutput, pos);
SetPos:封装一个设置光标位置的函数
1 //设置光标的坐标
2 void SetPos(short x, short y)
3 { COORD pos = { x, y };
3 HANDLE hOutput = NULL;
4 //获取标准输出的句柄(用来标识不同设备的数值)
5 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
6 //设置标准输出上光标的位置为pos
7 SetConsoleCursorPosition(hOutput, pos);
8 }
6.8 GetAsyncKeyState
参考:GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft Learn
获取按键情况,GetAsyncKeyState的函数原型如下:
1 SHORT GetAsyncKey State(
2 int vKey
3 );
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
1 #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
7. 贪吃蛇游戏设计与分析
7.1 地图
最终的贪吃蛇大纲要以下样子
这里得学习控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,得知道该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。控制台窗口的坐标如下图所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★。普通的字符占一个字节,这类宽字符占用2个字节。这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节。但这些假定并不非在世界上的任何地方都适用。
寻找这些符号:
C语言字符默认采用ASCII编码,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx。ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,如在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。如法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但不管怎样,所有编码方式中,0--127表示的符号是一样的,不一样的只是128--255这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定不够,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入宽字符的类型wchar_t 和宽字符的输入\输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
7.1.1 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。在标准可以中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
7.1.2 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
- LC_COLLATE:影响字符串比较函数strcoll( )和strxfrm
- LC_CTYPE: 影响字符处理函数的行为
- LC_MONETARY:影响货币格式
- LC_NUMERIC:影响printf( )的数字格式
- LC_TIME:影响时间格式strftime( )和wcsftime( )
- LC_ALL :针对所有类项修改,将以上所有类别设置为给定的语言环境
每个类项的详细说明,请参考:setlocale,_wsetlocale | Microsoft Learn
7.1.3 setlocale 函数
参考:setlocale - C++ Reference
1 char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。setlocale 的第一个参数可以是前面说明的类项中的一个,每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。C标准给第二个参数仅定义了2种可能取值:"C"(正常模式,C语言默认)和" "(本地模式)。在任意程序执行开始,都会隐藏式执行调用:
1 setlocale (LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
1 setlocale(LC_ALL, " ");//切换到本地环境
setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。
#include<locale.h>int main()
{char* loc;loc = setlocale(LC_ALL, NULL);printf("默认的本地信息:%s\n", loc);loc = setlocale(LC_ALL, "");printf("设置后的本地信息:%s\n", loc);return 0;
}
7.1.4 宽字符的打印
在屏幕上打印宽字符
宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc; 在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。
wprintf(wide)参考:wprintf - C++ Reference
#include<stdio.h>
#include<locale.h>int main()
{setlocale(LC_ALL, "");wchar_t ch1 = L'●';wchar_t ch2 = L'贝'; wchar_t ch3 = L'壳';wchar_t ch4 = L'★'; printf("%c%c\n", 'a', 'b'); wprintf(L"%c\n", ch1); wprintf(L"%c\n", ch2); wprintf(L"%c\n", ch3); wprintf(L"%c\n", ch4);return 0;
}
运行结果
从输出的结果来看,一个普通字符占一个字符的位置但是打印一个汉字字符,占用2个字符的位置,如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
普通字符和宽字符打印出宽度的展示如下:
7.1.5 地图坐标
假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
7.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
初步构思:
7.3 数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
1 typedef struct SnakeNode
2 {
3 int x;
4 int y;
5 struct SnakeNode* next;
6 }SnakeNode,* pSnakeNode;
要管理整条贪吃蛇,再封装一个Snake的结构来维护整条贪吃蛇:
1 typedef struct Snake
2 {
3 pSnakeNode _pSnake;//维护整条蛇的指针
4 pSnakeNode _pFood;//维护食物的指针
5
6 int _Sore;//当前获得分数
7 int _foodWeight;//默认每个食物10分
8 int _SleepTime;//每走一步休眠时间,休眠时间越短,蛇的速度越快9 enum GAME_STATUS _Status;//游戏当前状态
10 enum DIRCTION _Dir;//蛇头方向默认向右
11 }Snake,*pSnake;
蛇的方向,可以一一列举,使用枚举
1 //方向
2 enum DIRECTION
3 {
4 UP=1,
5 DOWN,
6 LIFT,
7 RIGHT
8 };
游戏状态,可以一一列举,使用枚举
1 //游戏状态
2 enum GAME_STATUS
3 {
4 OK = 1,
5 ESC,
6 KILL_BY_WALL,
7 KILL_BY_SELF
8 };
7.4 游戏流程设计
8. 核心逻辑实现分析
8.1 游戏主逻辑
#include"snake.h"void test()
{int ch = 0;srand((unsigned int)time(NULL));//创建贪吃蛇do {Snake snake = { 0 };GameStart(&snake);//游戏开始前的初始化GameRun(&snake);//玩游戏的过程GameEnd(&snake);//善后工作SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();getchar();//清理 \n} while (ch == 'Y' || ch == 'y');}int main()
{//修改适配本地中文环境setlocale(LC_ALL, "");test();//贪吃蛇游戏测试SetPos(0, 27);return 0;
}
8.2 游戏开始
void GameStart(pSnake ps)
{//设置控制台信息,窗口大小、窗口名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(ps);//创建食物CreateFood(ps);
}
8.2.1 打印欢迎界面
在游戏正式开始之前,做一些功能提醒
void WelcomeToGame()
{//欢迎信息SetPos(35, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38,20);system("pause");system("cls");//清理屏幕信息//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速\n");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls");
}
8.2.2 创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所以使用wprintf函数(打印格式串前使用L)。打印地图的关键是要算好坐标。墙体打印的宽字符:
1 #define WALL L'□'
创建地图函数CreateMap
void CreatMap()
{//上(0,0)-(56,0)SetPos(0, 0);int i = 0;for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下(0,26)-(56,26)SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左//x为0,y从1开始增长for (i = 1; i <= 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右//x是56,y从1开始增长SetPos(56, 0);for (i = 0; i <= 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}
8.2.3 初始化蛇身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。
再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。
蛇身打印的宽字符:
1 # define BODY L' ●'
初始化蛇身函数:InitSnake
void InitSnake(pSnake ps)
{//创建5个蛇身的节点pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next=NULL;//头插法if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇身cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//贪吃蛇的其他信息初始化ps->_Dir = RIGHT;ps->_foodWeight = 10;ps->_pFood = NULL;ps->_Sore = 0;ps->_SleepTime = 200;ps->_Status = OK;}
8.2.4 创建第一个食物
- 先随机生成食物的坐标
◦ x坐标必须是2的倍数
◦ 食物的坐标不能和蛇身每个节点的坐标重复
- 创建食物节点,打印食物
食物打印的宽字符:
1 #define FOOD L'★'
创建食物的函数:CreateFood
void CreateFood(pSnake ps)
{int x = 0;int y = 0;
again:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//坐标和蛇身每个节点的坐标比较pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创建食物pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->_pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);getchar();
}
8.3 游戏运行
游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下一步的方向、是否加速减速、是否暂停或者退出游戏。确定了蛇的方向和速度,蛇就可以移动了。
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前的分数情况SetPos(62, 10);printf("比赛总分:%5d\n", ps->_Score);SetPos(62, 11);printf("食物分值:%02d\n", ps->_FoodWeight);//检测按键//上、下、左、右、ESC、F3、F4if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN){ps->_Dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP){ps->_Dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status=ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戏要暂停pause();//暂停和恢复暂停}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 80){ps->_SleepTime -= 30;ps->_FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->_FoodWeight >2){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}//蛇每次移动之前要休眠,休眠时间越短,移动速度越快//睡眠一下Sleep(ps->_SleepTime);//走一步SnakeMove(ps);} while (ps->_Status==OK);}
8.3.1 KEY_PRESS
检测按键状态,封装了一个宏
1 # define KEY_PRESS(VK) ((GetAsyncKeyState(VK)& 0x1) ?1 : 0)
8.3.2 PrintHelpInfo
void PrintHelpInfo()
{SetPos(62, 15);printf("1. 不能撞墙,不能要到自己");SetPos(62, 16);printf("2.用 ↑.↓.←.→ 来控制蛇的移动");SetPos(62, 17);printf("3.F3是加速,F4是减速");SetPos(62, 19);printf("版权@AD_Grey");
}
8.3.3 蛇身移动
先创建下一个节点,根据移动方向和蛇头的坐标,确定蛇移动到下一个位置的坐标。
确定下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),不是食物则做前进一步的处理(NoFood)。蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
void SnakeMove(pSnake ps)
{//创建下一个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}pNextNode->next = NULL;//下一个节点的坐标由蛇头坐标和方向决定switch (ps->_Dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x-2;pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x+2;pNextNode->y = ps->_pSnake->y;break;}//下一个坐标处是否是食物if(NextIsFood(ps, pNextNode)){//是食物就吃掉EatFood(ps,pNextNode);}else{//不是食物就正常走一步NotFood(ps, pNextNode);}//检测撞墙KillByWall(ps);//检测撞到自己KillBySelf(ps);
}
8.3.3.1 NextIsFood
//pSnakeNode pNextNode是下一个节点的地址
//pSnake ps 维护蛇的指针int NextIsFood(pSnake ps, pSnakeNode pNextNode)
{if (ps->_pFood->x == pNextNode->x && ps->_pFood->y == pNextNode->y)return 1;elsereturn 0;
}
8.3.3.2 EatFood
void EatFood(pSnake ps, pSnakeNode pNextNode)
{//头插法pNextNode->next = ps->_pSnake;ps->_pSnake = pNextNode;pSnakeNode cur = ps->_pSnake;//打印蛇身while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_Score += ps->_FoodWeight;//释放旧的食物free(ps->_pFood);//新建食物CreateFood(ps);
}
8.3.3.3 NoFood
将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,放弃掉蛇身的最后一个节点
void NotFood(pSnake ps, pSnakeNode pNextNode)
{//头插法pNextNode->next=ps->_pSnake;ps->_pSnake = pNextNode;pSnakeNode cur = ps->_pSnake;//打印蛇身while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//将尾节点的位置打印成空白字符,然后释放节点SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next);cur->next = NULL;}
8.3.3.4 KillByWall
判断蛇头的坐标是否和墙的坐标冲突
void KillByWall(pSnake ps)
{if ((ps->_pSnake->x == 0 )||(ps->_pSnake->x == 56)||(ps->_pSnake->y == 0 )||(ps->_pSnake->y == 26)){ps->_Status = KILL_BY_WALL;return 1;}return 0;
}
8.3.3.5 KillBySelf
判断蛇头的坐标是否和蛇身体的坐标冲突
int KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;//从第二个节点开始while (cur){if ((cur->x == ps->_pSnake->x) && (cur->y == ps->_pSnake->y)){ps->_Status = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;
}
8.4 游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
void GameEnd(pSnake ps)
{SetPos(18, 12);switch (ps->_Status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_WALL:printf("很遗憾,撞墙了,游戏结束\n");break;case KILL_BY_SELF:printf("很遗憾,咬到自己了,游戏结束\n");break;}//释放贪吃蛇链表资源pSnakeNode cur = ps->_pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->_pFood);ps = NULL;
}
9. 参考代码
完整代码实现,分3个文件实现
//snake.h#include<stdio.h>
#include<locale.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'//蛇默认的起始坐标
#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//定位控制台光标位置
void SetPos(int x, int y);//游戏状态
enum GAME_STATUS
{OK = 1,//正常运行ESC,//按了ESC键退出,正常退出KILL_BY_WALL,//撞墙KILL_BY_SELF//撞到自身
};//方向
enum DIRECTION
{UP=1,DOWN,LEFT,RIGHT
};//贪吃蛇蛇身节点的定义
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode,* pSnakeNode;//贪吃蛇
typedef struct Snake
{pSnakeNode _pSnake;//维护整条蛇的指针,指向蛇头pSnakeNode _pFood;//维护食物的指针int _Score;//当前获得分数int _FoodWeight;//默认每个食物10分int _SleepTime;//每走一步休眠时间,休眠时间越短,蛇的速度越快enum GAME_STATUS _Status;//游戏当前状态enum DIRCTION _Dir;//蛇头方向默认向右
}Snake,*pSnake;//游戏开始前的准备
void GameStart(pSnake ps);//打印欢迎信息
void WelcomeToGame();//绘制地图
void CreatMap();//初始化蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏运行的整个逻辑
void GameRun(pSnake ps);//打印帮助信息
void PrintHelpInfo();//暂停
void pause();//蛇移动的函数——每次走一步
void SnakeMove(pSnake ps);//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps,pSnakeNode pNextNode);//下一步要走的位置处是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNextNode);//下一步要走的位置处不是食物,不吃食物
void NotFood(pSnake ps, pSnakeNode pNextNode);//检测撞墙
int KillByWall(pSnake ps);//检测撞到自己
int KillBySelf(pSnake ps);//游戏结束的资源释放
void GameEnd(pSnake ps);
//snake.c#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(35, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38,20);system("pause");system("cls");//清理屏幕信息//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速\n");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls"); }void CreatMap()
{//上(0,0)-(56,0)SetPos(0, 0);int i = 0;for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下(0,26)-(56,26)SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左//x为0,y从1开始增长for (i = 1; i <= 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右//x是56,y从1开始增长SetPos(56, 0);for (i = 0; i <= 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}void InitSnake(pSnake ps)
{//创建5个蛇身的节点pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next=NULL;//头插法if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇身cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//贪吃蛇的其他信息初始化ps->_Dir = RIGHT;ps->_FoodWeight = 10;ps->_pFood = NULL;ps->_Score = 0;ps->_SleepTime = 200;ps->_Status = OK;}//创建食物
void CreateFood(pSnake ps)
{int x = 0;int y = 0;
again:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//坐标和蛇身每个节点的坐标比较pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创建食物pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->_pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);
}void GameStart(pSnake ps)
{//设置控制台信息,窗口大小、窗口名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(ps);//创建食物CreateFood(ps);
}void PrintHelpInfo()
{SetPos(62, 15);printf("1. 不能撞墙,不能要到自己");SetPos(62, 16);printf("2.用 ↑.↓.←.→ 来控制蛇的移动");SetPos(62, 17);printf("3.F3是加速,F4是减速");SetPos(62, 19);printf("版权@AD_Grey");
}void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}int NextIsFood(pSnake ps, pSnakeNode pNextNode)
{if (ps->_pFood->x == pNextNode->x && ps->_pFood->y == pNextNode->y)return 1;elsereturn 0;
}void EatFood(pSnake ps, pSnakeNode pNextNode)
{pNextNode->next = ps->_pSnake;ps->_pSnake = pNextNode;pSnakeNode cur = ps->_pSnake;//打印蛇身while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_Score += ps->_FoodWeight;//释放旧的食物free(ps->_pFood);//新建食物CreateFood(ps);
}void NotFood(pSnake ps, pSnakeNode pNextNode)
{//头插法pNextNode->next=ps->_pSnake;ps->_pSnake = pNextNode;//释放尾结点pSnakeNode cur = ps->_pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//将尾节点的位置打印成空白字符SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next);cur->next = NULL;}int KillByWall(pSnake ps)
{if ((ps->_pSnake->x == 0 )||(ps->_pSnake->x == 56)||(ps->_pSnake->y == 0 )||(ps->_pSnake->y == 26)){ps->_Status = KILL_BY_WALL;return 1;}return 0;
}int KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;//从第二个节点开始while (cur){if ((cur->x == ps->_pSnake->x) && (cur->y == ps->_pSnake->y)){ps->_Status = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;
}void SnakeMove(pSnake ps)
{//创建下一个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}pNextNode->next = NULL;//下一个节点的坐标由蛇头坐标和方向决定switch (ps->_Dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x-2;pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x+2;pNextNode->y = ps->_pSnake->y;break;}//下一个坐标处是否是食物if(NextIsFood(ps, pNextNode)){//是食物就吃掉EatFood(ps,pNextNode);}else{//不是食物就正常走一步NotFood(ps, pNextNode);}//检测撞墙KillByWall(ps);//检测撞到自己KillBySelf(ps);
}void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前的分数情况SetPos(62, 10);printf("比赛总分:%5d\n", ps->_Score);SetPos(62, 11);printf("食物分值:%02d\n", ps->_FoodWeight);//检测按键//上、下、左、右、ESC、F3、F4if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN){ps->_Dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP){ps->_Dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT){ps->_Dir = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status=ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戏要暂停pause();//暂停和恢复暂停}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 80){ps->_SleepTime -= 30;ps->_FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->_FoodWeight >2){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}//蛇每次移动之前要休眠,休眠时间越短,移动速度越快//睡眠一下Sleep(ps->_SleepTime);//走一步SnakeMove(ps);} while (ps->_Status==OK);}void GameEnd(pSnake ps)
{SetPos(18, 12);switch (ps->_Status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_WALL:printf("很遗憾,撞墙了,游戏结束\n");break;case KILL_BY_SELF:printf("很遗憾,咬到自己了,游戏结束\n");break;}//释放贪吃蛇链表资源pSnakeNode cur = ps->_pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->_pFood);ps = NULL;
}
//test.c#include"snake.h"void test()
{int ch = 0;srand((unsigned int)time(NULL));//创建贪吃蛇do {Snake snake = { 0 };GameStart(&snake);//游戏开始前的初始化GameRun(&snake);//玩游戏的过程GameEnd(&snake);//善后工作SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y' || ch == 'y');}int main()
{//修改适配本地中文环境setlocale(LC_ALL, "");test();//贪吃蛇游戏测试SetPos(0, 27);return 0;
}
总结
本文系统讲解了利用控制台实现贪吃蛇小游戏的过程。通过编写贪吃蛇游戏,学习者可以加深对C语言基本语法和结构的理解,如变量、数组、循环、条件判断等。实现游戏的过程需要逻辑清晰,这有助于培养解决问题的思路和编程逻辑思维。贪吃蛇游戏中涉及到的坐标变换、碰撞检测等算法,可以帮助学习者理解更复杂的编程概念。在编写游戏的过程中,学习者需要不断调试和优化代码,这有助于提高编码能力和解决实际问题的能力。通过完成一个具体的项目,学习者可以获得宝贵的实践经验,这对于理解理论知识和未来的项目开发都是非常有益的。
相比于传统的习题练习,编写游戏更加有趣,能够激发学习者的学习兴趣和创造力。贪吃蛇游戏可以在多种平台上实现,如PC和手机等,这有助于学习者了解不同平台的编程环境和技巧。在编写贪吃蛇游戏时,学习者需要掌握诸如键盘输入、屏幕显示、计时器等计算机基础知识。贪吃蛇游戏可以根据难度设置不同的游戏规则,这有助于学习者逐步提升编程水平。
在游戏中,学习者可以即时看到代码修改的效果,这种即时反馈有助于快速学习和调整。贪吃蛇作为一个经典游戏,拥有庞大的爱好者社区,学习者可以从中获得帮助和支持。
当然以上内容只是基础版的游戏编写流程,可以发挥自己的创意,扩展更多游戏功能,如增加多条蛇、一次生成多个食物等;还可以学习网络相关知识,包装、渲染游戏,使其更加丰富,更具吸引力。
总之,通过C语言实现贪吃蛇游戏,不仅能够帮助学习者巩固和深化C语言的基础知识,还能够在实践中提升编程技能,同时也能够激发学习者对编程的兴趣,是一种有效且有趣的学习方法。