在我们学习完C语言 和单链表知识点后 我们开始写个贪吃蛇的代码
目标:使用C语言在Windows环境的控制台模拟实现经典小游戏贪吃蛇
贪吃蛇代码实现的基本功能:
地图的绘制
蛇、食物的创建
蛇的状态(正常 撞墙 撞到自己 正常退出)
蛇移动的方向(上 下 左 右)
蛇的加速 减速
游戏暂停
.......
该代码会运用到函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等
我们会在学习前 会引出一些必要的知识点
Win32 API介绍
控制台程序
控制台屏幕上的坐标COORD
给坐标赋值:
COORD pos = { 2, 10 };
GetStdHandle
HANDLE GetStdHandle(DWORD nStdHandle);
在我们贪吃蛇代码中 函数的参数是STD_OUTPUT_HANDLE 标准输出
实例:
HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
实例:
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息
CONSOLE_CURSOR_INFO
cursor_info.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息
//修改光标占比
cursor_info.dwSize = 50;
cursor_info.bVisible = false;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
SetConsoleCursorPosition
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//句柄houput获取标准输出的句柄 使得可以操作屏幕
//定位光标的位置
COORD pos = { 10,20 };
SetConsoleCursorPosition(houtput, pos);
SetPos:封装一个设置光标位置的函数 ----贪吃蛇地图 蛇 食物的绘制都需要移动光标
void SetPos(short x, short y)//封装一个函数SetPos 光标位置
{
HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
GetAsyncKeyState
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//这里的0x1也就是 1
Win32 API我们就讲到这里
除了Win32 API外 我们还有其他知识需要学习
我这里是用VS2022写的代码 这里还需要对运行窗口做些改变
贪吃蛇游戏设计与分析
地图的绘制
<locale.h>本地化
类项
setlocale函数
char* setlocale (int category, const char* locale);
setlocale(LC_ALL, " ");//切换到本地环境
宽字符的打印
1.绘制地图(CreatMap)
2.蛇身和食物(InitSnake) (CreateFood)
3.数据结构设计(蛇的结点 贪吃蛇的方向 状态 食物分数 总得分 加速 减速)
typedef struct Snakenode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct Snakenode* next;
}Snakenode,*pSnakeNode;//结构体指针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;
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
//正常 撞墙 撞到自己 正常退出
enum GAME_STATUS
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORMAL
};
4.游戏流程设计
5.核⼼逻辑实现分析
程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑
//Test.c
#include<locale.h>
#include"snake.h"
//完成的是游戏的测试逻辑
void test()
{
int ch = 0;
do
{//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1.打印环境页面
//2.功能介绍
//3.绘制地图
//4.创建蛇
//5.创建食物
//6.设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏--善后工作
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();//按下回车后 其实接收到的是\n 所以需要清理掉\n
//getchar();//清理掉\n
while (getchar() != '\n')
;
} while (ch=='Y' || ch == 'y');
SetPos(0, 26);//让代码结束的信息放在地图外
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");//中文 宽字符的打印
srand((unsigned int)time(NULL));
test();
return 0;
}
游戏开始(GameStart)
1.设置控制台窗口大小 名称 隐藏光标
void GameStart(pSnake ps)
{
//0.先设置窗口的大小 然后把光标隐藏
system("mode con cols=100 lines=30");//30行 100列
system("title 贪吃蛇");
HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info;
//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息
cursor_info.bVisible = false;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
//1.打印欢迎页面 //2.功能介绍
WelcomeToGame();
//3.绘制地图
CreatMap();
//4.创建蛇
InitSnake(ps);
//5.创建食物
CreateFood(ps);
}
2.欢迎页面及功能介绍(WelcomeToGame)
打印欢迎页面
在我们使用光标位置 之前 我们封装一个函数SetPos
void SetPos(short x, short y)//封装一个函数SetPos 光标位置
{
HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
void WelcomeToGame()
{
SetPos(45, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(46, 20);
system("pause");
system("cls");//清除控制台信息
SetPos(30, 14);
wprintf(L"用↑ ↓ ← → 来控制蛇的移动 按F3加速 F4减速\n");
SetPos(34, 15);
wprintf(L"加速能够得到更高的分数");
SetPos(46, 20);
system("pause");
system("cls");//紧接着下面应该是地图绘制
}
代码实现结果:
3.绘制地图(CreatMap)
绘制地图就是将墙体打印出来 因为这里是宽字符打印 所有打印使用wprintf函数 打印格式串前使用L 绘制地图的关键是算好光标的坐标 才能在想要的位置打印墙体
为墙体打印的宽字符写个宏:
#define WALL L'□'
这里的易错点也就是坐标的计算
代码如下:
void CreatMap()
{//27行 58列的棋盘
//上
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 <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
//getchar();
}
4.创建蛇(初始化蛇身)(InitSnake)
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()");
return;
}
cur->next = NULL;
cur->x =POS_X +i*2;//显示 + 未使用的表达式结果
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", BODY);
cur = cur->next;
}
//设置贪吃蛇的属性
ps->_dir = RIGHT;//默认向右
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//毫秒
ps->_status = OK;
/*getchar();*/为什么我的代码只打印一个●
}
#define BODY L'●'
5.创建食物(CreateFood)
先随机生成食物的坐标(rand和srand函数)
1.x坐标必须是2的倍数
2.食物的坐标不能和蛇身每个结点的坐标重复 不能越过墙体
创建食物结点 打印食物
食物打印的宽字符:
#define FOOD L'★'
CreateFood函数:
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//x = rand()%53+2;//x:2~54---0~52 //还要保证x是偶数
//y = rand()%25+1;//y:1~25---0~24
again:
do
{
x = rand() % 53 + 2;//x:2~54---0~52 //还要保证x是偶数
y = rand()%25+1;//y:1~25---0~24
} 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()");
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
游戏运行(GameRun)
1.游戏运⾏期间,右侧打印帮助信息(PrintHelpInfo)
void PrintHelpInfo()
{
SetPos(64, 10);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 11);
wprintf(L"%ls", L"用↑ ↓ ← → 来控制蛇的移动 \n");
SetPos(64, 12);
wprintf(L"%ls", L"按F3加速 F4减速");
SetPos(64, 13);
wprintf(L"%ls", L"按ESC退出游戏 空格暂停游戏");
SetPos(64, 14);
wprintf(L"%ls", L"made in LiFeiFei");
}
2.根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
游戏有两种状态:i.蛇撞墙而亡 ii.蛇撞到自己
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;//cur指向蛇身
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
3.如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
//获取按键情况 如果按键最小位为1 说明已按过
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
蛇身移动(SnakeMove)
蛇下一步移动方向会有几种情况:
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", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个结点
free(cur->next);
//将倒数第二个结点的地址域置为NULL
cur->next = NULL;
}
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
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", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
void pause()//暂停
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
在NoFood中 有一个易错点:在打印下一步的蛇身时 也就是将下一个结点当作蛇头 并将之前蛇身最后一个结点打印为空格 释放掉原本蛇身的最后一个结点 在释放掉最后一个结点后 还得将指向(原先蛇身的倒数第二个结点)最后一个结点的指针改为NULL 保证蛇尾打印可以正常结束 不会越界访问
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);
}
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印总分数 和食物的分值
SetPos(64, 8);
printf("总分数:%d\n", ps->_score);
SetPos(64, 9);
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))//ESC
{//正常退出游戏
ps->_status = END_NORMAL;//退出游戏
break;
}
else if (KEY_PRESS(VK_F3))//F3加速---休眠时间减少
{
if (ps->_sleep_time >= 80)//初始休眠时间为200-80=120 减4次
{//最小休眠时间为50
ps->_sleep_time -= 30;
ps->_food_weight += 2;//食物初始分值为10 ⼀个⻝物分数最⾼是20分
}
}
else if (KEY_PRESS(VK_F4))//F4减速
{
if (ps->_sleep_time < 320)//200 230 260 290 320
{ //10 8 6 4 2
ps->_sleep_time += 30;
ps->_food_weight -= 2;//⼀个⻝物分数最低是2分
}
}
SnakeMove(ps);//蛇走一步的过程
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
游戏结束
游戏状态不再是OK的时候 要告知游戏结束的原因 并且释放蛇身结点
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);
}
}
在这里我就不给予完整代码了 这篇文章已经快到1.3万字了 我会单独发一篇贪吃蛇代码