目录
1. 游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。 在编程语⾔的应用中,我将以贪吃蛇为例,从设计到代码实现来提升自己的编程能⼒和逻辑能⼒。
2. 游戏效果演示
提前说明:大家可以看到这种运行界面和我们平常看到的界面不一样,只有通过以下设置后,才能达到上图中的效果。这是VS的运行窗口:
记得要保存!!!!
3. Win32API 介绍
本次实现贪吃蛇会使⽤到的⼀些 Win32API 知识,接下来我们就学习⼀下
3.1 Win32API
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调⽤这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
3.2 控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令(mode命令)来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
(1)mode命令
system("mode con cols=100 lines=30");
(2)title命令
system("title 贪吃蛇"); //用来设置cmd窗口名称
3.3 控制台屏幕上的坐标COORD
COORD是WindowsAPI中定义的⼀个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
COORD类型的声明:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
用COORD给坐标赋值:
COORD pos = { 10, 15 };
3.4 GetStdHandle
GetStdHandle是一个WindowsAPI函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
3.5 GetConsoleCursorInfo
用于检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
3.5.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
3.6 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
3.7 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
3.8 GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
参考:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
4.基本功能的实现
4.1贪吃蛇地图绘制
我们最终的贪吃蛇大概要是这个样子,那我们的地图如何布置呢?
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占1个字节的,这类宽字符是占⽤2个字节。
那具体宽字符是怎么打印的呢?那我就来一一介绍。
这里再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
4.1.1 <locale.h>本地化
提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。在标准中,依赖地区的部分有以下几项:
•数字量的格式
•货币量的格式
•字符集
•日期和时间的表形式、
4.1.2 类 项
•LC_COLLATE:影响字符串比较函数 strcoll()和 strxfrm()。
•LC_CTYPE:影响字符处理函数的行为。
•LC_MONETARY:影响货币格式。
•LC_NUMERIC:影响 printf() 的数字格式。
•LC_TIME:影响时间格式 strftime() 和 wcsftime()。
•LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境。
4.1.3 setlocale函数
4.1.4宽字符的打印
4.1.5地图坐标
我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
#define WALL L'□'
#define BODY L'●'
#define FOOD 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);
}
}
4.2 蛇身和食物
4.2.1 蛇身
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
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;
cur->y = POS_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;}
4.2.2 食物
4.3 数据结构设计
4.4 游戏运行(GameRun)
4.4.1 KEY_PRESS
检测按键状态,我们封装了⼀个宏
4.4.2 PrintHelpInfo
4.4.3 蛇身移动
4.4.3.1 蛇的下一个移动位置为食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
return 1;
else
return 0;
}
4.4.3.2 蛇吃食物
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.4.3.3 蛇的下一个移动位置不是食物,蛇正常行走
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(" ");//释放最后一个节点
free(cur->next);//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
4.4.3.4 蛇若是撞墙则游戏结束
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;
}
}
4.4.3.5 蛇若是撞到自己则游戏结束
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;
}
}
4.5游戏结束
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);
}
}