正式学习c语言也几个月的时间了,前面学习了很多内容,循环,指针,数组,函数,链表等等(不过欠了好多篇博客还没有完成哈哈哈哈哈),今天发布的贪吃蛇是将前面许多内容综合运用的项目,很迫不及待地想和大家分享我实现贪吃蛇的过程和遇到的困难,也希望能对你有所帮助吧,那我们开始一起实现了哦。
贪吃蛇的实现目标
实现的功能
1.贪吃蛇的地图绘制
2.蛇吃食物的功能
3.蛇撞墙,撞到自己死亡
4.蛇运动的速度,分数
4.游戏的暂停,退出,继续
需要运用的主要知识
c语言的函数,指针,枚举,结构体,动态内存管理,预处理指令,链表,win32API
win32 API的介绍(在windows.h头文件下使用这些函数)
Win32API
他是一个服务中心(一个服务就是一个函数),可以帮助应用系统达到打开视图,描绘图形,使用周边设备等目的。
控制台程序
我们平常运行起来的黑框结构(可以自己改变颜色)就是控制台程序。
对于控制台,我们可以通过指令设置它的行列
mode con cols(列)=100 lines(行)=30
也可以通过命令设置控制台的名字
title 贪吃蛇
这两个对于我们执行贪吃蛇的代码齐了前期的准备工作
那么在c语言中只需要引用system函数便可以实现上面的效果
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
getchar();
return 0;
}
控制台屏幕上的坐标COORD
COORD是Windows API中定义的一个结构体,表示一个字符在屏幕上的位置:
typedef struct _COORD
{
SHORT X;
SHORT Y;
}COORD,*PCOORD;
给坐标赋值:
COORD pos={10,15};
GetStdHandle
1.作用:
它用于从一个特定的标准设备(标准输入,标准输出或标准错误)中获得一个手柄,可以用来操作设备。
HANDLE GetStdHandle(DWORD nStdHandle)
其中,括号中的内容只有三种可以填入:
STD_INPUT_HANDLE 标准输入设备。
STD_OUTPUT_HANDLE 标准输出设备。
STD_ERROR_HANDLE 标准错误设备。
我们要使用的是标准输出的手柄,所以用STD_OUTPUT_HANDLE。
2.实例:
HANDLE houtput=NULL:
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
1.功能:
检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。
BOOL WINAPI GetConsoleCursorInfo
(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
2.参数介绍:
hConsoleOutput——是控制台屏幕缓冲区的句柄;
lpConsoleCursorInfo——是指向 CONSOLE_CURSOR_INFO 结构指针,接受相关控制台的游标信息。
3.实例:
HANDLE houtput=NULL:
//获取标准输出的句柄
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput,&CursorInfo);//获取控制台光标信息
CONSOLE_CURSOR_INFO 结构
1.功能:
包含相关控制台游标的信息。
2.语法:
typedef struct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
3.结构体成员介绍:
dwSize —— 由游标填充的字符单元的百分比。 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。
bVisible —— 游标的可见性。 如果游标可见,则此成员为 TRUE;如果游标不可见,则此成员为FALSE;
CursorInfo.bVisible=false;//光标不可见
SetConsoleCursorInfo
1.功能:
设置指定的控制台屏幕缓冲区的光标的大小和可见性。
2.语法:
BOOL WINAPI SetConsoleCursorInfo
(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
3.参数介绍:
hConsoleOutput —— 是控制台屏幕缓冲区的句柄;
lpConsoleCursorInfo —— 指向 CONSOLE_CURSOR_INFO 结构的指针,该结构为控制台屏幕缓冲区的光标提供新的规范。
4.实例:
HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput,&CursorInfo);
CursorInfo.bVisible=false;
SetConsoleCursorInfo(houtput,&CursorInfo);
SetConsoleCursorPosition
1.功能:
设置指定控制台屏幕缓冲区中的光标位置。
2.语法
BOOL WINAPI SetConsoleCursorPosition
(
HANDLE hConsoleOutput,
COORD pos
);
我们可以将我们想设置的光标位置放在COORD类型的pos中,那么我们在使用这个函数时,光标位置则在我们设置的pos位置上。
3.实例
HANDLE houtput=NULL;
COORD pos={15,10};
//获取标准输出的手柄
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorInfoPosition(houtput,pos);
SetPos:封装一个设置光标位置的函数
void SetPos(int x,int y)
{
COORD pos={x,y};
HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorInfoPosition(houtput,pos);
}
GetAsyncKeyState
1.功能:
获取按键信息。
2.语法:
SHORT GetAsyncKeyState
(
int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们想要判定一个键是否被按过,则可以检测GetAsyncKeyState返回值的最低位是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
游戏设计与分析
地图
1.宽字符:
在贪吃蛇游戏中,我们使用的墙体,蛇身,还有食物都是宽字符,而宽字符是占用两个字节的,普通的字符只占用一个字节。
在c语言中,宽字符的类型是wchar_t,输入函数为wscanf(),输出函数位wprintf()。
后面还加入了locale.h,其中提供了允许程序员根据特定的地区调整程序行为的函数。
2.locale.h的本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。
在标准可以中,依赖地区的部分有以下几项:数字量的格式,货币量的格式,字符集,日期和时间的表示方式。
3.类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定⼀个类项:
• LC_COLLATE
• LC_CTYPE
• LC_MONETARY
• LC_NUMERIC
• LC_TIME
• LC_ALL 针对所有类项修改
4.setlocale函数
char * setlocale ( int category , const char * locale);
第一个参数可以是前面类项中的任何一个;
第二个参数c语言只定义了两个可能取值:" “和"C”。(c语言默认为第二种)
setlocale(LC_ALL,"");//切换为本地模式
5.宽字符的打印
重点关注几个地方的前面要加L。
int main()
{
setlocale(LC_ALL,"");
wchar_t ch1=L'●';
wprintf(L"%lc\n",ch1);
}
蛇身与食物
蛇身
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,比如(24,15)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。
食物
在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,不能出墙外,然后打印★。
数据结构设计
蛇的每一节其实就是链表的每一个节点
1.蛇节点结构如下:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode*next;
}SnakeNode,*pSnakeNode;
2.整个贪吃蛇的结构:
enum Direction
{
UP=1,
DOWN,
LEFT,
RIGHT
};
enum Game_Status
{
OK,
kILL_BY_WALL,
KILL_BY_SELF,
END_NOMAL
};
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇身的指针
pSnakeNode pFood;//维护食物的指针
int Socre;//当前获得的分数
int Foodweight;//每个食物的分数
int Sleeptime;//每走一步的休眠时间
enum Direction Dir;//蛇身的方向,默认向右
enum Game_Status Sta;//游戏状态
};
游戏核心部分的实现
游戏主逻辑
#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();
getchar();
} while (ch == 'Y' || ch == 'y');
}
int main()
{
setlocale(LC_ALL, "");
test();
SetPos(0, 28);
}
游戏运行代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
void WelcomeToGame()
{
//打印欢迎界面
SetPos(40, 10);
printf("欢迎来到贪吃蛇游戏\n");
SetPos(41, 15);
system("pause");
system("cls");
//打印功能界面
SetPos(35, 10);
printf("用↑ . ↓ . ← . →分别控制蛇的移动\n");
SetPos(33, 11);
printf("F3为加速,F4为减速,加速能获得更高的分数\n");
SetPos(41, 15);
system("pause");
system("cls");
}
void CreateMap()
{
SetPos(0,0);
for (int i = 0; i < 57; i = i + 2)
{
wprintf(L"%c", WALL);
}
//下
SetPos(0, 26);
for (int i = 0; i < 57; i = i + 2)
{
wprintf(L"%c", WALL);
}
//左
for (int i = 0; i <= 26; i++)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
//右
for (int i = 0; i <= 26; i++)
{
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
//创造蛇的节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc fail");
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"%c", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->Sleeptime = 200;
ps->Socre = 0;
ps->Foodweight = 10;
ps->Sta = OK;
ps->Dir = RIGHT;
}
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
srand((unsigned int)time(NULL));
again:
do
{
x = rand() % 53 + 2;
y = rand() % 24 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->pSnake;
//食物不能和蛇身重合
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
//创造食物
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood():malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%c", FOOD);
ps->pFood = pFood;
}
}
void GameStart(pSnake ps)
{
//设置游戏窗口的大小和名字
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);
//打印欢迎界面
WelcomeToGame();
//创建地图
CreateMap();
初始化蛇身
InitSnake(ps);
//创造食物
CreateFood(ps);
}
void PrintHelpInfo()
{
SetPos(63, 15);
printf("不能穿墙,不能咬自己\n");
SetPos(63, 16);
printf("用↑ . ↓ . ← . →分别控制蛇的移动\n");
SetPos(63, 17);
printf("F3为加速,F4为减速\n");
SetPos(63, 18);
printf("ESC:退出游戏 space:暂停游戏");
}
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
if ((ps->pFood->x == pNext->x) && (ps->pFood->y == pNext->y))
{
return 1;
}
else
{
return 0;
}
}
void EatFood(pSnake ps, pSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
pSnakeNode cur = ps->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
ps->Socre += ps->Foodweight;
free(ps->pFood);
CreateFood(ps);
}
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
//头插法
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//释放尾结点
pSnakeNode cur = ps->pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
//将尾节点的位置打印成空白字符
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->Sta = 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->Sta = KILL_BY_SELF;
}
cur = cur->next;
}
}
void SnakeMove(pSnake ps)
{
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove():malloc fail");
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(ps, pNextNode))
{
EatFood(ps, pNextNode);
}
else
{
NotEatFood(ps, pNextNode);
}
KillByWall(ps);
KillBySelf(ps);
}
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
SetPos(63, 10);
printf("当前分数:%5d", ps->Socre);
SetPos(63, 11);
printf("每个食物的分数:%2d", ps->Foodweight);
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->Sta = ESC;
}
else if (KEY_PRESS(VK_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->Sleeptime >= 100)
{
ps->Sleeptime -= 10;
ps->Foodweight += 2;
}
}
else if(KEY_PRESS(VK_F4))
{
ps->Sleeptime += 10;
if (ps->Sleeptime <250)
{
ps->Foodweight -= 2;
}
else
{
ps->Foodweight = 2;
}
}
Sleep(ps->Sleeptime);
SnakeMove(ps);
} while (ps->Sta == OK);
}
void GameEnd(pSnake ps)
{
SetPos(15, 12);
switch (ps->Sta)
{
case ESC:
printf("游戏正在退出中...\n");
break;
case KILL_BY_SELF:
printf("你撞到了自己,游戏结束!\n");
break;
case kILL_BY_WALL:
printf("你撞到了墙,游戏结束!\n");
break;
}
pSnakeNode cur = ps->pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
free(ps->pFood);
ps->pFood = NULL;
}
头文件
#pragma once
#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
enum Direction
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum Game_Status
{
OK,
ESC,
kILL_BY_WALL,
KILL_BY_SELF
};
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇身的指针
pSnakeNode pFood;//维护食物的指针
int Socre;//当前获得的分数
int Foodweight;//每个食物的分数
int Sleeptime;//每走一步的休眠时间
enum Direction Dir;//蛇身的方向,默认向右
enum Game_Status Sta;//游戏状态
}Snake, * pSnake;
//定位控制台光标位置
void SetPos(int x, int y);
//游戏开始前的准备
void GameStart(pSnake ps);
//打印欢迎界面
void WelcomeToGame();
//绘制地图
void CreateMap();
//初始化贪吃蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
void pause();
//蛇移动的函数- 每次走一步
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);
//游戏结束的资源释放
void GameEnd(pSnake ps);