贪吃蛇小游戏简单制作-C语言
文章目录
- 游戏背景介绍
- 实现目标
- 适合人群
- 所需技术
- 浅玩Window API
- 什么是API
- 控制台程序
- 窗口大小,名称设置
- Handle(句柄)
- 获取句柄
- 坐标结构体
- 设置光标位置
- 光标属性
- 获取光标属性
- 设置光标属性
- 按键信息获取
- 贪吃蛇游戏设计
- 游戏前的初始化
- 设置窗口的大小和名称
- 本地化设置
- 宽字符
- Waht is 宽字符
- 宽字符的打印
- 光标的设置
- 欢迎界面打印
- 地图绘制
- 帮助信息
- 蛇
- 蛇身的管理
- 贪吃蛇游戏的管理(插播,重要)
- 蛇的初始化
- 食物的管理
- 蛇的移动
- 游戏结束
- 拓展建议
- 寄语
- 源码
游戏背景介绍
贪吃蛇是一个非常经典的小游戏,笔者曾今在古早的案件手机,mp4上面玩过这款游戏,今天就让我们使用C语言一起复刻这个简单的小游戏吧~,好玩简单-
实现目标
在这个游戏中,我们需要控制一条可以上下左右移动的小蛇,在指定的墙体内进行移动,吃到食物后,蛇的身体会变长,当蛇撞墙或者撞到自己的身体的时候,游戏结束,我们需要实现的功能有:
- 蛇的移动
- 食物的生成
- 蛇的身体的增长
- 游戏结束的判断
- 游戏结束后的处理
适合人群
这是我学习完C语言语法和顺序表,链表之后的一个小项目,也同样适合像我一样刚学完C语言的同学,通过这个项目,巩固一下C语言的基础知识,也可以学习一下C语言的一些高级知识,比如Windows API的使用,宽字符的打印等等
所需技术
- C语言基础
- 链表
- 简单的Windows API
- 动态内存分配
- 等
浅玩Window API
什么是API
API全称Application Programming Interface,翻译过来就是应用程序接口,是一组预先定义的函数,类,协议的集合,这些函数,类,协议可以被其他程序调用,用来实现一些功能,比如Windows API就是用来实现Windows系统的一些功能的
上面说的有些复杂,我们可以把API想象成一个工厂,我们只需要知道向工厂输入什么,工厂会输出什么,而不需要知道工厂内部是怎么实现的
比如你向一个面包工厂输入了一些小麦,工厂会给你输出一些面包,而工厂内部可能是用了一些机器,工人等等来实现的,但是你不需要知道这些,你只需要知道你给他小麦,他给你面包就行了
控制台程序
控制台程序其实就是我们平时编译完文件之后运行打开的那个黑框框,我们可以在这个黑框框里面输入一些命令,然后程序会给我们输出一些结果,这个黑框框就是一个控制台,我们可以通过控制台程序来和用户进行交互
窗口大小,名称设置
既然是贪吃蛇小游戏,那么我们需要一个固定的窗口和一个有趣的名字,我们可以怎么做呢?
可以使用cmd命令在控制台程序中设置窗口的大小,名称
mode con cols=100 lines=30
title 贪吃蛇
- mode con cols=100 lines=30 设置窗口的大小为100列,30行
- title 贪吃蛇 设置窗口的名称为贪吃蛇
但是,我们只希望贪吃蛇程序运行后自动设置窗口的大小和名称,而不是让用户手动输入这些命令,因此我们可以使用system函数来调用这些命令,让程序自动设置窗口的大小和名称
#include <stdlib.h>
int main()
{system("mode con cols=100 lines=30");system("title 贪吃蛇");return 0;
}
Handle(句柄)
如果我们想要对控制台的一些属性进行设置,比如设置光标的位置,设置控制台的颜色等等,我们就需要使用句柄来进行操作,我们可以把句柄想象成是控制台大哥的身份标识,只有知道大哥的身份标识才能找到大哥并更改他的一些属性
获取句柄
我们可以使用GetStdHandle函数来获取控制台的句柄,这个函数的原型是
HANDLE GetStdHandle(DWORD nStdHandle);
我们可以使用一个HANDLE类型变量来接受返回值
坐标结构体
控制台上面的每一个字符都有一个坐标,
坐标是从左上角开始计算的,左上角的坐标是(0,0),向右是x轴正方向,向下是y轴正方向
我们可以使用一个结构体来表示这个坐标
COORD是Windows API中的一个结构体,定义如下
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;
设置光标位置
我们平时在使用printf打印字符的时候,每打印一个字符,光标都会自动向后移动一个位置,我们可以使用SetConsoleCursorPosition函数来手动设置光标的位置
BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD dwCursorPosition
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄COORD pos = {10, 10}; // 设置坐标SetConsoleCursorPosition(hOut, pos); // 设置光标位置printf("hello world");return 0;
}
光标属性
我们平时运行控制台程序的时候,会发现光标是一个闪烁的小方块,那我们有没有什么办法可以让小方块变大变小或是直接隐藏呢?
答案肯定是有哒
我们先来介绍一下光标的两个属性
- bVisible : 是否可见
- dwSize : 光标的大小 当这个值是100的时候,光标是一个小方块█,这个值也就是显示这个方块的百分比,比如50就是显示一半的方块
获取光标属性
我们可以使用GetConsoleCursorInfo函数来获取光标的属性
BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄CONSOLE_CURSOR_INFO cursorInfo;GetConsoleCursorInfo(hOut, &cursorInfo;);printf("bVisible:%d, dwSize:%d\n", cursorInfo.bVisible, cursorInfo.dwSize);return 0;
}
大家可以自行运行试一试
设置光标属性
在贪吃蛇游戏中,我们肯定希望没有光标的出现,因此我们可以使用SetConsoleCursorInfo函数来设置光标的属性
BOOL SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
eg:
#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄CONSOLE_CURSOR_INFO cursorInfo;cursorInfo.bVisible = 0; // 设置光标不可见 也可以写flasecursorInfo.dwSize = 100; // 设置光标大小SetConsoleCursorInfo(hOut, &cursorInfo);return 0;
}
按键信息获取
我们需要使用↑↓←→来控制蛇的移动,因此我们可以使用GetAsyncKeyState函数来获取按键信息
SHORT GetAsyncKeyState(int vKey
);
在返回的值中,如果最高位是1,表示这个键正在被按下,如果最低位是1,表示这个键被按过
我们可以使用一个预定义的宏来判断这个键是否被按下
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
这个大家肯定看的懂,如果不懂的话可以去学习一下预编译和位运算
贪吃蛇游戏设计
我们可以把贪吃蛇游戏分为三个部分:
- 游戏前的初始化,包括窗口的设置,光标的设置,欢迎界面打印,地图的初始化,蛇的初始化,食物的初始化等等
- 游戏中的循环,包括蛇的移动,食物的生成,蛇的身体的增长,游戏结束的判断等等
- 游戏结束后的处理,包括释放资源,打印游戏结束的信息等等
补充:当程序需要实现按任意键继续的时候,我们使用system(“pause”)函数即可
游戏前的初始化
设置窗口的大小和名称
void CmdInit(void)
{// 设置控制台窗口大小 100列 30行system("mode con cols=100 lines=30");// 设置控制台名称system("title snake");
}
本地化设置
正常在C语言中,我们使用的是ASCII字符集,他只使用了一个字节来表示一个字符,而在不同的国家和地区,字符集是不一样的,因此我们可以使用setlocale函数来设置字符集
char *setlocale(int category, const char *locale);
在C标准库中,我们可以更改的地区设置有以下这些:
- LC_ALL:所有的地区设置
- LC_COLLATE:字符串比较
- LC_CTYPE:字符分类和转换
- LC_MONETARY:货币格式
- LC_NUMERIC:非货币数字格式
- LC_TIME:时间格式
我么可以使用setlocale函数来设置地区,比如把地区设置为当前地区
#include <locale.h>
int main()
{setlocale(LC_ALL, "");return 0;
}
宽字符
Waht is 宽字符
宽字符是指一个字符占用两个字节的字符,中文以及一些特殊字符都是宽字符,
而且宽字符在控制台中是占用两个x坐标的
graph LR
A[宽字符] --> B[占用两个坐标位置] --> D[普通字符:█]
B --> E[宽字符:██]
A --> C[占用两个字节]
宽字符的打印
在C语言中,我们可以使用wprintf函数来打印宽字符,在打印宽字符之前,我们需要进行本地化设置
int wprintf(const wchar_t *format, ...);
eg:
#include <stdio.h>
int main()
{setlocale(LC_ALL, "");wchar_t str[] = L"你好,世界\n";wprintf(L"%s", str);return 0;
}
光标的设置
- 定义一个变量来接受控制台的句柄
- 定义一个变量来接受光标的属性
- 改变光标的属性
- 通过SetConsoleCursorInfo函数,输入句柄和光标属性,设置光标的属性
欢迎界面打印
这里我们需要设置光标位置并打印所需信息,因此我们可以封装一个函数来快捷地设置光标位置
void SetPos(int x, int y)
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = {x, y};SetConsoleCursorPosition(hOut, pos);
}
然后我们就可以打印欢迎信息和游戏规则了
// 打印欢迎信息
void WelcomeToGame(void)
{SetPos(45, 10);wprintf(L"欢迎来到贪吃蛇游戏");// 暂停SetPos(45, 20);system("pause");
}// 游戏介绍
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戏介绍:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的长度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4减速");SetPos(45, 18);wprintf(L"3. 空格暂停");SetPos(45, 20);wprintf(L"4. Esc退出游戏");// 暂停SetPos(45, 22);system("pause");
}


