目录
一. 贪吃蛇的介绍
二. Win32 API
1. 控制台程序
2. COORD 控制台屏幕上的坐标
3. GetStdHandle
4. GetConsoleCursorInfo
CONSOLE_CURSOR_INFO
5. SetConsoleCursorInfo
6. SetConsoleCursorPosition
封装的SetPos函数
7. GetAsyncKeyState
宏定义KEY_PRESS
三. 贪吃蛇游戏实现
1. 贪吃蛇地图
和类项
setlocale函数
打印地图
封装的墙函数
2. 设计蛇和食物
3. 游戏实现流程
1. 游戏开始-GameStart
1.打印欢迎界面
2.打印创建地图
3.创建蛇-初始化
4.创建食物
2. 游戏运行-GameRun
暂停—Pause函数
1.蛇的移动
2.下一个位置是否是食物
无食物-NoFood
判断是否是食物-NextIsFood
3.吃掉食物——EatFood
4.撞到墙——KillByWall
5.撞到自己——KillBySelf
3. 游戏结束——GameEnd
四. 展示所有全部代码
一. 贪吃蛇的介绍
我们都有玩过一个小游戏——贪吃蛇,贪吃蛇也是一个经典游戏。如上图所示,游戏玩法就是操控一个蛇,让它吃掉食物,每吃掉一个食物就会增加自己身体一格长度,并且保证自己不能撞到墙和自己本身,我们就可以通过这些功能,去用代码实现这些功能从而实现贪吃蛇游戏。(贪吃蛇游戏如上图所示)
我们总结一下贪吃蛇所需要的基本功能:
- 绘制贪吃蛇地图
- 蛇的移动
- 蛇吃食物
- 蛇的功能(上、下、左、右方向键,控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞到自己死亡
- 蛇速度加速和减速
- 暂停游戏
- 计算得分
- 退出游戏
我们完成的游戏,是在控制台窗口完成。
这里设计的知识要点有:C语言函数,枚举类型,结构体,动态内存管理,预处理指令,链表(链表用于维护贪吃蛇的蛇身),Win32 API等。
二. Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数)、可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
1. 控制台程序
控制台(console)程序可能听起来名字比较陌生,其实是如图所示:
打开方式可以按Windows+R,在输入cmd。
我们可以使用cmd命令设置控制台窗口的长宽:例如,30行,100列
mode con cols=100 lines=30
我们可以看一下对比,上图所示
设置控制台窗口的名字的指令:
title 贪吃蛇
这些操作可以调用C语言函数system执行:
注意:使用时记得包含stdlib.h头文件
#include <stdio.h>
#include <stdlib.h>
//system函数可以用来执行系统命令//
int main()
{
//设置控制台的相关属性
system("mode con cols=100 lines=30"); //设置大小
system("title 贪吃蛇"); //设置名字
system("pause"); //也可以getchar();效果一样。防止程序结束,导致结果有问题
return 0;
}
效果展示:
2. COORD 控制台屏幕上的坐标
控制台的坐标分布是上图所示
COORD类型的声明:
typedef struct _ COORD {SHORT X;SHORT Y;} COORD, *PCOORD;
COORD pos = { 10, 15 }; //给坐标赋值为(10,15)
注意:需要包含头文件windows.h
3. GetStdHandle
//函数声明
HANDLE GetStdHandle(DWORD nStdHandle);
函数参数:
函数使用:
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL; //HANDLE 是一个指针变量 typedef void *HANDLE
houtput = GetStdHandle(STD_OUTPUT_HANDLE); //获得屏幕标准输出设备
return 0;
}
4. GetConsoleCursorInfo
用来检索有关指定控制台屏幕缓重区的光标大小和可见性信息。
注:第二个参数是个指针,指向(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。
5. SetConsoleCursorInfo
//语法声明
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
第一个参数就是句柄,第二个参数就是光标信息的地址。
结合起来运用:
#include <stdio.h>
#include <stdbool.h> //false所需
#include <windows.h> //HANDLE等所需
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标的占比
//cursor_info.dwSize = 100;
cursor_info.bVisible = false;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
system("pause");
return 0;
}
效果如下:
改占比
修改可见性
6. SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
//语法声明
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
第一个参数是句柄
第二个参数就是位置
#include <stdio.h>
#include <windows.h>
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { 10, 20 };
SetConsoleCursorPosition(houtput, pos);
printf("hehe\n");
return 0;
}
效果如下:
我们把这个封装一个设置光标位置的函数
封装的SetPos函数
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
int main()
{
SetPos(10, 20);
printf("1\n");
SetPos(10, 10);
printf("2\n");
getchar();
//system("pause");
return 0;
}
效果展示如图:
7. GetAsyncKeyState
用来获取按键情况,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 返回值是short类型,在上一次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
SHORT GetAsyncKeyState(
int vKey
);
虚拟键码链接:虚拟键码
注:如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
因此我们可以定义一个宏
宏定义KEY_PRESS
//结果是1表示按过
//结果是0表示未按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
这个就以便于我们后面实现贪吃蛇用按键控制方向所使用。
三. 贪吃蛇游戏实现
1. 贪吃蛇地图
我们可以用自己所想要的符号来完成地图(墙)的创建,这里使用了宽字符,墙体的为□,蛇的为⚪,食物为★。
注意:普通字符占一个字节,宽字符占2个字节。
<locale.h>和类项
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。在标准中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,每一个宏,指定一个类项。每个类项的详细说明:类项
注:LC_ALL-针对所有类项修改,将所有类别设置为给定的语言环境。
setlocale函数
//函数原型
char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常⽅式执⾏,小数点是一个点。
若要改变地区,调用setlocale函数。用""作为第二个参数
setlocale(LC_ALL, " ");//切换到本地环境
打印地图
在此之前,需要知道宽字符打印的格式和函数
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应 wprintf() 的占位符为 %ls 。
//使用格式如下
#include <stdio.h>
#include <locale.h>
int main()
{
//设置本地化
setlocale(LC_ALL, "");
char a = 'a';
char b = 'b';
printf("%c%c\n", a, b);
wchar_t wc1 = L'测';
wchar_t wc2 = L'试';
wprintf(L"%lc\n", wc1);
wprintf(L"%lc\n", wc2);
wprintf(L"%lc\n", L'●');
wprintf(L"%lc\n", L'★');
return 0;
}
我们看一下结果:
ab是普通字符,以下都是宽字符,对比看出,ab两个字符所占字节才等于一个宽字符所占字节。
我们要设计一个27*27的范围,就是要设计一个27行,58列的棋盘,我们封装的函数SetPos设置光标坐标函数就用上了
封装的墙函数
#define WALL L'□'
//创建地图
void CreateMap()
{
//上
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26); //设置光标坐标
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i); //设置光标坐标
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i); //设置光标坐标
wprintf(L"%lc", WALL);
}
}
我们的墙就创建好了
2. 设计蛇和食物
蛇身我们用链表来维护,所以蛇的每一个节点就是链表的每个节点,所以节点要记录坐标就行。
注:蛇的每个节点的x坐标必须是2的倍数,并且食物随机位置的x坐标也要为2的倍数,并且不能跟蛇身重合。
蛇的节点结构:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//*pSnakeNode是创建一个结构体指针类型,也可也写成
//typedef struct SnakeNode *pSnakeNdoe;
贪吃蛇游戏需要完成管理蛇的事项:
- 指向蛇头的指针,以便于后续使用
- 指向食物节点的指针,以便找到食物
- 蛇的方向(↑、↓、←、→)
- 游戏的状态(正常运行、撞墙、撞到自己、正常结束退出)
- 食物的分数
- 总分数
- 每走一步休眠时间(控制蛇的速度的)
所以我们可以创建所需要的自定义类型:
//用于维护贪吃蛇的
typedef struct Snake
{
pSnakeNode _pSnake; //指向蛇头的指针 维护整条蛇的指针
pSnakeNode _pFood; //指向食物节点的指针 维护食物的指针
enum DIRECTION _dir; //蛇的方向
enum GAME_STATUS _status; //游戏的状态
int _food_weight; //一个食物的分数
int _score; //总分数
int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
//用于控制蛇的方向
//蛇的方向(上、下、左、右)
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//用于蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK, //正常运行
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞自己
END_NORMAL //正常退出
};
3. 游戏实现流程
所以游戏主逻辑是:
- 游戏开始——完成游戏的初始化
- 游戏运行——完成游戏运行逻辑的实现
- 游戏结束——完成游戏结束的说明,释放所用资源
我们需要分三个文件来完成:test.c(用于完成游戏逻辑测试)、snake.c(用于完成游戏所需逻辑代码函数等)、snake.h(用于包含游戏所需头文件和声明类型和函数等)
//snake.h文件
//把所需头文件和所要声明的函数和类型等都要包含在里面,方便使用
//声明的函数要在对应的snake.c里实现
#pragma once
#include<windows.h>
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include <time.h>
#define POS_X 24
#define POS_Y 5
//宏定义判断是否按过键
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
#define WALL L'□' //墙
#define BODY L'●' //蛇身
#define FOOD L'★' //食物
//类型的声明
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK, //正常运行
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞自己
END_NORMAL //正常退出
};
//声明的蛇身节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;
typedef struct Snake
{
pSnakeNode _pSnake; //指向蛇头的指针 维护整条蛇的指针
pSnakeNode _pFood; //指向食物节点的指针 维护食物的指针
enum DIRECTION _dir; //蛇的方向
enum GAME_STATUS _status; //游戏的状态
int _food_weight; //一个食物的分数
int _score; //总分数
int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
//函数的声明
//定位
void SetPos(short x, short y);
//游戏的初始化
void GameStart(pSnake ps);
//欢迎界面和功能介绍
void WelcomeToGame();
//地图创建
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行
void GameRun(pSnake ps);
//蛇移动-走一步
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);
//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);
//结束
void GameEnd(pSnake ps);
//撞墙
void KillByWall(pSnake ps);
//撞自己
void KillBySelf(pSnake ps);
//test.c文件中
#include <locale.h>
#include "snake.h"
//完成的是游戏的测试逻辑
void test()
{
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//开始游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏 - 善后工作
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n'); //用于清理\n
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL)); //用于rand函数随机生成食物
test();
return 0;
}
接下来就是实现snake.c里的函数逻辑了。
1. 游戏开始-GameStart
我们需要完成以下的:
- 初始化游戏,先设置窗口大小,再光标隐藏。
- 打印欢迎界面、功能介绍
- 创建地图
- 创建蛇-初始化
- 创建食物
void GameStart(pSnake ps)
{
//初始化游戏
//0.光标隐藏,先设置窗口大小,再光标隐藏
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(houtput, &CursorInfo); //设置控制台光标状态
//1.打印环境界面
//2.功能介绍
WelcomeToGame();
//3.绘制地图
CreateMap();
//4.创建蛇
InitSnake(ps);
//5.创建食物
CreateFood(ps);
1.打印欢迎界面
//欢迎界面和功能介绍
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(42, 20);
system("pause");
system("cls");
SetPos(25, 14);
wprintf(L"用↑.↓.←.→ 来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause");
system("cls");
}
注:在前面我们封装了SetPos函数,要放在snake.c里。(点击目录封装SetPos函数就可以看到)
2.打印创建地图
将墙打印出来,由于宽字符要用wprintf函数,并且注意格式,坐标的计算:
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
注:打印地图函数也封装在前面,在目录里封装的墙函数。
3.创建蛇-初始化
- 蛇开始的长度(如5节),每节对应链表的一个节点,每一个节点都有自己的坐标,创建这些节点存放到链表中管理,创建完后,将蛇的每一节打印在屏幕上
- 蛇的初始位置(如(24,5))
- 游戏状态:OK(正常运行)
- 蛇的移动速度:200毫秒
- 蛇的默认方向:RIGHT(右边)
- 初始成绩:0
- 食物的分数:10
//初始化蛇身
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL; //创建一个struct SnakeNode*类型(pSnakeNode)的指针变量cur,用于存储蛇的节点
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //申请动态空间用于存储蛇的节点
if (cur == NULL) //如申请失败,结束程序
{
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i; //初始化x坐标
cur->y = POS_Y; //初始化y坐标
//头插法插入链表
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->_score = 0; //默认总分0
ps->_food_weight = 10; //食物分数10
ps->_sleep_time = 200; //单位毫秒
ps->_status = OK; //正常运行
}
4.创建食物
- 随机生成食物的坐标:x坐标必须2的倍数、食物坐标不能和蛇身重复
- 创建食物节点,打印
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x是2的倍数
//x:2---52
//y:1---25
again:
//要使x坐标为2的倍数,否则对不齐蛇头坐标
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//x和y的坐标不能和蛇的身体坐标冲突
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;
}
else
{
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y); //定位位置
wprintf(L"%lc", FOOD);
ps->_pFood = pFood; //把随机的食物节点赋值到用于维护贪吃蛇的结构体成员中用于维护食物
}
}
查看一下成果:
可以看到开始游戏已经实现,蛇要运动还需要完成GameRun
2. 游戏运行-GameRun
- 根据游戏状态检查游戏是否继续,如果是OK,则继续,否则结束。
- 如果游戏继续,检查按键情况,确定蛇下一步方向、是否加速减速、是否暂停或者退出游戏。
- 我们要在(64,15)这个坐标开始,在运行期间,打印帮助信息,提示玩家。
先完成PrintHelpInfo打印帮助信息函数
//打印帮助信息
void PrintHelp()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls",L"用↑.↓.←.→ 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls",L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}
所需虚拟键盘的罗列:
- 上:VK_UP
- 下:VK_DOWN
-
左: VK_LEFT
-
右: VK_RIGHT
-
空格: VK_SPACE
- ESC:VK_ESCAPE
- F3:VK_F3
- F4:VK_F4
//游戏运行
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelp();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight); //%2d防止打印一位数时出bug
//判断按键情况,并做出相应操作
if (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_SPACE))
{
//暂停
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//贪吃蛇走一步的过程
Sleep(ps->_sleep_time); //蛇每次之间要休眠的时间,时间短,蛇移动的就快。
} while (ps->_status == OK);
}
暂停—Pause函数
//暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
1.蛇的移动
逻辑是:先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动下一个位置的坐标。
注:
- 确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。
-
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
//蛇移动
void SnakeMove(pSnake ps)
{
//创建一个结点,表示蛇即将到的下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
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(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
注意:x+2是要确保x坐标为2的倍数。
2.下一个位置是否是食物
无食物-NoFood
将下一个节点头插入蛇的身体,并将之前的蛇身最后一个节点打印为空格,释放掉蛇身最后一个节点。
注意:释放最后一个节点后,还要把指向最后一个节点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。
// 下一个位置不是食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印新的蛇的身体
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" "); //注意要两个空格,否则会出bug
//释放最后一个结点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
判断是否是食物-NextIsFood
只需判断下一个节点的坐标是否与所存储的食物的坐标一致就可,若是就返回真
//判断下一个坐标是否是食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->_pFood->x) && (pn->y == ps->_pFood->y);
}
3.吃掉食物——EatFood
只需将这个节点头插到蛇身,释放节点,再把蛇打印,无需打印空格,并且总分增加,最后重新创建食物。
//吃掉食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
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;
}
}
5.撞到自己——KillBySelf
判断蛇头坐标是否和蛇身体坐标重复
//撞到自己
void 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;
break;
}
cur = cur->next;
}
}
3. 游戏结束——GameEnd
游戏状态不再是OK的时候,要告知游戏结束原因,并且释放蛇的节点。
//结束
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status)
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
四. 展示所有全部代码
//test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include "snake.h"
//完成的是游戏的测试逻辑
void test()
{
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1. 打印环境界面
//2. 功能介绍
//3. 绘制地图
//4. 创建蛇
//5. 创建食物
//6. 设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏 - 善后工作
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');
//system("cls");
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
//snake.h
#pragma once
#include<windows.h>
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include <time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//宏定义判断是否按过键
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//类型的声明
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK, //正常运行
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞自己
END_NORMAL //正常退出
};
//声明的蛇身节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;
typedef struct Snake
{
pSnakeNode _pSnake; //指向蛇头的指针 维护整条蛇的指针
pSnakeNode _pFood; //指向食物节点的指针 维护食物的指针
enum DIRECTION _dir; //蛇的方向
enum GAME_STATUS _status; //游戏的状态
int _food_weight; //一个食物的分数
int _score; //总分数
int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
//函数的声明
//定位
void SetPos(short x, short y);
//游戏的初始化
void GameStart(pSnake ps);
//欢迎界面和功能介绍
void WelcomeToGame();
//地图创建
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行
void GameRun(pSnake ps);
//蛇移动-走一步
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);
//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);
//结束
void GameEnd(pSnake ps);
//撞墙
void KillByWall(pSnake ps);
//撞自己
void KillBySelf(pSnake ps);
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
//欢迎界面和功能介绍
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(42, 20);
system("pause");
system("cls");
SetPos(25, 14);
wprintf(L"用↑.↓.←.→ 来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause");
system("cls");
}
//创建地图
void CreateMap()
{
//上
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
//初始化蛇身
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL; //创建一个struct SnakeNode*类型(pSnakeNode)的指针变量cur,用于存储蛇的节点
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //申请动态空间用于存储蛇的节点
if (cur == NULL) //如申请失败,结束程序
{
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i; //初始化x坐标
cur->y = POS_Y; //初始化y坐标
//头插法插入链表
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->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200; //单位毫秒
ps->_status = OK;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x是2的倍数
//x:2---52
//y:1---25
again:
//要使x坐标为2的倍数,否则对不齐蛇头坐标
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//x和y的坐标不能和蛇的身体坐标冲突
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;
}
else
{
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y); //定位位置
wprintf(L"%lc", FOOD);
ps->_pFood = pFood; //把随机的食物节点赋值到用于维护贪吃蛇的结构体成员中用于维护食物
}
}
//开始游戏
void GameStart(pSnake ps)
{
//初始化游戏
//0.光标隐藏,先设置窗口大小,再光标隐藏
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(houtput, &CursorInfo); //设置控制台光标状态
//1.打印环境界面
//2.功能介绍
WelcomeToGame();
//3.绘制地图
CreateMap();
//4.创建蛇
InitSnake(ps);
//5.创建食物
CreateFood(ps);
}
//打印帮助信息
void PrintHelp()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用↑.↓.←.→ 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
SetPos(64, 25);
wprintf(L"%ls", L"DUST制作");
}
// 下一个位置不是食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印新的蛇的身体
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" "); //注意要两个空格,否则会出bug
//释放最后一个结点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
//判断下一个坐标是否是食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->_pFood->x) && (pn->y == ps->_pFood->y);
}
//吃掉食物
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
//撞墙
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;
}
}
//撞到自己
void 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;
break;
}
cur = cur->next;
}
}
//蛇移动
void SnakeMove(pSnake ps)
{
//创建一个结点,表示蛇即将到的下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
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(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
//暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//游戏运行
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelp();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight); //%2d防止打印一位数时出bug
//判断按键情况,并做出相应操作
if (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_SPACE))
{
//暂停
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//贪吃蛇走一步的过程
Sleep(ps->_sleep_time); //蛇每次之间要休眠的时间,时间短,蛇移动的就快。
} while (ps->_status == OK);
}
//结束
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status)
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
运行结果: