贪吃蛇实现
1.资料来源
https://learn.microsoft.com/zh-cn/windows/console/getstdhandle
2.前言
简介
贪吃蛇是久负盛名的游戏,和俄罗斯方块、扫雷等游戏位列于经典游戏的行列。
《贪食蛇》中玩家控制一条不断移动的蛇,在屏幕上吃掉出现的食物。每吃掉一个食物,蛇的身体就会变长。游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘。游戏最初是像素风格,后来发展出了3D版本和多人对战模式。玩家需要灵活操作,利用策略在有限的空间内避免碰撞,挑战高分。
实现基本功能
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上下左右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
技术要点
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win2API等。
WIN32API
Win32 API是Windows操作系统的核心编程接口,用于与操作系统内核直接交互。
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一种函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数的服务的对象是应用程序(Application),所以称之为Application Programming Interface,简称API函数, WIN32API也就是Microsoft Windows 32位平台的应用程序编辑接口。
控制台程序(Console)
平常运行起来的黑框其实就是控制台程序
win + R输入cmd打开控制台窗口
可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
mode con cols=100 lines=30
也可以通过命令命令设置控制台窗口的名字
title 贪吃蛇
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行,例如:
#include <stdlib.h>
//system函数可以执行系统命令
int main()
{
//设置控制台相关属性
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//暂停
//getchar();
system("pause");
return 0;
}
3.相关win32API函数
控制台屏幕上的坐标 COORSD
COORD 是Windows API上定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0, 0)的原点位于缓冲区的顶部左侧单元格。
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
示例
#include <windows.h>
int main()
{
COORD pos1 = { 0, 0 };
COORD pos2 = { 10, 20 };
system("pause");
return 0;
}
GetStdHandle 获取句柄
GetStdHandle是一个Windows API函数,它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中获取一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
HANDLE WINAPI GetStdHandle(
_In_ DWORD nStdHandle
);
HANDLE GetStdHandle(DWORD nStdHandle);
typedef void* HANDLE
CONSOLE_CURSOR_INFO 结构
包含有关控制台游标的信息。
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
GetConsoleCursorInfo 检索游标信息
检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput, //句柄
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo //指针
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关
主机游标(光标)的信息。
SetConsoleCursorInfo 设置游标信息
为指定的控制台屏幕缓冲区设置光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
示例1
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存储在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
printf("%d\n", cursor_info.dwSize);
system("pause"); //暂停
//修改光标的占比
cursor_info.dwSize = 50;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
system("pause");
return 0;
}
SetConsoleCursorPosition 设置控制台光标位置
设置指定控制台屏幕缓冲区中的光标位置。将要设置的坐标信息存储在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
示例:
void set_pos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
int main()
{
//直接定位光标位置
set_pos(10, 20);
system("pause");
return 0;
}
GetAsyncKeyState 获取按键情况
确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState 后是否按下了该键。
SHORT GetAsyncKeyState(
[in] int vKey
);
将键盘上每个键的虚拟键值传给函数,函数通过返回值来分辨键的状态。
GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键按过,否则为0。
参考:虚拟键代码 https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
分装宏函数
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1) ? 1 : 0)
//结果是1表示按过,结果是0表示没按过
示例:
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1) ? 1 : 0)
int main()
{
//按哪个数字打印哪个数字
while (1)
{
if (KEY_PRESS(0x30))
printf("0\n");
else if (KEY_PRESS(0x31))
printf("1\n");
else if (KEY_PRESS(0x32))
printf("2\n");
else if (KEY_PRESS(0x33))
printf("3\n");
else if (KEY_PRESS(0x34))
printf("4\n");
else if (KEY_PRESS(0x35))
printf("5\n");
else if (KEY_PRESS(0x36))
printf("6\n");
else if (KEY_PRESS(0x37))
printf("7\n");
else if (KEY_PRESS(0x38))
printf("8\n");
else if (KEY_PRESS(0x39))
printf("9\n");
}
return 0;
}
4.贪吃蛇游戏设计与分析
4.1地图
在游戏地图上,打印宽字符。普通的字符是占一个字节的,这类宽字符是占2个字节。
汉字本质上也是宽字符。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定地址都是单字节的,但是这些假定并不是在世界的任何地方都适用。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t 和宽字符的输入输出函数,加入了 <locale.h> 头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
4.1.1<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准库中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
4.1.2类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改。下面的一个宏,指定一个类项:
参考资料:https://learn.microsoft.com/zh-cn/cpp/text/locales-and-code-pages
https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170
4.1.3 setlocale函数
char *setlocale(
int category,
const char *locale
);
wchar_t *_wsetlocale(
int category,
const wchar_t *locale
);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值: “C”(正常模式)和 “”(本地模式)。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale(LC_ALL, "");
setlocale的返回值是一个字符串指针,表示已经设置好的格式,如果调用失败,则返回空指针NULL。
setlocale()可以用来查询当前的地区,这时第二个参数设为NULL就可以了。
示例:
#include <locale.h>
int main()
{
char* ret = setlocale(LC_ALL, NULL);
printf("%s\n", ret);
ret = setlocale(LC_ALL, "");
printf("%s\n", ret);
return 0;
}
参考资料:https://legacy.cplusplus.com/reference/clocale/setlocale/?kw=setlocale
/* setlocale example */
#include <stdio.h> /* printf */
#include <time.h> /* time_t, struct tm, time, localtime, strftime */
#include <locale.h> /* struct lconv, setlocale, localeconv */
int main ()
{
time_t rawtime;
struct tm * timeinfo;
char buffer [80];
struct lconv * lc;
time ( &rawtime );
timeinfo = localtime ( &rawtime );
int twice=0;
do {
printf ("Locale is: %s\n", setlocale(LC_ALL,NULL) );
strftime (buffer,80,"%c",timeinfo);
printf ("Date is: %s\n",buffer);
lc = localeconv ();
printf ("Currency symbol is: %s\n-\n",lc->currency_symbol);
setlocale (LC_ALL,"");
} while (!twice++);
return 0;
}
4.1.3 宽字符的打印
宽字符的字面量必须叫上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符。宽字符的打印使用wprintf,对应wprintf()的占位符为 %lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为 %ls。
int main()
{
//设置本地化
setlocale(LC_ALL, "");
//宽字符字面量前加 L
wchar_t ch1 = L'你';
wchar_t ch2 = L'好';
wchar_t ch3 = L'世';
wchar_t ch4 = L'界';
printf("%c%c\n", 'a', 'b');
//格式串前面也要加 L
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}
代码
snake.h
#pragma once
#include <stdio.h>
#include <locale.h>
#include <stdbool.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BOOY 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 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 KillByWall(pSnake ps);
//检查是否撞到自己
void KillBySelf(pSnake ps);
//结束运行-善后工作
void GameEnd(pSnake ps);
snake.c
#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(38, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(40, 20);
system("pause");
system("cls");
SetPos(30, 14);
wprintf(L"用↑.↓.→.←来控制蛇的移动,按z加速,按x减速\n");
SetPos(30, 15);
wprintf(L"加速能够获得更高的分数\n");
SetPos(40, 20);
system("pause");
system("cls");
}
//绘制地图
void CreateMap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
wprintf(L"%lc", WALL);
//左右
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 29; i++)
wprintf(L"%lc", WALL);
}
//初始化蛇身
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()");
exit(1);
}
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", BOOY);
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-54
// y: 1-25
//x = 2 * (rand() % 27) + 2;
again:
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;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
//1.游戏初始化
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.打印环境界面和功能介绍
WelcomeToGame();
//2.绘制地图
CreateMap();
//3.初始化蛇身
InitSnake(ps);
//4.创建食物
CreateFood(ps);
}
//打印帮助信息
void printHelpInfo()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用↑.↓.→.←来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按z加速,按x减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}
//检测虚拟键值
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1) ? 1 : 0)
//暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
//是食物返回1,不是食物返回0
//return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
return 1;
else
return 0;
}
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//在CreateFood()里创建了pFood和SnakeMove()里创建了pNextNode两个食物节点,需要连接一个,释放一个
//头插法
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", BOOY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(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", BOOY);
cur = cur->next;
}
//最后一个节点要打印空白字符(两个空格)覆盖原来的蛇身节点BOOY
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个节点
free(cur->next);
cur->next = NULL;
}
//检查是否撞墙
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);
}
//2.运行游戏
void GameRun(pSnake ps)
{
//打印帮助信息
printHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight);
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(0x5A))
{
//加速
if (ps->_sleep_time > 80)//分四档
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(0x58))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
//蛇的移动-走一步
SnakeMove(ps);
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
//3.结束运行-善后工作
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status)
{
case END_NORMAL:
printf("正常退出游戏\n");
break;
case KILL_BY_WALL:
printf("撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
printf("撞到自己,游戏结束\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 "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));
test();
return 0;
}