目录
1.目标
2.预备知识
①WIN32 API
②控制台程序
③控制台屏幕上的COORD
④GetStdHandle
⑤GetConsoleCursorInfo
CONSOLE_CURSOR_INFO
SetConsoleCursorInfo
⑥SetConsoleCursorPosition
⑦GetAsyncKeyState
3.实现过程
游戏主体逻辑
①游戏开始
本地化
②游戏过程
③游戏结束
4.游戏整体流程
5.源码
在控制台中实现一个简易的贪吃蛇小程序,蛇体可用●代替,作为边界的墙体用□表示,食物用★表示。
最终效果如下
同时实现蛇的移动,吃食物的反馈,以及游戏结束条件判断。
有关部分WIN32 API的介绍。window系统提供了一些函数接口,可以让我们对应用程序进行一些操作。这些函数大概就是WIN32 API。
控制台也是一种应用程序。可以在控制台中使用一些指令完成一些操作。
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
mode con cols=100 lines=30
而类似这种指令也可以调用C语言函数system执行
#include
int main()
{
//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗口名称
system("title 贪吃蛇");
return 0;
}
COORD是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标。
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
这是其定义。
对于控制台来说,坐标意义如下
如图为x轴和y轴。
“GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标
准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。”
这是官方的解释,通俗来讲,你可以同时开多个控制台窗口,并且你可以获得其中的一个窗口的句柄,用这个句柄来操作那个对应的控制台。
HANDLE GetStdHandle(DWORD nStdHandle);
nStdHandle只有三种取值
分别对应标准输入,标准输出,和标准错误。
这里我们要使用标准输出。
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
这样得到一个标准输出的句柄。
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
这是该函数声明。使用该函数可获得关于光标的有关信息,下图中白色闪烁的就是光标。
hConsoleOutput就是通过④获得的句柄,lpConsoleCursorInfo就是一个指向光标的指针。
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo); //获取控制台光标信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
这个结构体就是光标结构体,dwSize指光标的大小,范围为1~99,bVisible为true意为光标可见,
为假意为光标隐藏。
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
这是该函数声明,使用它可以改变指定的控制台的光标状态,比如 光标是否隐藏,大小又为多少。
//获得句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo); //获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo); //设置控制台光标状态
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
将光标设置到目标位置。
将其封装成设置光标位置的函数
//设置光标坐标
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
SHORT GetAsyncKeyState(
int vKey
);
vKey 是虚拟按键值,可以在微软官方查到Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn
可获取键盘上的按键的状态,分辨是否为按压,抬起,或已被按过,或未被按过等等。
GetAsyncKeyState返回值是short类型,当返回数据最低位是1时说明该按键已被按过。
可封装为宏
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
开始前想到需要管理蛇,食物,和整个游戏的各项参数(如单个食物得分,总得分,游戏是否正常进行。
我们使用链表来管理蛇,链表节点数据中存储蛇节点的坐标。
//蛇的节点坐标为数据类型
typedef SnakeNodePos SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SLT;
//蛇节点的坐标
typedef struct SnakeNodePos
{
int x;
int y;
}SnakeNodePos;
//蛇的每个节点
typedef SLT SnakeNode;
食物也需要存储坐标,因此也使用 SnakeNode来存储。
同时还需要管理整个游戏的相关参数,我们用
//游戏状态
enum GameStatus
{
//正常
OK = 1,
//ESC退出
ESC,
//撞墙结束
KILL_BY_WALL,
//撞到自己结束
KILL_BY_SELF
};
typedef enum Direction
{
UP = 1,
DOWN,
LEFT,
RIGHT
}Direction;
typedef struct Maintenance
{
//蛇头
SnakeNode* ps;
//游戏状态
enum GameStatus status;
//食物
SnakeNode* pFood;
//蛇运动方向
Direction direction;
//总得分
int Score;
//当前吃掉食物所获得的分数
int Add;
//停顿时间
int SleepTime;
}Maintenance;
游戏分为开始,过程和结束,并且能玩很多次
void test()
{
Maintenance* maintence = NULL;
char ch = 0;
do
{
//游戏开始前的准备工作
GameStart(&maintence);
//游戏开始
GameRun(&maintence);
//游戏结束
GameEnd(&maintence);
system("pause");
SetPos(5, COL / 2 + 1);
printf("再来一局?Y/N");
//读取字符是否为y或Y
ch = getchar();
getchar();
SetPos(0, COL + 1);
} while (ch == 'y' || ch == 'Y');
}
玩家开始操作蛇之前要完成的功能有打印地图边界,初始化一条五个节点的蛇,并且打印出一个食物。
那么,如何打印出这些信息?
事实上,之前决定用的字符,在默认的c标准下是不能直接打印的,像●,□,★都占了两个字节,属于宽字符。
这里简单讲下C语言国际化标准,早期C语言不适合非英语国家使用,因为ascii编码的字符数量太少,不足以满足世界各地的字符使用需求(汉字就有几万个)。为了解决这个问题,C语言引入了本地化。
该库提供的函数中可以根据地区不同进行不同的行为,如货币量的格式,⽇期和时间的表⽰形式,
使用该库中的setlocale函数,可以改变一个或多个类项。
char* setlocale (int category, const char* locale);
这里我们直接
setlocale(LC_ALL, " "); //切换到本地环境
就可以存储并使用宽字符。
初始化蛇,建立5个节点并链接起来,可以使用链表尾插的方式建立五个节点的链表。
然后决定地图边界的坐标,将其打印。
在按照蛇的每个节点存储的坐标,打印出蛇的每个节点。
最后生成食物,并打印到控制台,这里需要注意的是:
食物不能生成在地图边界
食物不能生成在蛇内
食物的x坐标是偶数
因为宽字符占了两位
如图,两个字符a和●平齐。
保持食物x坐标是偶数可以使得蛇吃食物成功时视觉效果更好,且避免出现意想不到的bug。
游戏运行,完成蛇的移动,加速减速,吃到食物的反馈及蛇身的变化。
移动需要确定方向,默认方向这里我们设置为左。
根据W,A,S,D是否被按下,确定蛇的运动方向。这里就可以用到上面在⑦中写的宏。
确定运动方向之后,就来实现运动功能。
不妨将运动的过程分解一下,分解为一个节点的运动,蛇向某个方向运动,就记录下,蛇头的下一个到达的坐标,若给坐标为食物坐标,就将这个节点头插到蛇头,若不是食物坐标,就头插后把尾节点删除。完成这步操作后,就打印新的蛇头节点。然后用循环,就可以连续运动。再借助Sleep函数,就可以产生运动的视觉效果。
还有暂停效果的实现:
使用Sleep函数和循环即可。
最后判断是否咬到自己和是否撞到边界较简单。
结束较为简单,只需要释放蛇占用的空间即可。
void GameEnd(Maintenance** ppM)
{
assert(ppM);
SnakeNode* cur = (*ppM)->ps;
//释放空间
while (cur)
{
SnakeNode* next = cur->next;
free(cur);
cur = next;
}
//重新将各项数值变成原始状态,以便开始新的一局
(*ppM)->ps = NULL;
(*ppM)->direction = LEFT;
(*ppM)->status = OK;
(*ppM)->pFood = NULL;
(*ppM)->Score = 0;
(*ppM)->Add = 20;
(*ppM)->SleepTime = 200;
}
头文件存放函数声明和有关结构体
Snake.h
#pragma once
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//地图长宽
#define ROW 58//对应x坐标
#define COL 27//对应y坐标
//蛇身初始长度
#define SnakeLength 5
#define WALL L'□'
#define NODE L'●'
#define FOOD L'★'
//声明链表结构
typedef struct SListNode SLT;
//蛇节点的坐标
typedef struct SnakeNodePos
{
int x;
int y;
}SnakeNodePos;
//蛇的每个节点
typedef SLT SnakeNode;
//游戏状态
enum GameStatus
{
//正常
OK = 1,
//ESC退出
ESC,
//撞墙结束
KILL_BY_WALL,
//撞到自己结束
KILL_BY_SELF
};
typedef enum Direction
{
UP = 1,
DOWN,
LEFT,
RIGHT
}Direction;
typedef struct Maintenance
{
//蛇头
SnakeNode* ps;
//游戏状态
enum GameStatus status;
//食物
SnakeNode* pFood;
//蛇运动方向
Direction direction;
//总得分
int Score;
//当前吃掉食物所获得的分数
int Add;
//停顿时间
int SleepTime;
}Maintenance;
//设置光标位置
void SetPos(short x, short y);
//游戏开始的欢迎语和地图打印
void GameStart(Maintenance** ppM);
void PrintMap(void);
//初始化蛇为5个节点
void SnakeInit(Maintenance** ppM);
//打印蛇
void PrintSnake(SnakeNode* ps);
//创建食物
bool FoodInSnake(Maintenance** ppM);
void CreateFood(Maintenance** ppM);
//游戏运行
void GameRun(Maintenance** ppM);
void MoveSnake(Maintenance** ppM, SnakeNode** Next);
void Pause(Maintenance** ppM);
void EatFood(Maintenance** ppM, SnakeNode** Next);
void NotEatFood(Maintenance** ppM, SnakeNode** Next);
void PrintHelpInfo(Maintenance** ppM);
bool KillBySelf(Maintenance** ppM, SnakeNode** Next);
bool KillByWall(SnakeNode** Next);
//游戏结束
void GameEnd(Maintenance** ppM);
实现函数接口的源文件
Snake.c
#include"SList.h"
//设置光标坐标
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
void PrintMap(void)
{
int i = 0;
setlocale(LC_ALL, "");
wchar_t wall = WALL;
for (i = 0; i < ROW; i += 2)
wprintf(L"%c", wall);
SetPos(0, 1);
for (i = 0; i < COL - 1; i++)
wprintf(L"%c\n", wall);
SetPos(ROW - 2, 1);
for (i = 0; i < COL - 1; i++)
{
wprintf(L"%c", wall);
SetPos(ROW - 2, i + 2);
}
SetPos(0, 27);
for (i = 0; i < ROW; i += 2)
wprintf(L"%c", wall);
}
void GameStart(Maintenance** ppM)
{
//调整控制台大小
system("mode con cols=100 lines=35");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
//欢迎语
SetPos(40, 16);
printf("欢迎来到贪吃蛇");
SetPos(40, 20);
system("pause");
printf("按空格以继续");
system("cls");
//游戏操作说明
SetPos(32, 16);
printf("用W A S D控制方向,Q是加速,E是减速\n");
SetPos(40, 20);
printf("加速获得更高分数");
SetPos(40, 25);
system("pause");
system("cls");
//打印地图
PrintMap();
//蛇初始化
SnakeInit(ppM);
//食物初始化
CreateFood(ppM);
}
void PrintSnake(SnakeNode* ps)
{
assert(ps);
SnakeNode* cur = ps;
wchar_t node = NODE;
while (cur)
{
SetPos(cur->data.x, cur->data.y);
wprintf(L"%c", node);
cur = cur->next;
}
}
//初始化蛇
void SnakeInit(Maintenance** ppM)
{
assert(ppM);
//初始化蛇的位置坐标
SnakeNodePos pos[SnakeLength] = { {16,8},{18,8},{20,8},{22,8},{24,8} };
if (*ppM == NULL)
*ppM = (Maintenance*)malloc(sizeof(Maintenance));
if (*ppM == NULL)
{
perror("SnakeInit malloc");
return;
}
(*ppM)->ps = NULL;
for (int i = 0; i < SnakeLength; i++)
SLTPushBack(&(*ppM)->ps, &pos[i]);//链表的尾插操作
//打印蛇身
PrintSnake((*ppM)->ps);
//设置初始运动方向
(*ppM)->direction = LEFT;
(*ppM)->status = OK;
(*ppM)->pFood = NULL;
(*ppM)->Score = 0;
(*ppM)->Add = 20;
(*ppM)->SleepTime = 200;
}
//判断食物是否生成在蛇体内
bool FoodInSnake(Maintenance** ppM)
{
assert(ppM);
SnakeNode* cur = (*ppM)->ps;
while (cur)
{
if (cur->data.x == (*ppM)->pFood->data.x
&& cur->data.y == (*ppM)->pFood->data.y)
return true;
cur = cur->next;
}
return false;
}
void PrintHelpInfo(Maintenance** ppM)
{
assert(ppM);
SetPos(60, 13);
printf("用W,A,S,D控制移动,Q加速,E减速,空格暂停 ");
SetPos(60, 14);
printf("加速可获得更高分数");
SetPos(60, 15);
printf("不能撞墙,咬到自己");
SetPos(60, 16);
printf("当前食物分数:%02d",(*ppM)->Add);
SetPos(60, 17);
printf("当前总得分:%02d", (*ppM)->Score);
}
//创建食物
void CreateFood(Maintenance** ppM)
{
assert(ppM);
if (*ppM == NULL)
*ppM = (Maintenance*)malloc(sizeof(Maintenance));
if (*ppM == NULL)
{
perror("SnakeInit malloc");
return;
}
int x, y;
//生成食物
(*ppM)->pFood = (SnakeNode*)malloc(sizeof(SnakeNode));
if ((*ppM)->pFood == NULL)
{
perror("Food Malloc");
return;
}
//生成食物坐标
srand((size_t)time(NULL));
do
{
x = rand() % (ROW - 5) + 2;
y = rand() % (COL - 1) + 1;
(*ppM)->pFood->data.x = x;
(*ppM)->pFood->data.y = y;
} while (x % 2 || FoodInSnake(ppM));
//初始化食物
(*ppM)->pFood->next = NULL;
//打印食物
wchar_t food = FOOD;
SetPos(x, y);
wprintf(L"%c", food);
}
//游戏运行,完成蛇的移动,加速减速,吃到食物的反馈及蛇身的变化
void GameRun(Maintenance** ppM)
{
assert(ppM);
PrintHelpInfo(ppM);
do
{
SnakeNode* Next = (SnakeNode*)malloc(sizeof(SnakeNode));
//各个按键的反馈
if (KEY_PRESS(0x41) && (*ppM)->direction != RIGHT)
(*ppM)->direction = LEFT;
else if (KEY_PRESS(0x44) && (*ppM)->direction != LEFT)
(*ppM)->direction = RIGHT;
else if (KEY_PRESS(0x57) && (*ppM)->direction != DOWN)
{
(*ppM)->direction = UP;
}
else if (KEY_PRESS(0x53) && (*ppM)->direction != UP)
(*ppM)->direction = DOWN;
else if (KEY_PRESS(VK_SPACE))
{
//暂停功能
Pause(ppM);
}
else if (KEY_PRESS(VK_ESCAPE))
{
(*ppM)->status = ESC;
break;
}
//蛇移动一步
if((*ppM)->status == OK)
MoveSnake(ppM, &Next);
} while ((*ppM)->status == OK);
//游戏结束的各个状态
SetPos(5, COL / 2);
if ((*ppM)->status == KILL_BY_SELF)
printf("咬到自己,游戏结束");
else if ((*ppM)->status == KILL_BY_WALL)
printf("撞到墙,游戏结束");
else if ((*ppM)->status == ESC)
printf("游戏结束");
}
void MoveSnake(Maintenance** ppM, SnakeNode** Next)
{
assert(ppM);
if ((*Next) == NULL)
{
perror("MoveSnake malloc");
return;
}
//获取蛇头下一个位置的坐标
switch ((*ppM)->direction)
{
case UP:
{
(*Next)->data.x = (*ppM)->ps->data.x;
(*Next)->data.y = (*ppM)->ps->data.y - 1;
break;
}
case DOWN:
{
(*Next)->data.x = (*ppM)->ps->data.x;
(*Next)->data.y = (*ppM)->ps->data.y + 1;
break;
}
case LEFT:
{
(*Next)->data.x = (*ppM)->ps->data.x - 2;
(*Next)->data.y = (*ppM)->ps->data.y;
break;
}
case RIGHT:
{
(*Next)->data.x = (*ppM)->ps->data.x + 2;
(*Next)->data.y = (*ppM)->ps->data.y;
break;
}
}
//判断是否咬到自己
if (KillBySelf(ppM, Next))
{
(*ppM)->status = KILL_BY_SELF;
return;
}
//判断撞墙
if (KillByWall(Next))
{
(*ppM)->status = KILL_BY_WALL;
return;
}
//判断是否吃到食物
if ((*Next)->data.x == (*ppM)->pFood->data.x
&& (*Next)->data.y == (*ppM)->pFood->data.y)
{
EatFood(ppM, Next);
(*ppM)->Score += (*ppM)->Add;
SetPos(60, 17);
printf("当前总得分:%02d", (*ppM)->Score);
free((*ppM)->pFood);
CreateFood(ppM);
}
else
{
NotEatFood(ppM, Next);
}
//判断加速减速
if (KEY_PRESS(0x51) && (*ppM)->status == OK && (*ppM)->SleepTime > 10)
{
SetPos(60, 16);
(*ppM)->Add += 2;
printf("当前食物分数:%02d", (*ppM)->Add);
(*ppM)->SleepTime -= 10;
}
if (KEY_PRESS(0x45) && (*ppM)->status == OK && (*ppM)->Add > 2)
{
SetPos(60, 16);
(*ppM)->Add -= 2;
printf("当前食物分数:%02d", (*ppM)->Add);
(*ppM)->SleepTime += 10;
}
Sleep((*ppM)->SleepTime);
}
void EatFood(Maintenance** ppM, SnakeNode** Next)
{
assert(ppM);
SnakeNode* cur = (*ppM)->ps;
//头插入Next
SetPos((*Next)->data.x, (*Next)->data.y);
wchar_t node = NODE;
wprintf(L"%lc", node);
(*Next)->next = (*ppM)->ps;
(*ppM)->ps = (*Next);
}
void NotEatFood(Maintenance** ppM, SnakeNode** Next)
{
assert(ppM && *ppM);
SnakeNode* cur = (*ppM)->ps;
//找尾
while (cur->next->next)
{
cur = cur->next;
}
SnakeNode* ptail = cur->next;
//找到尾,把尾的坐标处打印为空格
SetPos(ptail->data.x, ptail->data.y);
printf(" ");
free(ptail);
//倒数第二个成为新的尾
cur->next = NULL;
//Next成为新的头
SetPos((*Next)->data.x, (*Next)->data.y);
wchar_t node = NODE;
wprintf(L"%lc", node);
(*Next)->next = (*ppM)->ps;
(*ppM)->ps = (*Next);
}
bool KillBySelf(Maintenance** ppM, SnakeNode** Next)
{
SnakeNode* cur = (*ppM)->ps->next;
while (cur)
{
if (cur->data.x == (*Next)->data.x
&& cur->data.y == (*Next)->data.y)
return true;
cur = cur->next;
}
return false;
}
bool KillByWall(SnakeNode** Next)
{
SnakeNode* cur = (*Next);
if (cur->data.x == 0 || cur->data.x == ROW - 2
|| cur->data.y == 0 || cur->data.y == COL)
return true;
return false;
}
void Pause(Maintenance** ppM)
{
assert(ppM);
while (true)
{
Sleep(1);
if (KEY_PRESS(VK_SPACE) && (*ppM)->status == OK)
break;
if (KEY_PRESS(VK_ESCAPE))
{
(*ppM)->status = ESC;
break;
}
}
}
void GameEnd(Maintenance** ppM)
{
assert(ppM);
SnakeNode* cur = (*ppM)->ps;
while (cur)
{
SnakeNode* next = cur->next;
free(cur);
cur = next;
}
(*ppM)->ps = NULL;
(*ppM)->direction = LEFT;
(*ppM)->status = OK;
(*ppM)->pFood = NULL;
(*ppM)->Score = 0;
(*ppM)->Add = 20;
(*ppM)->SleepTime = 200;
}
测试test.c文件
#include"SList.h"
void test()
{
Maintenance* maintence = NULL;
char ch = 0;
do
{
//游戏开始前的准备工作
GameStart(&maintence);
//游戏开始
GameRun(&maintence);
//游戏结束
GameEnd(&maintence);
system("pause");
SetPos(5, COL / 2 + 1);
printf("再来一局?Y/N");
//读取字符是否为y或Y
ch = getchar();
getchar();
SetPos(0, COL + 1);
} while (ch == 'y' || ch == 'Y');
}
int main()
{
test();
return 0;
}