一、游戏说明
- 贪吃蛇地图绘制
- 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
二、地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
三、头文件
#include<stdio.h> #include<stdlib.h> #include<windows.h> #include<stdbool.h> #include<locale.h> #define Case break;case #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 ) //贪吃蛇,蛇身节点的定义 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode; //typedef struct SnakeNode* pSnakeNode; enum GAME_STATUS //游戏状态 { OK = 1,//正常运行 ESC,//按了ESC键退出,正常退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//撞到自身 }; //行走的方向 enum DIRECTION //方向 { UP = 1, DOWN, LEFT, RIGHT }; //贪吃蛇 typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头 pSnakeNode pFood;//指向食物的指针 int Score;//当前累积的分数 int FoodWeight;//一个食物的分数,默认每个食物10分 int SleepTime;//每走一步休眠时间? //蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢 enum GAME_STATUS status;//游戏当前的状态 enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右 //... }Snake,* pSnake; //typedef struct 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 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);
四、蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
五、数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行。
#define WALL L'□' 墙 #define BODY L'●' 蛇身 #define FOOD L'☆' 食物
蛇节点结构如下:
//贪吃蛇,蛇身节点的定义 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; //是一个指向下一个 SnakeNode 类型节点的指针,用于构建链表来表示蛇的身体。 }SnakeNode, * pSnakeNode; //typedef struct SnakeNode* pSnakeNode;
封装一个Snake的结构来维护整条贪吃蛇:
pSnakeNode pSnake:这是一个指向 SnakeNode 类型的指针,代表蛇的头部。通常,贪吃蛇的实现会用一个链表来表示蛇的身体,其中每个节点(SnakeNode)代表蛇身体的一部分,而 pSnake 指向这个链表的第一个节点,即蛇头。
pSnakeNode pFood:这是一个指向 SnakeNode 类型的指针,代表食物的位置。在贪吃蛇游戏中,食物会被随机放置在游戏区域内,当蛇吃到食物时,这个食物会被移除,并且蛇的身体会增长。
enum GAME_STATUS status;:这是一个枚举类型,表示游戏当前的状态。具体的枚举值没有在代码中给出,但可能包括“游戏中”、“游戏结束”等状态。
enum DIRECTION dir;:这是一个枚举类型,表示蛇当前移动的方向。具体的枚举值也没有在代码中给出,但通常包括“向上”、“向下”、“向左”、“向右”等方向。
typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头 pSnakeNode pFood;//指向食物的指针 int Score;//当前累积的分数 int FoodWeight;//一个食物的分数,默认每个食物10分 int SleepTime;//每走一步休眠时间? //蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢 enum GAME_STATUS status;//游戏当前的状态 enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右 //... }Snake,* pSnake; //typedef struct Snake* pSnake;
蛇的方向,可以一一列举,使用枚举:
//行走的方向 enum DIRECTION //方向 { UP = 1, DOWN, LEAF, RIGHT };
游戏状态,可以一一列举,使用枚举:
enum GAME_STATUS //游戏状态 { OK = 1,//正常运行 ESC,//按了ESC键退出,正常退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//撞到自身 };
六、Snake.c
5.1、游戏开始函数
void GameStart(pSnake ps) { //设置控制台的信息,窗口大小,窗口名 system("mode con cols=120 lines=40"); 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 SetPos(int x, int y) { //获得设备句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //根据句柄设置光标的位置 COORD pos = { x,y }; SetConsoleCursorPosition(handle, pos); }
欢迎来到贪吃蛇游戏
void welcomeToGame() { //欢迎信息 SetPos(35, 10); printf("欢迎来到贪吃蛇小游戏\n"); SetPos(38, 20); system("pause"); system("cls"); //功能介绍信息 SetPos(15, 10); printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速\n"); SetPos(15, 11); printf("加速能得到更高的分数"); SetPos(38, 20); system("pause"); system("cls"); }
创建一个地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
void CreateMap() { int i = 0; //上 SetPos(0, 0); for (i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //下 SetPos(0, 25); for (i = 0; i <= 56; i += 2) { 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); } }
初始化创建蛇身的节点
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。
结构体成员:记录它们的坐标:(x,y),和记录下一个位置的前驱结构体指针:next。
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("InsitSnkae():malloc()"); 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->dir = RIGHT; ps->FoodWeight = 10; ps->pFood = NULL; ps->Score = 0; ps->SleepTime = 200; ps->status = OK; }
创建第一个食物
- 先随机生成食物的坐标
- x坐标必须是2的倍数
- 食物的坐标不能和蛇身每个节点的坐标重复
- 创建食物节点,打印食物
void CreateFood(pSnake ps) { int x = 0;//x范围: 2~54 -> 0~52 + 2 -> rand()%53 + 2 int y = 0;//y范围: 1~25 -> 0~24 + 1 -> rand()%24 + 1 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 = malloc(sizeof(SnakeNode)); if (pFood == NULL) { perror("CreateFood:malloc()"); return; } pFood->x = x; pFood->y = y; ps->pFood = pFood; SetPos(x, y); wprintf(L"%lc", FOOD); }
5.2、游戏运行函数
游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
确定了蛇的方向和速度,蛇就可以移动了。
void GameRun(pSnake ps) { //打印帮助信息 PrintHelpInfo(); //检测按键 do { //当前的分数情况 SetPos(62, 10); printf("总分:%5d\n", ps->Score); SetPos(62, 11); printf("食物的分支:%02d\n", 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_LEFT) && ps->dir != RIGHT) { ps->dir = LEFT; } else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) { ps->dir = RIGHT; } 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); }
检测按键状态,我们封装了一个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
打印帮助信息
//打印帮助信息 void PrintHelpInfo() { SetPos(62, 15); printf("1. 不能穿墙. 不能咬到自己"); SetPos(62, 16); printf("2. 用 ↑ . ↓ . ← . → 来控制蛇的移动"); SetPos(62, 17); printf("3. F3是加速,F4是减速"); SetPos(62, 19); printf(" "); }
暂停函数
void pause() { while (1) { Sleep(100); if (KEY_PRESS(VK_SPACE)) { break; } } }
下一个是否是食物
//pSnakeNode psn 是下一个节点的地址 //pSnake ps 维护蛇的指针 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"%lc", BODY); cur = cur->next; } ps->Score += ps->FoodWeight; //释放旧的食物 free(ps->pFood); //新建食物 CreateFood(ps); }
如果下一步不是食物
将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,放弃掉蛇身的最后一个节点
//pSnakeNode psn 是下一个节点的地址 //pSnake 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"%lc", 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 == 25) { 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; } }
蛇移动的函数
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
void SnakeMove(pSnake ps) { pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext == NULL) { perror("SnakeMove():malloc()"); return; } pNext->next = NULL; switch (ps->dir) { case UP: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y - 1; break; Case DOWN: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y + 1; Case LEFT: pNext->x = ps->pSnake->x - 2; pNext->y = ps->pSnake->y; Case RIGHT: pNext->x = ps->pSnake->x + 2; pNext->y = ps->pSnake->y; break; } //下一个坐标是否是食物 if (NextIsFood(ps, pNext)) { //是食物就吃掉 EatFood(ps, pNext); } else { //不是食物就正常一步 NotEatFood(ps, pNext); } //检测撞墙 KillByWall(ps); //检测是否撞自己 KillBySelf(ps); }
七、游戏结束的资源释放
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
//游戏结束的资源释放 void GameEnd(pSnake ps) { SetPos(20, 15); switch (ps->status) { case ESC: printf("主动退出游戏,正常退出\n"); Case KILL_BY_WALL: printf("很遗憾,撞墙了,游戏结束\n"); Case KILL_BY_SELF: printf("很遗憾,撞到自己了,游戏结束\n"); break; } //释放贪吃蛇的链表资源 pSnakeNode cur = ps->pSnake; pSnakeNode del = NULL; while (cur) { del = cur; cur = cur->next; free(del); } free(ps->pFood); ps = NULL; }
八、Test.c
void test() { //创建贪食蛇 Snake snake = { 0 }; //GameStart(&snake);//游戏开始前的初始化 //GameRun();//玩游戏的过程 //GameEnd();//善后的工作 int ch = 0; do { Snake snake = { 0 }; GameStart(&snake);//游戏开始前的初始化 GameRun(&snake);//玩游戏的过程 GameEnd(&snake);//善后的工作 SetPos(15, 20); printf("再来一局吗?(Y/N):"); ch = getchar(); getchar();// 清理\n } while (ch == 'Y' || ch == 'y'); } int main() { //修改适配本地的环境 setlocale(LC_ALL, ""); test();//贪吃蛇游戏的测试 SetPos(0, 30); return 0; }
今天就先到这了!!!
看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。