本文章使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
1.贪吃蛇地图绘制。
2.蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
3.蛇撞墙死亡
4.蛇咬到自己死亡
5.计算得分
6.蛇加速、减速
7.暂停游戏
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API……
使用cmd命令来设置控制台窗口的长宽:
mode con clos=100 lines=30
title 贪吃蛇
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行
代码展示:
int main()
{
system("mode con clos=100 lines=30");
system("title 贪吃蛇");
return 0;
}
COORD是WindowsAPI中定义的一个结构体,表示光标在控制台屏幕上的坐标
typedef struct COORD
{
SHORT X;
SHORT Y;
}COORD,*PCOORD;
给坐标赋值
COORD pos={10,15};
利用好这个就可以实现将想在控制台屏幕上打印的字符出现在预先设想好的位置
GetStdHandle是一个WindowsAPI函数,它用于从一个特定的标准设备(标准输入[STD_INPUT_HANDLE]、标准输出[STD_OUTPUT_HANDLE]或标出错误[STD_ERROR_HANDLE])中获取一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备
HANDLE GetStdHandle(DWORD,nStdHandle);
运用示例 :
HANDLE handle =GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorinfo
{
HANDLE;
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
} ;
这个结构体包含有关控制台光标的信息
typedef strcut _CONSOLE_CURSOR_INFO{
DWORD dwSize;
BOOL bVisible;
}CONSOLE_CURSOR_INFO,*PCONSOLE_CURSOR_INFO;
dwSize 由光标填充的字符单元格的百分比。此值介于1到100之间 。光标外观会发生变化,范围从完全填充单元格到单元格底部的水平线。
bVisible 游标的可见性。如果为true则光标可见。
CursorInfo.bVisible=false;
设置指定控制台屏幕缓冲区的光标大小和可见性
BOOL WINAPI SetConsoleCursorInfo {
HANDLE hConsoleOutput;
const CONSOLE_CURSOR_INFO*lpConsoleCursorInfo
};
了解上面的几点后,我们需要将几点进行整合,实现光标的隐藏。
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定位置。
BOOL WINAPI SetConsoleCursorPosition{
HANDLE HConsoleOutput;
COORD POS;
}
运用这些我们可以封装成一个函数SetPos(int x,int y)将光标位置调置到(x,y)
void SetPos(int x,int y)
{
//获得设备句柄
HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);
//根据句柄设置光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(hanlde, pos);}
注意:控制台的坐标从左到右依次递增,从上到下依次递增
获取按键情况
SHORT GetAsyncKeyState{
int vKey
} ;
GetAsyncKeyState的返回值是short类型,调用GetAsyncKeyState后如果返回的16为数据中最高位是1,说明该键是按下状态,如果是0,则是抬起状态。如果最低位是1则说明,该键被按过,否则为0。
所以判断一个键是否被按过我们可以判断GetAsyncKeyState函数返回值最低位是否为1.
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇的指针(头指针)
pSnakeNode pFood;//食物的制作
int score;//累积得分
int Foodweight;//食物的分数
int sleeptime;//蛇休眠的时间
enum GAME_STATUS status;//游戏当前的状态
enum DIRECTION dir;//蛇当前走的方向
}Snake,*pSnake;
enum GAME_ATATUS
{
OK = 1,
ESC,
KILL_BY_WALL,
KILL_BY_SELF
};
enum DIRECTION
{
UP=1,
DOWN,
RIGHT,
LEFT
};
实现效果:
void WelcomeToGame()
{
//欢迎信息
SetPos(40, 12);//定好相应位置进行信息打印
printf("欢迎来到贪吃蛇小游戏");
SetPos(42, 24);
system("pause");
system("cls");//清除屏幕信息
//功能介绍信息
SetPos(35, 12);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(35, 13);
printf("加速能获得更高分");
SetPos(42, 24);
system("pause");
system("cls");
}
效果实现:
这里运用到了宽字符墙体用□,蛇身用●,食物用★(宽字符占用两个字节)
#define Wall L'□'
#define BODY L'●'
#define FOOD L'★'
void CreateMap()
{
SetPos(0, 0);
int i = 0;
//上
for (i = 0; i <= 56; i += 2)//宽字符占用两个字节
{
wprintf(L"%lc", Wall);//宽字符打印用到wprintf函数
}
SetPos(0, 26);
//下
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", Wall);
}
//左
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", Wall);
}
//右
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", Wall);
}
}
对蛇的相关信息进行初始化,首先我们先默认蛇的开始长度为5个节点。创建五个蛇节点并将其打印出来,然后再对剩余数据进行初始化。
设置设节点开始坐标(POS_X,POS_Y)
#define POS_X 24
#define POS_Y 5
void InitSnake(pSnake ps)
{
//创建五个蛇身节点
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc fail!");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;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->Foodweight = 10;//每一个食物的初始分数
ps->score = 0;//当前的得分
ps->sleeptime = 200;//蛇每走一步休眠的时间
ps->status = OK;//状态设置为正常
ps->dir = RIGHT;//默认蛇开始的移动的方向是右
}
创建食物节点时我们需要注意两个点
1.食物必须在地图内且x坐标是2的倍数(由于蛇节点时宽字符打印占两个字节且从偶数开始)
2.食物不能与蛇身重合
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 24 + 1;} while (x % 2 != 0);
//判断食物节点位置是否在蛇身上
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 fail!");
return;
}pFood->x = x;
pFood->y = y;ps->pFood = pFood;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
void PrintHelpInfo()
{
SetPos(62, 15);
printf("1。不能穿墙 不能咬到自己");
SetPos(62, 16);
printf("2.用 ↑.↓.←.→ 来控制蛇的移动");
SetPos(62, 17);
printf("3.F3是加速,F4是减速");SetPos(62,19);
printf("版权@M--Y");
}
SetPos(62, 10);
printf("累积总分:%5d", ps->score);
SetPos(62, 11);
printf("每个食物得分:%02d", ps->Foodweight);
这里需要运用到上面提到的GetAsyncKeyState()函数判断按键情况和虚拟键值(虚拟键盘对照表(KEY 按键) – 源码巴士)
//显然当蛇准备向某个方向移动时,原运动方向肯定不是与准备方向相反的
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_RIGHT)&&ps->dir!=LEFT)
{
ps->dir = RIGHT;
}
else if (KEY_PRESS(VK_LEFT)&&ps->dir!=RIGHT)
{
ps->dir = LEFT;
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
//暂停游戏
pause();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->sleeptime > 80)
{
ps->sleeptime -= 30;
ps->Foodweight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->Foodweight > 2)
{
ps->sleeptime += 30;
ps->Foodweight -= 2;
}
}
上面紫色标注出的暂停函数实现:
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
break;
}
}
根据按键方向来确定蛇头下一节点的坐标(cur是新增节点)
switch (ps->dir)
{
case UP:
cur->x = ps->pSnake->x;
cur->y = ps->pSnake->y - 1;
break;
case DOWN:
cur->x = ps->pSnake->x;
cur->y = ps->pSnake->y + 1;
break;
case RIGHT:
cur->x = ps->pSnake->x+2;
cur->y = ps->pSnake->y ;
break;
case LEFT:
cur->x = ps->pSnake->x-2;
cur->y = ps->pSnake->y ;
break;
}
但是这就里就存在一个问题了,需要考虑下一个节点是不是食物。如果是食物就将这个节点加入到蛇身中,如果不是就将蛇往这个方向移动一个节点并将尾节点除去。
判断语句(if (ps->pFood->x == cur->x && ps->pFood->y == cur->y))
void EATFOOD(pSnake ps,pSnakeNode cur)
{//头插
cur->next = ps->pSnake;
ps->pSnake = cur;pSnakeNode p = ps->pSnake;
while (p)//打印蛇身
{
SetPos(p->x, p->y);
wprintf(L"%lc", BODY);
p = p->next;
}
ps->score += ps->Foodweight;free(ps->pFood);//释放旧食物的指针
CreateFood(ps);//创建新食物
}
void NOTEATFOOD(pSnake ps, pSnakeNode cur)
{
cur->next = ps->pSnake;
ps->pSnake = cur;pSnakeNode p = ps->pSnake;
while (p->next->next)
{
SetPos(p->x, p->y);
wprintf(L"%lc", BODY);
p = p ->next;
}
SetPos(p->next->x, p->next->y);
printf(" ");//将原蛇身尾节点打印成空字符free(p->next);
p->next = NULL;}
判断条件处为p->next->next的解释:
蛇移动好后要判断是否有撞到墙,或是咬到自己
void KILLByWall(pSnake ps);//检查是否有撞墙
void KILLBySelf(pSnake 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;
return;
}
cur = cur->next;
}
}
判断是什么原因结束的游戏并打印出来
SetPos(20, 12);
switch (ps->status)
{
case ESC:
printf("主动正常退出 ");
break;
case KILL_BY_SELF:
printf("很遗憾 你咬到自己了,游戏结束");
break;
case KILL_BY_WALL:
printf("很遗憾 你撞墙了,游戏结束");
break;
}
最后进行资源释放,将创建的指针进行释放处理。
SnakeNode cur = ps->pSnake;
while (cur)
{
pSnakeNode next = cur->next;
free(cur);
cur = next;
}free(ps->pFood);
ps->pFood = NULL;
在主函数中进行do while循环使游戏可以重复多次进行、
int ch = 0;
do
{
Snake snake = { 0 };
srand((unsigned int)time(NULL));GameStart(&snake);//游戏前准备工作
GameRun(&snake);//游戏过程
GameEnd(&snake);//游戏善后工作
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'y' || ch == 'Y');
#pragma once
#define Wall L'□'
#define BODY L'●'
#define FOOD L'★'#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
#include
#include
#include
#include
#include
#include//游戏当前状态
enum GAME_ATATUS
{
OK = 1,
ESC,
KILL_BY_WALL,
KILL_BY_SELF
};//蛇走的方向
enum DIRECTION
{
UP=1,
DOWN,
RIGHT,
LEFT
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇的指针(头指针)
pSnakeNode pFood;//食物的制作
int score;//累积得分
int Foodweight;//食物的分数
int sleeptime;//蛇休眠的时间
enum GAME_STATUS status;//游戏当前的状态
enum DIRECTION dir;//蛇当前走的方向
}Snake,*pSnake;//定位
void SetPos(int x, int y);//打印欢迎信息
void WelcomeToGame();//绘制地图
void CreateMao();//初始化蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏前
void GameStart(pSnake ps);//打印帮助信息
void PrintHelpInfo();//如果下一节点是食物就吃掉
void EATFOOD(pSnake ps,pSnakeNode cur);
//不是
void NOTEATFOOD(pSnake ps, pSnakeNode cur);//判断是否撞墙了
void KILLByWall(pSnake ps);//检查是否咬到就自己
void KILLBySelf(ps);//走一步
void SnakeMove(pSnake ps);//游戏过程
void GameRun(pSnake ps);//游戏善后工作
void GameEnd(pSnake ps);
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"void SetPos(int x, int y)
{
//获得设备句柄
HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);
//根据句柄设置光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(hanlde, pos);
}void WelcomeToGame()
{
//欢迎信息
SetPos(40, 12);
printf("欢迎来到贪吃蛇小游戏");
SetPos(42, 24);
system("pause");
system("cls");//功能介绍信息
SetPos(35, 12);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(35, 13);
printf("加速能获得更高分");
SetPos(42, 24);
system("pause");
system("cls");
}void CreateMap()
{
SetPos(0, 0);
int i = 0;
//上
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", Wall);
}
SetPos(0, 26);
//下
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", Wall);
}
//左
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", Wall);
}
//右
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", Wall);
}
}void InitSnake(pSnake ps)
{
//创建五个蛇身节点
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc fail!");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;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->Foodweight = 10;
ps->score = 0;
ps->sleeptime = 200;
ps->status = OK;
ps->dir = RIGHT;
}//创建食物
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 24 + 1;} while (x % 2 != 0);
//判断食物节点位置是否在蛇身上
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 fail!");
return;
}pFood->x = x;
pFood->y = y;ps->pFood = pFood;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}void GameStart(pSnake ps)
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//打印欢迎信息
WelcomeToGame();//绘制地图
CreateMap();//初始化蛇
InitSnake(ps);//创建食物
CreateFood(ps);
}//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 15);
printf("1。不能穿墙 不能咬到自己");
SetPos(62, 16);
printf("2.用 ↑.↓.←.→ 来控制蛇的移动");
SetPos(62, 17);
printf("3.F3是加速,F4是减速");SetPos(62,19);
printf("版权@M--Y");
}void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
break;
}
}void EATFOOD(pSnake ps,pSnakeNode cur)
{
cur->next = ps->pSnake;
ps->pSnake = cur;pSnakeNode p = ps->pSnake;
while (p)//打印蛇身
{
SetPos(p->x, p->y);
wprintf(L"%lc", BODY);
p = p->next;
}
ps->score += ps->Foodweight;free(ps->pFood);//释放旧食物的指针
CreateFood(ps);//创建新食物
}void NOTEATFOOD(pSnake ps, pSnakeNode cur)
{
cur->next = ps->pSnake;
ps->pSnake = cur;pSnakeNode p = ps->pSnake;
while (p->next->next)
{
SetPos(p->x, p->y);
wprintf(L"%lc", BODY);
p = p ->next;
}
SetPos(p->next->x, p->next->y);
printf(" ");free(p->next);
p->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;
return;
}
cur = cur->next;
}
}
void SnakeMove(pSnake ps)
{
pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("SaveMove()::malloc fail!");
return;
}
cur->next = NULL;switch (ps->dir)
{
case UP:
cur->x = ps->pSnake->x;
cur->y = ps->pSnake->y - 1;
break;
case DOWN:
cur->x = ps->pSnake->x;
cur->y = ps->pSnake->y + 1;
break;
case RIGHT:
cur->x = ps->pSnake->x+2;
cur->y = ps->pSnake->y ;
break;
case LEFT:
cur->x = ps->pSnake->x-2;
cur->y = ps->pSnake->y ;
break;
}if (ps->pFood->x == cur->x && ps->pFood->y == cur->y)
EATFOOD(ps,cur);//下个节点是食物就吃掉食物
else
NOTEATFOOD(ps,cur);KILLByWall(ps);//检查是否有撞墙
KILLBySelf(ps);//检查是否咬到自己
}
//游戏过程
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();do
{
//当前的分数情况
SetPos(62, 10);
printf("累积总分:%5d", ps->score);
SetPos(62, 11);
printf("每个食物得分:%02d", ps->Foodweight);
//检测按键
//上、下、左、右、ESC、空格、F3、F4
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_RIGHT)&&ps->dir!=LEFT)
{
ps->dir = RIGHT;
}
else if (KEY_PRESS(VK_LEFT)&&ps->dir!=RIGHT)
{
ps->dir = LEFT;
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
//暂停游戏
pause();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->sleeptime > 80)
{
ps->sleeptime -= 30;
ps->Foodweight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->Foodweight > 2)
{
ps->sleeptime += 30;
ps->Foodweight -= 2;
}
}
//走一步
SnakeMove(ps);//睡眠一下
Sleep(ps->sleeptime);} while (ps->status == OK);
}//游戏后工作
void GameEnd(pSnake ps)
{
SetPos(20, 12);
switch (ps->status)
{
case ESC:
printf("主动正常退出 ");
break;
case KILL_BY_SELF:
printf("很遗憾 你咬到自己了,游戏结束");
break;
case KILL_BY_WALL:
printf("很遗憾 你撞墙了,游戏结束");
break;
}pSnakeNode cur = ps->pSnake;
while (cur)
{
pSnakeNode next = cur->next;
free(cur);
cur = next;
}free(ps->pFood);
ps->pFood = NULL;
}
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"
void test()
{
int ch = 0;
do
{
Snake snake = { 0 };
srand((unsigned int)time(NULL));GameStart(&snake);//游戏前准备工作
GameRun(&snake);//游戏过程
GameEnd(&snake);//游戏善后工作
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'y' || ch == 'Y');
}int main()
{
//修改适配本地中文环境
setlocale(LC_ALL, "");
test();
SetPos(0, 27);return 0;
}