地图绘制
我们这里使用□来表示墙体,然后运用宽字符的打印知识来绘制一个27 * 27的地图
// 地图绘制
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{ // 清屏system("cls");// 上墙for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墙for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已经打印过了// 左墙for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墙for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}
帮助信息
我们在地图的右侧打印一些帮助信息,比如当前的分数,按键操作等等
// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"单个食物分数: 10");SetPos(70, 7);wprintf(L"总分数: 0");SetPos(70, 9);wprintf(L"操作说明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 减速");SetPos(70, 23);wprintf(L"空格: 暂停");SetPos(70, 25);wprintf(L"Esc: 退出");
}

蛇
蛇身的管理
我们可以将蛇看作是一个一个节点相互连接组成,因此我们可以使用链表来管理蛇的身体,我们先创建一个结构体来管理蛇身的节点
// 蛇节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;
贪吃蛇游戏的管理(插播,重要)
使用一个结构体来管理整个贪吃蛇游戏,包括:
- 蛇身
- 食物
- 单个食物分数
- 总分数
- 睡眠时间
- 方向
- 游戏状态
// 方向
typedef enum Direction
{UP = 1, // 上DOWN, // 下LEFT, // 左RIGHT // 右} Direction;// 游戏状态
typedef enum GameStatus
{OK = 0, //运行中KILL_BY_WALL, //撞墙KILL_BY_SELF, //撞自己PAUSE, //暂停ESC //退出
} GameStatus;// 贪吃蛇游戏
typedef struct Snake
{pSnakeNode _pSnake; // 蛇头pSnakeNode _pFood; // 食物 可以共用蛇节点比较方便,下面会提int _foodScore; // 单个食物分数int _totalScore; // 总分数int _sleepTime; // 睡眠时间 -- 控制速度Direction _dir; // 方向GameStatus _status; // 游戏状态
} Snake, * pSnake;
在游戏开始前对这个结构体进行初始化
pSnake ps = (pSnake)malloc(sizeof(Snake));
蛇的初始化
我们可以使用一个链表来存储蛇的身体,这里我们创建一个有5个节点的蛇,并且初始化蛇的位置
#define BODY L'●'
#define POS_X 6
#define POS_Y 6// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 创建蛇身节点 并初始化坐标 for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 头插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}

食物的管理
我们可以把食物也看作是一个蛇节点,只不过还没有加入到蛇身体中,因此我们可以使用蛇身结构体来存储食物
我们在游戏开始和蛇吃到食物的时候,生成一个食物节点,并且打印食物到屏幕上
// 创建食物节点
void CreatFood(pSnake ps)
{// 申请节点空间pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 随机生成食物坐标 在墙范围内但是不能在蛇身上while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);
}
这里的随机数并不是真正的随机数,而是伪随机数,因此我们可以在mian里面使用srand函数来设置随机数的种子
int main()
{srand(time(NULL));return 0;
}

蛇的移动
- 按键检测, 改变方向
- 判断是否吃到食物
- 吃到食物,头插食物节点,创建新的食物节点
- 没吃到食物,头插一个新的节点,新的节点是蛇头移动到的下一个坐标,尾删一个节点
- 蛇移动后进行打印,判断是否撞到自己或者墙壁,是则游戏结束
这里封装了一个宏来读取按键状态,如果最低位是1,则返回1,反之返回0
也就是一个按键按下过返回1,否则返回0
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按键检测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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 设置下一个节点的x,y坐标switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判断是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墙检测if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己检测if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again; //使用goto 返回到前面}// 撞墙打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墙墙被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你带走了");}return;
}
EatFood函数
void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印总分数SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 头插食物节点 创建新节点ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇头SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 创建新食物CreatFood(ps);
}
NotEatFood函数
// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{// 创建新节点并头插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 顺便删除尾节点 释放空间 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L" ");free(cur->next);cur->next = NULL;
}
IsKillByWall函数
// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}

