贪吃蛇游戏
- [一, 准备工作](#一, 准备工作)
-
- [1. vs控制台](#1. vs控制台)
- [2. Win32 API介绍](#2. Win32 API介绍)
- 3.控制台程序,cmd指令,system函数
- [4.Win32 API函数介绍](#4.Win32 API函数介绍)
-
- [4.1. 控制台屏幕上的坐标COORD](#4.1. 控制台屏幕上的坐标COORD)
- [4.2. 获取句柄权限GetStdHandle](#4.2. 获取句柄权限GetStdHandle)
- [4.3. 屏幕光标属性](#4.3. 屏幕光标属性)
-
- [4.3.1. GetConsoleCursorInfo](#4.3.1. GetConsoleCursorInfo)
- [4.3.2. CONSOLE_CURSOR_INFO](#4.3.2. CONSOLE_CURSOR_INFO)
- [4.3.3. SetConsoleCursorInfo](#4.3.3. SetConsoleCursorInfo)
- [4.3.4. SetConsoleCursorPosition](#4.3.4. SetConsoleCursorPosition)
- [4.4. 获取键盘按键情况GetAsyncKeyState](#4.4. 获取键盘按键情况GetAsyncKeyState)
- [4.5. setlocale函数和宽字符](#4.5. setlocale函数和宽字符)
- 二,游戏实现效果
- 三,游戏整体架构与逻辑分析
-
- [1. 项目结构](#1. 项目结构)
- [2. 游戏逻辑](#2. 游戏逻辑)
- [3. 整体大纲](#3. 整体大纲)
- [四, 游戏功能实现](#四, 游戏功能实现)
-
- [1 所需结构体数据](#1 所需结构体数据)
- [2 游戏开始GameStart(pSnake ps)](#2 游戏开始GameStart(pSnake ps))
-
- [2.1 设置窗口信息和光标属性](#2.1 设置窗口信息和光标属性)
- [2.2 打印欢迎界面WellcomeGame();](#2.2 打印欢迎界面WellcomeGame();)
- [2.3 初始化地图CreateMap();](#2.3 初始化地图CreateMap();)
- [2.4 初始化蛇身InitSnak(ps);](#2.4 初始化蛇身InitSnak(ps);)
- [2.5 创建出一个食物CreateFood(ps);](#2.5 创建出一个食物CreateFood(ps);)
- [3 游戏运行GameRun(pSnake ps)](#3 游戏运行GameRun(pSnake ps))
-
- [3.1 SnakeMove(ps);](#3.1 SnakeMove(ps);)
- [3.2 EatFood](#3.2 EatFood)
- [3.3 NoFood](#3.3 NoFood)
- [3.4 KillBySelf](#3.4 KillBySelf)
- [3.5 KillByWall](#3.5 KillByWall)
- [3.6 pause暂停](#3.6 pause暂停)
- [4 游戏结束GameEnd(pSnake ps);](#4 游戏结束GameEnd(pSnake ps);)
- 五,源代码
-
- [1. snake.h](#1. snake.h)
- [2. snake.c](#2. snake.c)
- [3. text.c](#3. text.c)
一, 准备工作
1. vs控制台
- 设置vs运行环境为Window控制台而并非是Window终端
- 正确的控制台界面
如果你的界面是这样的:
那么就需要进行修改:
2. Win32 API介绍
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程式达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便称之为Application Programming Interface,简称API函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
3.控制台程序,cmd指令,system函数
平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,35⾏,100列
也可以使用cmd命令来设置窗口的名字:贪吃蛇
system("mode con cols=100 lines=35");
system("title 贪吃蛇");
4.Win32 API函数介绍
4.1. 控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀种结构,表⽰⼀个字符在控制台屏幕上的坐标
c
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标赋值
d
COORD pos = { 10, 15 };
4.2. 获取句柄权限GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
简单点就是我们想要对屏幕进行操作,我们首先要拿到屏幕的控制权,这个句柄就有点像是控制权一样。
c
HANDLE GetStdHandle(DWORD nStdHandle);
实例:
c
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
4.3. 屏幕光标属性
4.3.1. GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息
c
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
功能:
获取光标相关信息
参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
lpConsoleCursorInfo 指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关控制台游标的信息。
返回值:
如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError
实例:
c
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
4.3.2. CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台游标的信息
c
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
参数:
dwSize,由光标填充的字符单元格的百分⽐。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条。
bVisible,游标的可⻅性。如果光标可⻅,则此成员为true,否则就是false。
CursorInfo.bVisible = false; //隐藏控制台光标
4.3.3. SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性
c
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
功能:
设置光标的属性
参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
lpConsoleCursorInfo(输出型参数) 指向CONSOLE_CURSOR_INFO结构的指针,该结构为控制台屏幕缓冲区的游标提供新规范。
返回值:
如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
实例:
c
//获得句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
4.3.4. SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
c
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
实例:
c
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
SetPos:封装⼀个设置光标位置的函数
c
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
4.4. 获取键盘按键情况GetAsyncKeyState
c
SHORT GetAsyncKeyState(
int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.也就是一个和简单的按位与操作即可。
c
//用宏定义,更方便
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
4.5. setlocale函数和宽字符
包含头文件<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
c
char* setlocale (int category, const char* locale);
具体是怎么实现的我们也不关心直接拿来用就行:
c
setlocale(LC_ALL, " ");//切换到本地环境
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
打印宽字符的格式:
c
wprintf(L"%lc", 打印的宽字符);
二,游戏实现效果
三,游戏整体架构与逻辑分析
1. 项目结构
- 头文件snake.h存放蛇身节点结构体,蛇身结构体,方向枚举,状态枚举,宏定义,以及实现函数声明
- 源文件snake.c用于申明函数的定义实现
- 源文件text.c整体代码的测试和进入窗口
2. 游戏逻辑
- 点击欢迎界面,点击开始游戏界面
- 用↑,↓,←,→来控制运行方向
- 按空格键暂停,Esc退出游戏
- 按键F3加速,F4减速,加速每个食物得分更高,减速每个食物得分更低
- 撞墙或者撞到自己游戏结束
- 提示是否再来一局信息
3. 整体大纲
四, 游戏功能实现
1 所需结构体数据
c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <locale.h>
#include <stdbool.h>
#include <time.h>
#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)
//状态,
enum State
{
OK, //正常运行
NORMAL_EXIT, //按Esc退出键
KILL_BY_WALL, //撞墙了
KILL_BY_SELF //撞到自己
};
//蛇头的方向
enum Dirction
{
UP = 1, //上
DOWN, //下
LEFT, //左
RIGHT //右
};
//组成蛇身的每个节点
typedef struct SnakeNode
{
int _x; //横坐标
int _y; //纵坐标
struct SnakeNode* next; //链表的下一个节点
}SnakeNode, *pSnakeNode;
//整体的蛇身结构
typedef struct Snake
{
pSnakeNode _head; //蛇头节点
pSnakeNode _food; //食物节点
enum State _state; //蛇的运行状态
enum Driction _dir; //蛇的运行方向
int _score; //总得分
int _add; //每个食物的得分
int _sleep; //蛇的移动速度
}Snake, *pSnake;
2 游戏开始GameStart(pSnake ps)
void GameStart(pSnake ps)函数用于这个有些界面的初始化工作,包括地图,蛇身,食物,提示信息等
- 设置窗口大小光标属性
- 打印欢迎界面WellcomeGame();
- 初始化地图CreateMap();
- 初始化蛇身InitSnak(ps);
- 创建出第一个食物CreateFood(ps);
2.1 设置窗口信息和光标属性
c
void GameStart(pSnake ps)
{
//1.这只窗口大小
system("mode con cols=100 lines=35");
system("title 贪吃蛇");
//获取标准输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 2. 隐藏光标
CONSOLE_CURSOR_INFO CurosrInfo;
GetConsoleCursorInfo(hOutput, &CurosrInfo);
CurosrInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CurosrInfo);
//3. 打印欢迎界面
WellcomeGame();
//4. 初始化地图
CreateMap();
//5. 初始化蛇身
InitSnak(ps);
//6. 创建第一个食物
CreateFood(ps);
}
2.2 打印欢迎界面WellcomeGame();
void WellcomeGame();
配合SetPos设置坐标打印就可以按照我们的意向打印在中间
c
void WellcomeGame()
{
SetPos(40, 14);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 12);
printf("请使用↑,↓,←,→来控制蛇的运行方向,F3为加速,F4为减速");
SetPos(40, 25);
system("pause");
system("cls");
}
2.3 初始化地图CreateMap();
void CreateMap();
这里要注意了,我们地图是用宽字符来充当的,而宽字符是占两个字节的,也就是屏幕上的两个格子(横坐标)。
这我们设置的是x轴有28个格子也就是x轴单位长度是56,而y轴式26个格子,由于只是x轴方向上式占两个字节的,而y轴并没有所以这里的y轴的单位长度同样也是26。这样我们就可以绘制地图了
- x上(0,0)------(56,0)
- x下(0,26)------(56,26)
- y左((0,1)------(1,25)
- y右(56,2)------(56,25)
c
void CreateMap()
{
//上
SetPos(0, 0);
for (int i = 0; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (int i = 0; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
SetPos(0, 0);
for (int i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
2.4 初始化蛇身InitSnak(ps);
void InitSnak(pSnake ps);
InitSnak(ps)用于在地图上创建出蛇身
- 这里初始化蛇身的长度是5,采用头插法
- 默认蛇头的方向是向右的,正常速度
- 打印蛇身在地图上
- 初始化蛇身的数据
这里要特别注意的就是我们现在是以一个宽字符为基本单位的,所以这里的蛇身的节点宽度也必须是宽字符的长度,也就是说蛇身的节点必须是2的倍数。
c
void InitSnak(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
//蛇身的节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnak()::malloc()");
}
//设置坐标
cur->next = NULL;
cur->_x = POS_X + i * 2; //这里要确保是2的倍数
cur->_y = POS_Y;
//头插法
if (ps->_head == NULL)
{
ps->_head = cur;
}
else
{
cur->next = ps->_head;
ps->_head = cur;
}
}
//打印蛇身
cur = ps->_head;
while (cur)
{
SetPos(cur->_x, cur->_y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_food = NULL;
ps->_sleep = 200;
ps->_add = 10;
ps->_score = 0;
ps->_state = OK;
ps->_dir = RIGHT;
}
2.5 创建出一个食物CreateFood(ps);
void CreateFood(pSnake ps);创建一个食物初始化的时候我们要确保有一个食物
- 食物的基本单位也要是一个宽字符的单位长度,也就是说也要是2的倍数
- 食物的创建不能是在墙以外的地方
- 食物的创建不能出现在蛇的身体上
c
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//食物不能出现在蛇的身体处
pSnakeNode cur = ps->_head;
while (cur)
{
if (cur->_x == x && cur->_y == y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc");
return;
}
else
{
pFood->_x = x;
pFood->_y = y;
SetPos(pFood->_x, pFood->_y);
wprintf(L"%lc", FOOD);
ps->_food = pFood;
}
}
3 游戏运行GameRun(pSnake ps)
void GameRun(pSnake ps)
- 打印帮助文档和时事得分
- 获取按键信息使用宏定义#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0),跟新蛇身结构体中的方向信息
- 移动蛇身
- 如果按的空格键也就是暂停,那么也就是说处于一种不懂的状态类似于阻塞,不会到SnakeMove函数,我们就可以设计一个死循环。
c
void GameRun(pSnake ps)
{
//打印帮助文档和时事得分
PrintfHelpInfo();
do
{
SetPos(64, 10);
printf("得分:%d\n", ps->_score);
SetPos(64, 11);
printf("每个食物的分数:%2d", ps->_add);
//获取按键信息
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))
{
ps->_state = NORMAL_EXIT;
break;
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_add > 6)
{
ps->_sleep += 30;
ps->_add -= 2;
}
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_sleep > 80)
{
ps->_sleep -= 30;
ps->_add += 2;
}
}
//Sleep的长度就是蛇的移动速度
Sleep(ps->_sleep);
SnakeMove(ps);
} while (ps->_state == OK);
}
3.1 SnakeMove(ps);
void SnakeMove(pSnake ps);用于实现蛇的移动
- 先创建一个临时的节点,通过蛇身结构的里的方向信息确定这个节点的x,y的值
- 判断下一个位置是否是食物
- 如果是食物就进行头插,并释放食物节点,最后打印蛇身
- 如果不是食物同样进行头插,只是打印蛇身的时候最后一个节点并不打印出来而是打印两个空格,并释放最后一个节点。
- 由于是以宽字符为基本的运行单位长度同样要确保下一个位置的横坐标是2的倍数
c
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->_head->_x;
pNextNode->_y = ps->_head->_y - 1;
break;
case DOWN:
pNextNode->_x = ps->_head->_x;
pNextNode->_y = ps->_head->_y + 1;
break;
case LEFT:
pNextNode->_x = ps->_head->_x - 2;
pNextNode->_y = ps->_head->_y;
break;
case RIGHT:
pNextNode->_x = ps->_head->_x + 2;
pNextNode->_y = ps->_head->_y;
break;
}
//如果下一个是食物
if (pNextNode->_x == ps->_food->_x && pNextNode->_y == ps->_food->_y)
{
EatFood(ps, pNextNode);
}
else
{
NoFood(ps, pNextNode);
}
//判读是否是撞墙还是撞到自己了
KillBySelf(ps);
KillByWall(ps);
}
3.2 EatFood
void EatFood(pSnake ps, pSnakeNode pNextNode)
- 如果是食物的话直接头插,并打印蛇身
- 同时要释放食物节点,并创建一个新的食物节点
c
void EatFood(pSnake ps, pSnakeNode pNextNode)
{
//发现食物,头插
pNextNode->next = ps->_head;
ps->_head = pNextNode;
//打印蛇身
pSnakeNode cur = ps->_head;
while (cur)
{
SetPos(cur->_x, cur->_y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//释放食物节点
free(ps->_food);
ps->_food = NULL;
ps->_score += ps->_add;
//创建食物
CreateFood(ps);
}
3.3 NoFood
void NoFood(pSnake ps, pSnakeNode pNextNode);
- 如果不是食物则同样进行头插
- 打印舍身的时候并不打印最后一个蛇的节点,而是打印两个空格,并释放最后一个节点。
c
void NoFood(pSnake ps, pSnakeNode pNextNode)
{
//没有发现食物,通用吧pNextNode头插,同时释放最后一个节点
pNextNode->next = ps->_head;
ps->_head = pNextNode;
//打印蛇身
pSnakeNode cur = ps->_head;
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;
}
3.4 KillBySelf
void KillBySelf(pSnake ps);
c
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_head;
//判断这个新节点是否是蛇身节点中的一个
pSnakeNode Next = cur->next;
while (Next)
{
if (cur->_x == Next->_x && cur->_y == Next->_y)
{
ps->_state = KILL_BY_SELF;
break;
}
Next = Next->next;
}
}
3.5 KillByWall
void KillByWall(pSnake ps);
- 根据我们的墙的范围判断是否撞墙
c
void KillByWall(pSnake ps)
{
if (ps->_head->_x == 0 ||
ps->_head->_x == 56 ||
ps->_head->_y == 0 ||
ps->_head->_y == 26)
ps->_state = KILL_BY_WALL;
}
3.6 pause暂停
void pause();当空格键时直接进入死循环,只有再次按空格时才正常运行。
c
void pause()
{
while (1)
{
if (KEY_PRESS(VK_SPACE))
break;
}
}
4 游戏结束GameEnd(pSnake ps);
void GameEnd(pSnake ps);
- 根据结束的条件判断游戏结束原因
- 同时释放所有的蛇身节点
c
void GameEnd(pSnake ps)
{
SetPos(20, 13);
switch (ps->_state)
{
case NORMAL_EXIT:
printf("离开游戏\n");
break;
case KILL_BY_SELF:
printf("自杀了,游戏结束");
break;
case KILL_BY_WALL:
printf("撞墙了,游戏结束\n");
}
//释放节点
pSnakeNode cur = ps->_head;
while (cur)
{
pSnakeNode dele = cur;
cur = cur->next;
free(dele);
}
}
五,源代码
1. snake.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <locale.h>
#include <stdbool.h>
#include <time.h>
#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)
enum State
{
OK,
NORMAL_EXIT,
KILL_BY_WALL,
KILL_BY_SELF
};
enum Dirction
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
typedef struct SnakeNode
{
int _x;
int _y;
struct SnakeNode* next;
}SnakeNode, *pSnakeNode;
typedef struct Snake
{
pSnakeNode _head;
pSnakeNode _food;
enum State _state;
enum Driction _dir;
int _score;
int _add;
int _sleep;
}Snake, *pSnake;
//设置坐标
void SetPos(short x, short y);
//开始游戏
void GameStart(pSnake ps);
//欢迎界面
void WellcomeGame();
//创建地图
void CreateMap();
//初始化蛇身
void InitSnak(pSnake ps);
//产生食物
void CreateFood(pSnake ps);
//运行游戏
void GameRun(pSnake ps);
//打印帮助文档
void PrintfHelpInfo();
//蛇移动
void SnakeMove(pSnake ps);
//吃到食物
void EatFood(pSnake ps, pSnakeNode pNextNode);
//没有食物
void NoFood(pSnake ps, pSnakeNode pNextNode);
//暂停
void pause();
//结束游戏
void GameEnd(pSnake ps);
//是否撞墙/咬到自己
void KillBySelf(pSnake ps);
void KillByWall(pSnake ps);
2. snake.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
void WellcomeGame()
{
SetPos(40, 14);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 12);
printf("请使用↑,↓,←,→来控制蛇的运行方向,F3为加速,F4为减速");
SetPos(40, 25);
system("pause");
system("cls");
}
void CreateMap()
{
//上
SetPos(0, 0);
for (int i = 0; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (int i = 0; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
SetPos(0, 0);
for (int i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
void InitSnak(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
//蛇身的节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnak()::malloc()");
}
//设置坐标
cur->next = NULL;
cur->_x = POS_X + i * 2;
cur->_y = POS_Y;
//头插法
if (ps->_head == NULL)
{
ps->_head = cur;
}
else
{
cur->next = ps->_head;
ps->_head = cur;
}
}
//打印蛇身
cur = ps->_head;
while (cur)
{
SetPos(cur->_x, cur->_y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_food = NULL;
ps->_sleep = 200;
ps->_add = 10;
ps->_score = 0;
ps->_state = OK;
ps->_dir = RIGHT;
}
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//食物不能出现在蛇的身体处
pSnakeNode cur = ps->_head;
while (cur)
{
if (cur->_x == x && cur->_y == y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc");
return;
}
else
{
pFood->_x = x;
pFood->_y = y;
SetPos(pFood->_x, pFood->_y);
wprintf(L"%lc", FOOD);
ps->_food = pFood;
}
}
void GameStart(pSnake ps)
{
//1.这只窗口大小
system("mode con cols=100 lines=35");
system("title 贪吃蛇");
//获取标准输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 2. 隐藏光标
CONSOLE_CURSOR_INFO CurosrInfo;
GetConsoleCursorInfo(hOutput, &CurosrInfo);
CurosrInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CurosrInfo);
//3. 打印欢迎界面
WellcomeGame();
//4. 初始化地图
CreateMap();
//5. 初始化蛇身
InitSnak(ps);
//6. 创建第一个食物
CreateFood(ps);
}
void PrintfHelpInfo()
{
SetPos(64, 15);
printf("1.不能穿墙,不能咬自己\n");
SetPos(64, 16);
printf("2.用↑,↓,←,→来控制运行方向\n");
SetPos(64, 17);
printf("3.F3为加速,F4为减速\n");
SetPos(64, 18);
printf("4.Esc退出游戏,空格暂停游戏\n");
}
void EatFood(pSnake ps, pSnakeNode pNextNode)
{
//发现食物,头插
pNextNode->next = ps->_head;
ps->_head = pNextNode;
//打印蛇身
pSnakeNode cur = ps->_head;
while (cur)
{
SetPos(cur->_x, cur->_y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//释放食物节点
free(ps->_food);
ps->_food = NULL;
ps->_score += ps->_add;
//创建食物
CreateFood(ps);
}
void NoFood(pSnake ps, pSnakeNode pNextNode)
{
//没有发现食物,通用吧pNextNode头插,同时释放最后一个节点
pNextNode->next = ps->_head;
ps->_head = pNextNode;
//打印蛇身
pSnakeNode cur = ps->_head;
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 KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_head;
pSnakeNode Next = cur->next;
while (Next)
{
if (cur->_x == Next->_x && cur->_y == Next->_y)
{
ps->_state = KILL_BY_SELF;
break;
}
Next = Next->next;
}
}
void KillByWall(pSnake ps)
{
if (ps->_head->_x == 0 ||
ps->_head->_x == 56 ||
ps->_head->_y == 0 ||
ps->_head->_y == 26)
ps->_state = KILL_BY_WALL;
}
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->_head->_x;
pNextNode->_y = ps->_head->_y - 1;
break;
case DOWN:
pNextNode->_x = ps->_head->_x;
pNextNode->_y = ps->_head->_y + 1;
break;
case LEFT:
pNextNode->_x = ps->_head->_x - 2;
pNextNode->_y = ps->_head->_y;
break;
case RIGHT:
pNextNode->_x = ps->_head->_x + 2;
pNextNode->_y = ps->_head->_y;
break;
}
//如果下一个是食物
if (pNextNode->_x == ps->_food->_x && pNextNode->_y == ps->_food->_y)
{
EatFood(ps, pNextNode);
}
else
{
NoFood(ps, pNextNode);
}
KillBySelf(ps);
KillByWall(ps);
}
void pause()
{
while (1)
{
if (KEY_PRESS(VK_SPACE))
break;
}
}
void GameRun(pSnake ps)
{
PrintfHelpInfo();
do
{
SetPos(64, 10);
printf("得分:%d\n", ps->_score);
SetPos(64, 11);
printf("每个食物的分数:%2d", ps->_add);
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))
{
ps->_state = NORMAL_EXIT;
break;
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_add > 6)
{
ps->_sleep += 30;
ps->_add -= 2;
}
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_sleep > 80)
{
ps->_sleep -= 30;
ps->_add += 2;
}
}
Sleep(ps->_sleep);
SnakeMove(ps);
} while (ps->_state == OK);
}
void GameEnd(pSnake ps)
{
SetPos(20, 13);
switch (ps->_state)
{
case NORMAL_EXIT:
printf("离开游戏\n");
break;
case KILL_BY_SELF:
printf("自杀了,游戏结束");
break;
case KILL_BY_WALL:
printf("撞墙了,游戏结束\n");
}
//释放节点
pSnakeNode cur = ps->_head;
while (cur)
{
pSnakeNode dele = cur;
cur = cur->next;
free(dele);
}
}
3. text.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void text()
{
int ch = 0;
do
{
Snake snake = { 0 };
// 1. 游戏开始
GameStart(&snake);
// 2. 游戏运行
GameRun(&snake);
// 3. 游戏结束
GameEnd(&snake);
SetPos(20, 14);
printf("游戏结束是否继续:[Y/N]:");
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//设置本地信息
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
text();
SetPos(0, 27);
return 0;
}