1. 项目目标
项目为控制台程序,在控制台上实现。
1. 实现游戏开始界面,让用户选择模式(撞墙/循环)等;
2. 实现地图的绘制;
3. 实现贪吃蛇,并使其根据用户的命令移动;
4. 实现食物的随机刷新,并为每一种食物设置分值;
5. 在游戏过程中打印适当的提示信息以及实时信息的刷新;
6. 实现贪吃蛇的变长;
7. 实时检测游戏状态(正常/撞墙/撞到自己);
2. 所需知识
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
这些知识,想必大家在学习C语言的过程中已经熟练到出神入化了,但是对于Win32 API可能会感到有点陌生。
接下来我们重点介绍一下Win32 API。
2.1 WIn32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。
Win32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
利用Win32 API提供的函数,我们就可以比较方便地在控制台上进行信息的打印,地图的绘制等。
以下为这个项目需要用到的一些API函数。
2.1.1 GetStdHandle 函数 - Windows Console | Microsoft Learn
HANDLE GetStdHandle(DWORD nStdHandle);
GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值,类型为HANDLE),使用这个句柄可以操作设备。
句柄其实就类似于FILE*指针:当你需要对某个文件进行操作时,你需要将对应的文件指针传入对应的函数;当你需要对某个设备进行操作时,你需要将对应的句柄传入对应的函数。
他的参数有以下三个:
值 | 含义 |
STD_INPUT_HANDLE | 标准输入设备 |
STD_OUTPUT_HANDLE | 标准输出设备 |
STD_ERROR_HANDLE | 标准错误设备 |
控制台的屏幕属于标准输出设备,所以在该项目中我们在使用这个函数时传入的都是STD_OUTPUT_HANDLE,然后定义一个HANDLE类型的变量来接收其返回的句柄即可。
例如:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2.1.2 GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
GetConsoleCursorInfo函数用于检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息 。
它的第一个参数为标准输出设备的句柄,第二个参数为一个指向 CONSOLE_CURSOR_INFO(CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn) 结构的指针,该结构接收有关控制台游标的信息。
CONSOLE_CURSOR_INFO结构的定义如下
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
• dwSize:表示光标填充的字符单元格的百分比, 此值介于1到100之间。
光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
• bVisible:表示光标的可见性。 如果光标可见,则此成员为 TRUE。
在使用时,我们需要先定义一个CONSOLE_CURSOR_INFO类型的变量,然后将其传入函数中。
此时,这个变量中就存储了当前光标的大小和可见性信息,我们可以通过访问这个结构体变量的成员来修改光标的参数。
例如:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
但是,该变量只是获得了一个备份并将其储存,并没有直接与光标建立联系。
要使修改后的参数得到应用,我们还需要调用GetConsoleCursorInfo函数。
2.1.3 SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
SetConsoleCursorInfo函数用于设置光标的参数,本质上就是让刚才获得的CONSOLE_CURSOR_INFO类型的变量存储的信息得到应用。
它的参数与GetConsoleCursorInfo函数一模一样,只需要在设置完光标参数之后,将变量再统统扔给它即可。
例如:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
2.1.4 SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
SetConsoleCursorPosition函数用于设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD(COORD 结构 - Windows Console | Microsoft Learn)类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
COORD结构体的定义如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。
水平向右为x轴正方向,竖直向下为y轴正方向。
y轴的单位长度(一个中文字符的高度/宽度)是x轴单位长度的两倍。
例如:
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
这段代码就是将光标的位置设置到(10,5)的位置。
由于在项目中,无论是绘制地图,打印蛇,打印食物还是在指定位置打印信息都需要设置光标位置,所以我们将设置光标位置的功能封装为了一个函数SetPos。
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
2.1.5 getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn
SHORT GetAsyncKeyState(
int vKey
);
GetAsyncKeyState函数用于获取按键的状态。
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中:
• 最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬 起。
• 如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
为了使用方便,我们定义了下面这个宏:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
2.2 C语言的国际化特性
如果你需要用一些中文的宽字符来表示表示墙体,蛇,食物的话,就需要用到这个知识。
例如:打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
具体的细节,如果感兴趣的话可以自己去了解,在该项目中,你只需要知道setlocale函数即可。
char* setlocale (int category, const char* locale);
要使程序能够打印这些宽字符,只需要下面这个语句即可:
setlocal(LC_ALL, "");
3. 项目实现
3.1 Snake.h
这里我没有使用宽字符来表示墙,蛇,和食物,而是直接使用的汉字,目的是为了方便读者分辨定义的符号是什么意思。
要增加食物数量或种类的话,将FOOD_MAX的值修改之后,在FOOD_SHAPES和FOOD_WEIGHTS中分别添加其符号和分值即可。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <locale.h>
#include <time.h>
#include <stdbool.h>
#define WALL L'墙'
#define BODY L'蛇'
#define FOOD_MAX 3//食物种数
#define FOOD_SHAPES {L'史', L'蕉', L'苹'}//食物外形列表
#define FOOD_WEIGHTS {1145, 10, 20}//食物分值列表
#define ORIGIN_LEN 5
#define ORIGIN_X 24
#define ORIGIN_Y 5
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 0x1) ? 1 : 0)
//类型定义
enum DIRECTIONS//方向
{
UP,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
enum STATUS//游戏状态
{
NORMAL,//正常
END_NORMAL,//正常退出
END_WALL,//撞墙结束
END_SELF//撞到自己结束
};
//蛇身结点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, *pSnakeNode;
//食物结点
typedef struct Food
{
int x;
int y;
int weight;
WCHAR shape;
}Food, *pFood;
//贪吃蛇游戏
typedef struct Snake
{
pSnakeNode pSnakeHead;//蛇头
Food Recipe[FOOD_MAX];//食物清单
enum DIRECTIONS Direction;//运动方向
enum STATUS Status;//游戏状态
int Score;//总分数
int SleepTime;//睡眠时间
int Attached;//附加分数
}Snake, *pSnake;
//函数定义
//设置光标位置
void SetPos(int x, int y);
//游戏开始
int GameBegin(pSnake ps);
//欢迎界面
int WelComeToGame();
//创建地图26/58
void CreatMap();
//初始化贪吃蛇
void InitSnake(pSnake ps);
//初始化食物
void InitRecipe(pSnake ps);
//游戏运行
void GameRun(pSnake ps, int flag);
//打印提示信息
void PrintHelpInfo();
//打印实时信息
void PrintTimelyInfo(pSnake ps);
//暂停
void Pause(pSnake ps);
//蛇行
void SnakeMove(pSnake ps, int flag);
//是否即将触碰食物
int IsTouchingFood(pSnakeNode pn, pSnake ps);
//没吃到食物
void NoFood(pSnake ps);
//改变某食物坐标
void FoodPosModify(pSnake ps, int pos);
//检测是否撞墙
void TouchingWall(pSnakeNode pn, pSnake ps, int flag);
//检测是否撞到自己
void TouchingSelf(pSnakeNode pn, pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
3.2 Snake.c
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"
//设置光标位置
void SetPos(int x, int y)
{
HANDLE HandOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(HandOutPut, pos);
}
//欢迎界面
int WelComeToGame()
{
SetPos(45, 14);
wprintf(L"%ls", L"欢迎来到贪吃蛇小游戏");
SetPos(45, 16);
wprintf(L"%ls", L"1.撞墙模式(撞墙会死)");
SetPos(45, 17);
wprintf(L"%ls", L"2.循环模式(撞墙不会死)");
int choice = 0;
SetPos(43, 16);
printf(">");
do
{
if (KEY_PRESS(VK_UP))//撞墙模式
{
SetPos(43, 16);
printf(">");
SetPos(43, 17);
printf(" ");
choice = 0;
}
else if (KEY_PRESS(VK_DOWN))//循环模式
{
SetPos(43, 17);
printf(">");
SetPos(43, 16);
printf(" ");
choice = 1;
}
else if (KEY_PRESS(VK_RETURN))
{
break;
}
} while (1);
SetPos(45, 25);
system("pause");
system("cls");
SetPos(37, 14);
wprintf(L"用上下左右来控制蛇的移动,按F3加速,按F4减速\n");
SetPos(48, 15);
wprintf(L"加速能获得更高的分数\n");
SetPos(48, 17);
system("pause");
system("cls");
return choice;
}
//创建地图26/58
void CreatMap()
{
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 < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
//初始化贪吃蛇
void InitSnake(pSnake ps)
{
//初始化贪吃蛇属性
ps->pSnakeHead = NULL;
ps->Direction = RIGHT;
ps->Score = 0;
ps->Status = NORMAL;
ps->SleepTime = 200;
ps->Attached = 0;
//创建蛇
pSnakeNode cur = NULL;
for (int i = 0; i < ORIGIN_LEN; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake::malloc");
return;
}
cur->next = NULL;
cur->x = ORIGIN_X + 2 * i;
cur->y = ORIGIN_Y;
//头插
cur->next = ps->pSnakeHead;
ps->pSnakeHead = cur;
}
//打印蛇
cur = ps->pSnakeHead;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
//获取合法的食物坐标
void GetPos(pSnake ps, int* x, int* y)
{
again:
do
{
*x = rand() % 54 + 2;
*y = rand() % 25 + 1;
} while (*x % 2 != 0);
pSnakeNode cur = ps->pSnakeHead;
while (cur)
{
if (cur->x == *x && cur->y == *y)
goto again;
cur = cur->next;
}
for (int i = 0; i < FOOD_MAX; i++)
{
if (ps->Recipe[i].x == *x && ps->Recipe[i].y == *y)
goto again;
}
}
//初始化食物
void InitRecipe(pSnake ps)
{
int x = 0;
int y = 0;
WCHAR shapes[FOOD_MAX] = FOOD_SHAPES;
int weights[FOOD_MAX] = FOOD_WEIGHTS;
for (int i = 0; i < FOOD_MAX; i++)
{
GetPos(ps, &x, &y);
ps->Recipe[i].x = x;
ps->Recipe[i].y = y;
ps->Recipe[i].shape = shapes[i];
ps->Recipe[i].weight = weights[i];
SetPos(x, y);
wprintf(L"%lc", ps->Recipe[i].shape);
}
}
//游戏开始
int GameBegin(pSnake ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//欢迎界面
int flag = WelComeToGame();
//创建地图27/58
CreatMap();
//初始化贪吃蛇
InitSnake(ps);
//初始化食物
InitRecipe(ps);
return flag;
}
//打印提示信息
void PrintHelpInfo()
{
SetPos(64, 16);
wprintf(L"%ls", L"不能咬到自己");
SetPos(64, 17);
wprintf(L"%ls", L"用上下左右来控制蛇移动");
SetPos(64, 18);
wprintf(L"%ls", L"按F3加速,按F4减速");
SetPos(64, 19);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}
//打印实时信息
void PrintTimelyInfo(pSnake ps)
{
SetPos(64, 9);
printf("总分数:%d\n", ps->Score);
for (int i = 0; i < FOOD_MAX; i++)
{
SetPos(64, 11 + i);
wprintf(L"%lc", ps->Recipe[i].shape);
printf("的分数为:%d", ps->Recipe[i].weight);
}
SetPos(64, 11 + FOOD_MAX);
printf("附加分数为:%d", ps->Attached);
}
//暂停
void Pause(pSnake ps)
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
if (KEY_PRESS(VK_ESCAPE))
{
ps->Status = END_NORMAL;
break;
}
}
}
//是否即将触碰食物(是,则返回食物下标;否,则返回-1)
int IsTouchingFood(pSnakeNode pn, pSnake ps)
{
for (int i = 0; i < FOOD_MAX; i++)
{
if (pn->x == ps->Recipe[i].x && pn->y == ps->Recipe[i].y)
return i;
}
return -1;
}
//没吃到食物
void NoFood(pSnake ps)
{
pSnakeNode cur = ps->pSnakeHead;
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
//改变某食物坐标
void FoodPosModify(pSnake ps, int pos)
{
int x = 0;
int y = 0;
GetPos(ps, &x, &y);
ps->Recipe[pos].x = x;
ps->Recipe[pos].y = y;
SetPos(x, y);
wprintf(L"%lc", ps->Recipe[pos].shape);
}
//检测是否撞墙
void TouchingWall(pSnakeNode pn, pSnake ps, int flag)
{
if (flag == 1)
{
if (pn->x == 0)
{
pn->x = 54;
}
if (pn->x == 56)
{
pn->x = 2;
}
if (pn->y == 26)
{
pn->y = 1;
}
if (pn->y == 0)
{
pn->y = 25;
}
}
else
{
if (pn->x == 0 || pn->x == 56 || pn->y == 26 || pn->y == 0)
{
ps->Status = END_WALL;
}
}
}
//检测是否撞到自己
void TouchingSelf(pSnakeNode pn, pSnake ps)
{
pSnakeNode cur = ps->pSnakeHead;
while (cur)
{
if (cur->x == pn->x && cur->y == pn->y)
{
ps->Status = END_SELF;
return;
}
cur = cur->next;
}
}
//蛇行
void SnakeMove(pSnake ps, int flag)
{
pSnakeNode newhead = (pSnakeNode)malloc(sizeof(SnakeNode));
if (newhead == NULL)
{
perror("SnakeMove::malloc");
return;
}
switch (ps->Direction)
{
case UP:
newhead->x = ps->pSnakeHead->x;
newhead->y = ps->pSnakeHead->y - 1;
break;
case DOWN:
newhead->x = ps->pSnakeHead->x;
newhead->y = ps->pSnakeHead->y + 1;
break;
case LEFT:
newhead->x = ps->pSnakeHead->x - 2;
newhead->y = ps->pSnakeHead->y;
break;
case RIGHT:
newhead->x = ps->pSnakeHead->x + 2;
newhead->y = ps->pSnakeHead->y;
break;
}
//检测是否撞墙
TouchingWall(newhead, ps, flag);
//检测是否撞到自己
TouchingSelf(newhead, ps);
newhead->next = ps->pSnakeHead;
ps->pSnakeHead = newhead;
SetPos(newhead->x, newhead->y);
wprintf(L"%lc", BODY);
//判断是否即将碰到食物
int judge = IsTouchingFood(newhead, ps);
if (judge == -1)
{
//还没吃到食物,删掉最后一个结点
NoFood(ps);
}
else
{
//修改某食物坐标
FoodPosModify(ps, judge);
ps->Score += ps->Recipe[judge].weight + ps->Attached;
}
}
//游戏运行
void GameRun(pSnake ps, int flag)
{
//打印提示信息
PrintHelpInfo(ps);
do
{
//打印实时信息
PrintTimelyInfo(ps);
//检测按键
if (KEY_PRESS(VK_UP) && ps->Direction != DOWN)
{
ps->Direction = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->Direction != UP)
{
ps->Direction = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->Direction != RIGHT)
{
ps->Direction = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->Direction != LEFT)
{
ps->Direction = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
//暂停
Pause(ps);
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->Status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->SleepTime > 80)
{
ps->SleepTime -= 30;
ps->Attached += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->SleepTime < 320)
{
ps->SleepTime += 30;
ps->Attached -= 2;
}
}
SnakeMove(ps, flag);
Sleep(ps->SleepTime);
} while (ps->Status == NORMAL);
}
//游戏结束
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->Status)
{
case END_NORMAL:
printf("退出游戏!\n");
break;
case END_WALL:
printf("撞墙啦牢底!\n");
break;
case END_SELF:
printf("紫砂啦牢底!\n");
break;
}
SetPos(24, 13);
//释放蛇身链表
pSnakeNode cur = ps->pSnakeHead;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
3.3 test.c
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"
void SnakeGame()
{
Snake snake = { 0 };
//光标隐藏
HANDLE HandOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(HandOutPut, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(HandOutPut, &CursorInfo);
int ch = 0;
do
{
fflush(stdin);
//游戏开始
int flag = GameBegin(&snake);
//游戏运行
GameRun(&snake, flag);
//游戏结束
GameEnd(&snake);
printf("要再来一次吗(Y/N):");
ch = getchar();
while (getchar() != '\n');
KEY_PRESS(VK_RETURN);
} while (ch == 'Y' || ch == 'y');
SetPos(0, 28);
}
int main()
{
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
SnakeGame();
return 0;
}