IsKillBySelf函数
// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 从第二个节点开始遍历 并判断是否和蛇头坐标相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}

除了提到的以外,我们还要对加速减速的按键进行处理,还有总分的统计,失败的打印信息等等,这里就由大家自由发挥啦,不行的话也可以看我的源码
游戏结束
恭喜你,你已经得到了一个属于你的小小贪吃蛇游戏了,童年的游戏已经被你复刻,但是你现在还不可以拍拍屁股走人哦,我们还需要做好善后工作啦
- 释放资源
// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{// 释放蛇节点pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 释放食物节点free(ps->_pFood);
}
拓展建议
本文会有一些不足之处,但不影响整体的学习,如果大家有问题可以用自己的方法解决或者发在评论区
拓展建议:
- 可以优化画面,多加一些符号,可以fancy一点,比如这样
// 使用--,|,/ 绘制英文SNAKE
/* _____ _ _____| \ |\_ | / \ | _/ ||_____ | \ | / \ |__/ |_____| | \_ | /_____\ | \_ |\____| | \| / \ | \ |_____*/
void PrintSnake(void)
{SetPos(32, 5);printf(" _____ _ _____");SetPos(32, 6);printf(" | \\ |\\_ | / \\ | _/ |");SetPos(32, 7);printf(" |_____ | \\ | / \\ |__/ |_____");SetPos(32, 8);printf(" | | \\_ | /_____\\ | \\_ |");SetPos(32, 9);printf(" \\____| | \\| / \\ | \\ |_____");
}

