在我们学习完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();//清理掉\nwhile (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);//将倒数第二个结点的地址域置为NULLcur->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次{//最小休眠时间为50ps->_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 2ps->_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万字了 我会单独发一篇贪吃蛇代码