文章目录
- 游戏背景介绍
- 实现目标
- 适合人群
- 所需技术
- 浅玩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);
}