- 可以配合EasyX图形库,做一个图形化的贪吃蛇游戏
- 可以加入再来一局的功能
- 可以加入排行榜,计分等,配合写入读取文件
- 可以加入音效,配合Windows API的Beep函数,不过会有阻塞
- 加入双人模式,可以使用WSAD控制第二条蛇
- 等等等等还有好多啦,大家可以自行探索
寄语
感谢每一位看到这里的自己🌹🌹🌹
分享一句话:
我将玫瑰藏于身后,风起花落,从此鲜花赠自己,纵马踏花向自由
源码
test.c
#include "snake.h"int main()
{// 使用当前的时间作为种子值srand(time(NULL));pSnake ps = (pSnake)malloc(sizeof(Snake));SnakeInit(ps);GameStart(ps); // 游戏初始化GameRun(ps); // 游戏运行GameEnd(ps); // 游戏结束return 0;
}
snake.h
#pragma once#include <Windows.h>
#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>// 方向
typedef enum Direction
{UP = 1, // 上DOWN, // 下LEFT, // 左RIGHT // 右} Direction;// 游戏状态
typedef enum GameStatus
{OK = 0, //运行中KILL_BY_WALL, //撞墙KILL_BY_SELF, //撞自己PAUSE, //暂停ESC //退出
} GameStatus;// 蛇节点
// struct SnakeNode 取别名 SnakeNode
// struct SnakeNode * 取别名 pSnakeNode
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;// 贪吃蛇游戏
typedef struct Snake
{pSnakeNode _pSnake; // 蛇头pSnakeNode _pFood; // 食物int _foodScore; // 单个食物分数int _totalScore; // 总分数int _sleepTime; // 睡眠时间 -- 控制速度Direction _dir; // 方向GameStatus _status; // 游戏状态
} Snake, * pSnake;void GameStart(pSnake ps);
void SnakeInit(pSnake ps);
void GameRun(pSnake ps);
void GameEnd(pSnake ps);
snake.c
#include "snake.h"
#include <stdio.h>
#include <stdlib.h>#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 6
#define POS_Y 6// ↑↓←→●□★ // 贪吃蛇游戏结构体初始化
void SnakeInit(pSnake ps)
{ps->_pSnake = NULL;ps->_pFood = NULL;ps->_foodScore = 10;ps->_totalScore = 0;ps->_status = OK;ps->_dir = RIGHT;ps->_sleepTime = 200;
}// 设置控制台窗口大小和名称
void CmdInit(void)
{// 设置控制台窗口大小 100列 30行system("mode con cols=100 lines=30");// 设置控制台名称system("title snake");
}// 光标隐藏
void CursorHide(void)
{// 获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 光标信息CONSOLE_CURSOR_INFO cursor_info = {0};// 获取光标信息GetConsoleCursorInfo(handle, &cursor_info);// 设置光标属性cursor_info.dwSize = 10;cursor_info.bVisible = 0;SetConsoleCursorInfo(handle, &cursor_info);
}// 设置光标位置
void SetPos(int x, int y)
{// 获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 设置光标位置COORD pos = {x, y};SetConsoleCursorPosition(handle, pos);
}void PrintSnake(void)
{SetPos(45, 10);wprintf(L"欢迎来到贪吃蛇游戏");
}// 打印欢迎信息
void WelcomeToGame(void)
{PrintSnake();// 暂停SetPos(45, 20);system("pause");
}// 游戏介绍
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戏介绍:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的长度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4减速");SetPos(45, 18);wprintf(L"3. 空格暂停");SetPos(45, 20);wprintf(L"4. Esc退出游戏");// 暂停SetPos(45, 22);system("pause");
}// 地图绘制
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{ // 清屏system("cls");// 上墙for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墙for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已经打印过了// 左墙for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墙for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"单个食物分数: 10");SetPos(70, 7);wprintf(L"总分数: 0");SetPos(70, 9);wprintf(L"操作说明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 减速");SetPos(70, 23);wprintf(L"空格: 暂停");SetPos(70, 25);wprintf(L"Esc: 退出");
}// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 创建蛇身节点 并初始化坐标 for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 头插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}// 创建食物节点
void CreatFood(pSnake ps)
{// 申请节点空间pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 随机生成食物坐标 在墙范围内但是不能在蛇身上 而且x坐标是偶数while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y || x % 2 != 0){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);ps->_pFood = foodnode;
}// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 从第二个节点开始遍历 并判断是否和蛇头坐标相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}// 吃掉食物后的处理
void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印总分数SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 头插食物节点 创建新节点ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇头SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 创建新食物CreatFood(ps);
}// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{// 创建新节点并头插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 顺便删除尾节点 释放空间 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L" ");free(cur->next);cur->next = NULL;
}// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按键检测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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 设置下一个节点的x,y坐标switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判断是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墙检测if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己检测if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again;}// 撞墙打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墙墙被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你带走了");}return;
}// 游戏前的初始化
void GameStart(pSnake ps)
{// 初始化控制台CmdInit();CursorHide();// 本地化配置setlocale(LC_ALL, "");WelcomeToGame();GameIntroduction();MapDraw();InitSnake(ps);CreatFood(ps);
}// 游戏运行
void GameRun(pSnake ps)
{PrintStaticHelp();SnakeMove(ps);SetPos(45, 28);system("pause");
}// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{// 释放蛇节点pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 释放食物节点free(ps->_pFood);
}
相关文章:
贪吃蛇小游戏简单制作-C语言
文章目录 游戏背景介绍实现目标适合人群所需技术浅玩Window API什么是API控制台程序窗口大小,名称设置 Handle(句柄)获取句柄 坐标结构体设置光标位置 光标属性获取光标属性设置光标属性 按键信息获取 贪吃蛇游戏设计游戏前的初始化设置窗口的大小和名称本地化设置 宽字符Waht …...
Oracle数据库-重点信息查询方法
文章目录 一、数据库信息及查询方法1.1是否为RAC1.2 数据库存储容量大小1.3 在线会话数1.4 最大分区数1.5 最大存储过程行数1.6 单表最大行数1.7 最大单表大小1.8 表总数量1.9 无主键表的数量1.10 字段数超过200的宽表1.11 关注CPU耗时高的SQL 一、数据库信息及查询方法 1.1是…...
【全开源】多平台租房系统源码(Fastadmin+ThinkPHP+Uniapp)
🏠多平台租房系统:一站式租房新体验🔍 🌐一、引言:租房市场的变革 在快节奏的现代生活中,租房已成为许多人解决居住问题的首选。然而,传统的租房方式往往繁琐且效率低下。随着互联网的飞速发展…...
Pythond 的 corr函数
Python corr函数科普 在数据分析和机器学习领域,数据的相关性是一个非常重要的概念。相关性可以帮助我们理解数据之间的关系,并且可以作为一种预测模型的基础。Python中的corr()函数是一个用于计算数据之间相关性的强大工具。本文将介绍corr()函数的使用方法,并通过代码示例…...
Fiddler 中文版 (强大的网络响应HTPP协议抓包工具)
前言 Fiddler Web Debugger,功能强大的抓包工具,Web调试工具,HTTP协议抓包调试工具。它能够捕获浏览器和程序的所有http/https通信连接,可以针对访问请求,分析请求数据报文、设置断点、调试web程序、解密和美化JS脚本…...
初出茅庐的小李博客之JSON格式介绍
什么是JSON JSON:JavaScript Object Notation (翻译就是JavaScript 对象表示法),是一种表示对象的方法。 JSON 是存储和交换文本信息的语法,类似 XML。但是JSON 比 XML 更小、更快,更易解析。此外JSON也易于人阅读和编写。而且主流的编程语言…...
Vue3相关语法内容,组件传值,事件监听,具名插槽。
1、Vue3相关语法内容 赋值语句(ref、reactive系列)组件传值(父子,子父)watch,watchEffect监听slot具名插槽 1、赋值语法(ref,reactive) 1.1、ref 、isRef、 shallowRef、triggerRef、customRef 支持所有的类型&…...
Linux用户,用户组,所有者权限分配,sftp用户权限分配
注意以下命令执行需要在root用户下执行 tenant命令切换至root命令 sudo -do root 删除用户信息 1.不删除用户主目录 userdel user_name 2.删除用户主目录 userdel -r user_name usermod命令修改用户账户权限 更改用户名 sudo usermod -l newusername oldusername 更…...
iFlyCode:AI智能编程助手引领未来软件开发新趋势
体验地址 在当前软件行业飞速发展的背景下,开发效率和代码质量成为了衡量软件工程师工作效能的两大关键指标。为了应对日益增长的市场需求和紧迫的发布时间,科大讯飞推出了iFlyCode2.0——一款集AI技术于一身的智能编程助手,旨在引领未来软件…...
高低温测试发现文件被篡改
背景 高低温测试-40度和85度压测,出现程序崩溃现象(挂测日志看)。设备常温后也无法恢复,重启后也无法恢复。 定位排查 先校验程序资源文件一致性是否正确 1.取出设备中的程序资源,包括执行文件和主要的so文件(可以从大的文件开始) 2.…...
高考真的不再重要了吗?
阅读本文大概需要 1.11 分钟 一年一度的高考又落幕了,看到不少人说今年的高考热度好像少了几分,不再像过去那样热闹。于是就有人纳闷,高考是不是不那么重要了。 其实你觉得高考不重要,可能是因为你家今年没考生。就像你不怎么关注…...
spring常用注解(八)@Async
一、介绍 1、介绍 二、原理 三、集成与使用 1、集成方法 (1)开启 使用以下注解开启 EnableAsync (2)使用 在需要异步处理的方法上加上 Async 2、返回值 Async注解的方法返回值只能为void或者Future<T>。 &…...
B站画质补完计划(3):智能修复让宝藏视频重焕新生
1 老片存在什么画质问题? B站作为一个拥有浓厚人文属性的平台社区,聚集了诸如《雍正王朝》、《三国演义》等经典影视剧集,同时也吸引了大量用户欣赏、品鉴这些人文经典 。但美中不足的是,由于拍摄年代久远、拍摄设备落后、数据多次…...
Spring Cloud Stream整合RocketMQ
Spring Cloud Stream整合RocketMQ 这里书接上回,默认你已经搭建好了RocketMQ主从异步集群,前面文章已经介绍过搭建方法。 1、Spring Cloud Stream介绍 Spring Cloud Stream是一个框架,用于构建与共享消息系统连接的高度可扩展的事件驱动微服…...
Web前端浪漫源码:编织梦想与爱的交织乐章
Web前端浪漫源码:编织梦想与爱的交织乐章 在数字世界的广袤宇宙中,Web前端浪漫源码犹如一段段秘密的旋律,编织着梦想与爱的交织乐章。它们不仅是技术的结晶,更是情感的载体,将浪漫与创意融入每一个像素和每一行代码之…...
【云岚到家】-day02-4-我的账户-实名认证
【云岚到家】-day02-4-我的账户-实名认证 1 我的账户设置-实战1.1 配置OSS1.2 需求分析1.2.1 服务端设置银行账户1.2.2 机构端设置银行账户1.2.3 表结构设计1.2.4 表结构相关的controller、service、mapper、entity 1.3 服务端设置银行账户接口设计1.3.1 新增或更新银行账号信息…...
MySQL复习题(期末考试)
MySQL复习题(期末考试) 1.MySQL支持的日期类型? DATE,DATETIME,TIMESTAMP,TIME,TEAR 2.为表添加列的语法? alter table 表名 add column 列名 数据类型; 3.修改表数据类型的语法是? alter table 表名 modify 列名 新…...
利用DVWA演示文件上传漏洞获取网站shell权限(二)
文件上传漏洞是网络安全中常见的一种漏洞类型,攻击者可以利用该漏洞上传恶意文件到服务器上,从而获得对网站的远程控制权限。本文将以DVWA (Damn Vulnerable Web Application) 为例,演示如何利用文件上传漏洞的Medium级别设置,绕过…...
Java---BigInteger和BigDecimal和枚举
1.简介 1.BigInteger可以支持任意长度的整数 2.BigDecimal可以支持任意精度的浮点数 3.用来做精确计算 2.创建方式 new BigInteger(); new BigInteger(参数1,进制):可以将不同进制转成10进制显示 new BigDecimal(); BigInteger.valueOf(); BigDecimal.valueOf();…...
mybatis数据批量更新
1、mybatis批量更新mapper <update id"updateBatchById"><foreach collection"list" item"s" separator";">updatetableNamesetname #{name},whereid #{id}</foreach> </update>通过在数据库连接URL中指定…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
