目录
2)GetConsoleCursorInfo--获取光标信息 和 SetConsoleCursorInfo--设置光标信息
3)SetConsoleCursorPosition--设置光标位置
4.初始化游戏GameStart(&snake); 内容讲解
6.结束游戏--善后工作--释放空间GameEnd(&snake);内容讲解
我的代码中没有对颜色进行修改,用的白色背景色,黑色前景色
如果你想让贪吃蛇界面五彩缤纷,那么就认真学习我在这篇文章中的颜色详解部分,学完你就可以独立设置打印的颜色了!!!让颜色变得简单起来吧!!!
一、电脑终端设置
在所有应用中找到终端打开,在终端黑框上边缘右键单击选择设置,设置为Windows控制台主机
或者随便运行一个代码,在代码运行黑框上边缘右键单击选择设置,设置为Windows控制台主机
按如上设置之后才能使用system函数对控制台的大小、颜色等属性进行控制
二、部分核心代码讲解
1.Windows_API讲解
下面是贪吃蛇中需要使用的Windous_API函数
Windows_API
GetStdHandle--获取控制台句柄
GetConsoleCursorInfo--获取光标信息
SetConsoleCursorInfo--设置光标信息
SetConsoleCursorPosition--设置光标位置
GetAsyncKeyState--获取按键状态
1)GetStdHandle--获取控制台句柄
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)。因为很多API函数都需要用到句柄,所以GetStdHandle是一个很重要的函数。
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);
2)GetConsoleCursorInfo--获取光标信息 和 SetConsoleCursorInfo--设置光标信息
GetConsoleCursorInfo函数:可以获取光标的占空比(即占总高度的多少)也就是高度(dwSize);也可以获取光标的亮灭(bVisible),该变量为bool变量,当它为false时表示光标透明,为true时光标为黑色。
SetConsoleCursorInfo函数:在获取光标信息后对光标信息进行期望的修改后调用函数进行设置。
_In_ HANDLE hConsoleOutput这个参数就是前面获取的控制台句柄!
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ CONST CONSOLE_CURSOR_INFO* lpConsoleCursorInfo
);
3)SetConsoleCursorPosition--设置光标位置
采用结构体储存光标位置坐标,在传入该函数进行光标位置的设置。
_In_ HANDLE hConsoleOutput这个参数就是前面获取的控制台句柄!
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
4)GetAsyncKeyState--获取按键状态
当某个键按下时该函数返回1,没按下则返回0。其中vKey就是按键的参数值,这里Windows已经定义好了,可以在WinUser.h这个头文件中查看,这里我把要用到的按键列出来
SHORT WINAPI GetAsyncKeyState( _In_ int vKey);
#define VK_LEFT 0x25//左
#define VK_UP 0x26//上
#define VK_RIGHT 0x27//右
#define VK_DOWN 0x28//下
#define VK_SPACE 0x20//空格
#define VK_ESCAPE 0x1B//ESC
#define VK_F3 0x72//F3
#define VK_F4 0x73//F4
2.控制台属性设置函数
注意这里的颜色设置只能设置全部背景色,在设置前景色的时候会将所有文字同时设置为一个颜色,也就是无法做到同一时刻屏幕上打印多彩的文字
但是!!!这是可以解决的,请看接着的解决办法
设置控制台的相关属性
system("mode con cols=100 lines=30");//设置大小
system("title 贪吃蛇");//设置名字
system("pause");//暂停程序
system("color 74");//设置颜色:两位数---十位是背景色---个位是前景色
system("cls");//清屏函数
颜色对应表
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 浅绿色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
3.颜色设置详解(背景色+前景色)
当想要实现五颜六色的文字时,我们可以用下面的方法。在这函数中我们发现他是对局部区域前景色和背景色的设置,所以我们要同时设置前景色(FOREGROUND)和背景色(BACKGROUND),在windous提供的颜色库中我们简单运算就发现按16进制理解就是第一位代表前景色(对应十进制中的个位),第二位代表背景色(对应十进制中的十位),所以可以用或这个运算法来表示颜色的总体设置,如前红后白就是0x04|0x70(即4|7*16),因为任何数或上0还是这个数;我们发现这就是前面我给到大家的颜色数字库,只要将颜色乘以16即可得到背景色颜色的数字库。
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND|BACKGROUND);
//这也是一个Windows_API函数
//可以对其进行封装
void color(int FOREGROUND,int BACKGROUND)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND|BACKGROUND); //颜色设置
//注:SetConsoleTextAttribute是一个API(应用程序编程接口)
}
#define FOREGROUND_BLUE 0x0001 // text color contains blue.
#define FOREGROUND_GREEN 0x0002 // text color contains green.
#define FOREGROUND_RED 0x0004 // text color contains red.
#define FOREGROUND_INTENSITY 0x0008 // text color is intensified.
#define BACKGROUND_BLUE 0x0010 // background color contains blue.
#define BACKGROUND_GREEN 0x0020 // background color contains green.
#define BACKGROUND_RED 0x0040 // background color contains red.
#define BACKGROUND_INTENSITY 0x0080 // background color is intensified.
#define COMMON_LVB_LEADING_BYTE 0x0100 // Leading Byte of DBCS
#define COMMON_LVB_TRAILING_BYTE 0x0200 // Trailing Byte of DBCS
#define COMMON_LVB_GRID_HORIZONTAL 0x0400 // DBCS: Grid attribute: top horizontal.
#define COMMON_LVB_GRID_LVERTICAL 0x0800 // DBCS: Grid attribute: left vertical.
#define COMMON_LVB_GRID_RVERTICAL 0x1000 // DBCS: Grid attribute: right vertical.
#define COMMON_LVB_REVERSE_VIDEO 0x4000 // DBCS: Reverse fore/back ground attribute.
#define COMMON_LVB_UNDERSCORE 0x8000 // DBCS: Underscore.
#define COMMON_LVB_SBCSDBCS 0x0300 // SBCS or DBCS flag.
这里有趣的地方在于三原色的定义 他们的二进制是 蓝色(0001)绿色(0010)红色(0100),在这三个位置的给1定义使得我们在代码中也可以进行三原色的组合,我们通过 或 运算可以得到各种颜色,就比如 红+蓝=紫(0001|0100=0101=5)。
哈哈,代码是不是很有趣呢?
三、游戏代码讲解
在经过前面的了解之后,相信大家也具备了对贪吃代码编写的能力,下面就开始代码的编写吧!
0.光标定位函数
这是贯穿全局的函数所以就先讲了
//光标位置设置
void set_pos(short x, short y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
COORD pos = { x,y };//光标位置信息结构体--Windows已经定义好了
SetConsoleCursorPosition(houtput, pos);//定位光标
}
1.游戏坐标设定
在终端中一个y轴纵向距离是一个x轴横向距离的两倍,后面的代码实现就是对相应坐标的指定打印的过程。
y = 2x
---------------------->x col//列
|
|
|
|
|
|
|
\/
y
row//行
2.主函数逻辑讲解
main函数中 setlocale(LC_ALL, "") 这个函数是进行本地化设置,目的是将编码方式设置为对应的编码方式(因为每个国家的语言有差异,就需要不同的编码方式来打印不同国家的文字)
#include"snake.h"
//游戏逻辑
void game()
{
char ch;
do
{
system("cls");
Snake snake = { 0 };//创建贪吃蛇
GameStart(&snake);//初始化游戏
GameRun(&snake);//运行游戏
GameEnd(&snake);//结束游戏--善后工作--释放空间
set_pos(20, 15);
printf("再来一局吗?(Y/N)(y/n):");
ch = getchar();//消除缓冲区的回车字符
while (getchar() != '\n');//消除输入的多余字符
} while (ch == 'y' || ch == 'Y');
set_pos(0, row);//定位光标--打印结束语
}
//主函数逻辑
int main()
{
setlocale(LC_ALL, "");//本地化设置
srand((unsigned int)time(NULL));//生成随机数
game();//进入游戏
return 0;
}
3.snake.h头文件中的声明
包含蛇信息结构体,蛇身结构体,游戏状态枚举,移动方向枚举
这里宏定义了一个函数:
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)
这就是之前说的Windows_API中的按键状态获取函数,再通过三目操作符将状态结果赋给KEY_PRESS(vk)。GetAsyncKeyState()的返回值表示两个内容,一个是最高位bit的值,代表这个键是否被按下,一个是最低位bit的值,代表在上次调用GetAsyncKeyState()后,这个键是否被按下。由于贪吃蛇是在移动中的方向改变,所以我们采用后者(最低位bit的值),所以我们将函数返回值&0x1即可(因为0x1二进制是0000 00001,如 0111 0101 & 0x1=0000 0001,0111 0100 & 0x1=0000 0000),得到最低位状态。
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define row 26//坐标轴定义
#define col 29
#define POS_X 24
#define POS_Y 5
#define WALL L'□'//中国地区编码方式 L'字符' 表示宽字符,即横宽两格
#define BODY L'●'
#define FOOD L'◆'
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)//获取按键状态
typedef enum DIRECTION//方向
{
UP,//上
DOWN,//下
LEFT,//左
RIGHT//右
}direction;
typedef enum GAME_STATUS//游戏状态
{
OK,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//吃自己
END_NORMAL//退出
}game_status;
typedef struct SnakeNode//蛇身体
{
int x, y;//坐标
struct SnakeNode* next;//指向下一个节点指针
}SnakeNode, * pSnakeNode;
typedef struct Snake//蛇的信息
{
pSnakeNode _pSnakeHead;//头节点
pSnakeNode _pFood;//食物
direction _dir;//移动方向
game_status _status;//游戏状态
int _food_weight;//一个食物的分数
int _score;//总得分
int _sleep_time;//移动速度--睡眠时间
int _MAX;//最高得分记录
}Snake,* pSnake;
//初始化游戏
void GameStart(pSnake ps);
//运行游戏
void GameRun(pSnake ps);
//移动
void SnakeMove(pSnake ps);
//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps);
//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps);
//文件保存最高分
int SaveScore(pSnake ps);
//文件下载最高分
int LoadScore(pSnake ps);
//定位光标
void set_pos(short x, short y);
下面我们对游戏的主要三部分进行讲解:
//初始化游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏--善后工作--释放空间
GameEnd(&snake);
4.初始化游戏GameStart(&snake); 内容讲解
主要分为四部分:
//1.打印环境 + 2.功能介绍
WelcomeToGame();
//3.绘制地图□
CreatMap();
//4.创建蛇
CreatSnake(ps);
//5.创建食物
CreatFood(ps);
加上隐藏光标 HideCurSor();
//初始化游戏
void GameStart(pSnake ps)
{
/*
0.设置窗口大小光标隐藏
1.打印环境
2.功能介绍
3.绘制地图
4.创建蛇
5.创建食物
6.设置游戏的相关信息
*/
//0.设置窗口大小光标隐藏 + 6.设置游戏的相关信息
system("mode con cols=100 lines=30");//设置大小
system("title 贪吃蛇");//设置名字
HideCurSor();//隐藏光标
//1.打印环境 + 2.功能介绍
WelcomeToGame();
//3.绘制地图□
CreatMap();
//4.创建蛇
CreatSnake(ps);
//5.创建食物
CreatFood(ps);
}
1)隐藏光标 HideCurSor();
其中 GetStdHandle(STD_OUTPUT_HANDLE) 函数就是获取控制台句柄函数,其返回值就是获取到的句柄,所以直接用
//隐藏光标
void HideCurSor()
{
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//获取光标信息
cursor_info.bVisible = false;//修改光标信息--透明
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//设置光标信息--隐藏光标
}
2)打印环境 + 功能介绍WelcomeToGame();
这 wprintf() 函数就是本地化设置之后的专属打印函数,用于打印本地编码字符,所以要在字符前面加上 L“ ” 来表明打印本地字符(一个汉字占横向两个)。
//1.打印环境 + 2.功能介绍
void WelcomeToGame()
{
set_pos(40, 14);//定位光标
wprintf(L"欢迎来到贪吃蛇小游戏\n");
set_pos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清屏
set_pos(25, 14);//定位光标
wprintf(L"用↑,↓,←,→来控制蛇的移动,按F3加速,按F4减速\n");
set_pos(40, 16);//定位光标
wprintf(L"加速可以获得更高的分数\n");
set_pos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//暂停程序
}
3)绘制地图CreatMap();
打印宽字符用 wprintf() 用法与 printf() 类似
//3.绘制地图□
void CreatMap()
{
//上
for (int i = 0; i < col; i++)
wprintf(L"%lc", WALL);
//下
set_pos(0, row - 1);
for (int i = 0; i < col; i++)
wprintf(L"%lc", WALL);
//左
for (int i = 1; i < row - 1; i++)
{
set_pos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 1; i < row - 1; i++)
{
set_pos(2 * col - 2, i);
wprintf(L"%lc",WALL);
}
}
4)创建蛇CreatSnake(ps);
蛇身采用单向链表的数据结构来储存
void SnakePrintf(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
while (pcur)
{
set_pos(pcur->x, pcur->y);//光标定位
wprintf(L"%lc", BODY);
pcur = pcur->next;//找到下一个身体节点
}
}
//4.创建蛇
void CreatSnake(pSnake ps)
{
pSnakeNode pcur = NULL;//蛇身节点指针
for (int i = 0; i < 5; i++)//初始化身体5节长
{
pcur = (pSnakeNode)malloc(sizeof(SnakeNode));//动态空间申请
if (pcur == NULL)
{
perror("CreatSnake()::malloc()");//空间申请是否出现错误判断
return;
}
pcur->next = NULL;//下一个节点
pcur->x = POS_X + 2 * i;//记录初始化位置坐标
pcur->y = POS_Y;
//头插法插入链表
if (ps->_pSnakeHead == NULL)//头节点是否存在的判断,即第一个申请的节点作为头节点
{
ps->_pSnakeHead = pcur;
}
else//头节点已经存在,对下一个节点进行指向
{
pcur->next = ps->_pSnakeHead;
ps->_pSnakeHead = pcur;
}
}
SnakePrintf(ps);//打印蛇函数
//蛇总信息初始化
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//ms
ps->_status = OK;
ps->_MAX = 0;
LoadScore(ps);//读取文件保存的最高分
}
//读取文件保存的最高分
int LoadScore(pSnake ps)
{
FILE* pf = fopen("MAX_SCORE.txt", "rb");//二进制只读打开文件
if (pf == NULL)
{
perror("fopen fail");//判断打开文件是否出现错误
return 0;
}
fread(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//读取最高分信息
fclose(pf);//关闭文件
pf = NULL;//避免野指针
return 1;
}
5)创建食物CreatFood(ps);
这里主要是食物生成的横坐标问题,要为偶数,因为我们从0开始每两格打印一个字符,所以x表示打印字符的起始位置(偶数)。
//5.创建食物
void CreatFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % (2 * col -5) + 2;//随机生成x坐标--在墙体内--【2,54】--56,57为墙
y = rand() % (row - 2) + 1;//随机生成y坐标--在墙体内--【1,24】--25为墙
} while (x%2);//保证生成的x坐标是偶数
pSnakeNode pcur = ps->_pSnakeHead;
while (pcur)//判断食物是否与蛇体重叠
{
if (x == pcur->x || y == pcur->y)
{
goto again;//重叠则重新生成食物
}
pcur = pcur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请食物空间
if (pFood == NULL)
{
perror("CreatFood()::malloc()");//判断空间申请是否出现错误
return;
}
//食物节点也用的是蛇身节点结构体
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
set_pos(pFood->x, pFood->y);//光标定位
wprintf(L"%lc", FOOD);//打印食物
ps->_pFood = pFood;//记录食物到蛇总信息
}
5.运行游戏GameRun(&snake);内容讲解
注意食物也是动态空间,也要申请和释放
注意对尾节点的释放,注意原来的蛇要清除
其他细节见注释
//打印提示信息
void PrintHelp()
{
set_pos(64, 14);
wprintf(L"%ls\n", L"不能穿墙,不能咬到自己");
set_pos(64, 15);
wprintf(L"%ls\n", L"用↑,↓,←,→来控制蛇的移动");
set_pos(64, 16);
wprintf(L"%ls\n", L"按F3加速,按F4减速");
set_pos(64, 17);
wprintf(L"%ls\n", L"按ESC退出,按空格暂停");
}
//暂停游戏
void Pause()
{
while (1)
{
Sleep(200);//ms
if (KEY_PRESS(VK_SPACE))break;//当空格再次按下时继续游戏
}
}
//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps)
{
if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)//头节点坐标与食物坐标比较
{
return 1;
}
return 0;
}
//清除屏幕上的蛇--方便移动后的打印
void ClearSnake(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
while (pcur)
{
set_pos(pcur->x, pcur->y);
printf(" ");//打印两格空格
pcur = pcur->next;
}
}
//吃到食物
void EatFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
ClearSnake(ps);//清楚屏幕上的蛇
pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
ps->_pSnakeHead = pn;//头节点移动到的位置
//吃食物,身体变长--尾节点位置不变,释放食物空间
free(ps->_pFood);//释放食物申请的空间
ps->_pFood = NULL;//避免野指针
CreatFood(ps);//重新创建食物
ps->_score += ps->_food_weight;//加分
}
//没有食物
void NoFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
ClearSnake(ps);//清楚屏幕上的蛇
pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
ps->_pSnakeHead = pn;//头节点移动到的位置
//没有食物,身体不变长,要改变尾节点位置,并释放原来尾节点的空间
pSnakeNode pcur = ps->_pSnakeHead;
pSnakeNode del = pcur->next;
while (del->next)//循环找到原来尾节点
{
pcur = del;
del = del->next;
}
free(del);
del = pcur->next = NULL;//防止野指针--将原倒数第二个节点设为尾节点
}
//移动
void SnakeMove(pSnake ps)
{
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("CreatSnake()::malloc()");
return;
}
switch (ps->_dir)//判断方向,并对移动到的下一个节点的坐标进行设置
{
case UP:
pNextNode->x = ps->_pSnakeHead->x;
pNextNode->y = ps->_pSnakeHead->y - 1;
break;
case DOWN:
pNextNode->x = ps->_pSnakeHead->x;
pNextNode->y = ps->_pSnakeHead->y + 1;
break;
case LEFT:
pNextNode->x = ps->_pSnakeHead->x - 2;
pNextNode->y = ps->_pSnakeHead->y;
break;
case RIGHT:
pNextNode->x = ps->_pSnakeHead->x + 2;
pNextNode->y = ps->_pSnakeHead->y;
break;
}
//移动后判断是否吃到食物
if (is_Food(pNextNode, ps))//吃食物
{
EatFood(pNextNode, ps);
}
else//没食物
{
NoFood(pNextNode, ps);
}
SnakePrintf(ps);//再打印蛇身产生移动
}
//判断是否撞墙
void KillByWall(pSnake ps)
{
if (ps->_pSnakeHead->x == 0 || ps->_pSnakeHead->x == 56 ||
ps->_pSnakeHead->y == 0 || ps->_pSnakeHead->y == 25)
{
ps->_status = KILL_BY_WALL;//撞墙则改变游戏状态为 KILL_BY_WALL
}
}
//判断是否吃到自己
void KillBySelf(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead->next;//找到头节点
while (pcur)
{
if (pcur->x == ps->_pSnakeHead->x && pcur->y == ps->_pSnakeHead->y)
{
ps->_status = KILL_BY_SELF;//吃到自己则改变游戏状态为 KILL_BY_SELF
break;
}
pcur = pcur->next;
}
}
//运行游戏
void GameRun(pSnake ps)
{
//打印提示信息
PrintHelp();
do
{
set_pos(64, 6);
printf("历史最高分数:%d\n", ps->_MAX);
set_pos(64, 10);
printf("当前得分:%d\n", ps->_score);
set_pos(64, 11);
printf("当前食物分数为:%02d\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))//退出
{
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))//加速
{
if (ps->_sleep_time > 80)//速度上限
{
ps->_sleep_time -= 30;//减少睡眠时间--即打印移动后的蛇的间隔实间
ps->_food_weight += 2;//加速--食物分值提高
}
}
else if (KEY_PRESS(VK_F4))//减速
{
if (ps->_sleep_time < 320)//速度下限
{
ps->_sleep_time += 30;//增加睡眠时间--即打印移动后的蛇的间隔实间
ps->_food_weight -= 2;//减速--食物分值降低
}
}
SnakeMove(ps);//移动蛇
ps->_MAX = ps->_score > ps->_MAX ? ps->_score : ps->_MAX;//判断最高分是否更新
KillByWall(ps);//判断是否撞墙
KillBySelf(ps);//判断是否吃到自己
Sleep(ps->_sleep_time);//睡眠时间
} while (ps->_status == OK);//判断游戏状态
}
6.结束游戏--善后工作--释放空间GameEnd(&snake);内容讲解
三部分:
WhichCause(ps);//游戏结束原因
SaveScore(ps);//更新最高分到文件
DistorySnake(&ps);//销毁蛇链表
//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps)
{
WhichCause(ps);//游戏结束原因
SaveScore(ps);//更新最高分到文件
DistorySnake(&ps);//销毁蛇链表
}
1)WhichCause(ps);游戏结束原因
找到退出原因并打印相应提示信息
void WhichCause(pSnake ps)
{
set_pos(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;
}
}
2)SaveScore(ps);更新最高分到文件
//更新最高分到文件
int SaveScore(pSnake ps)
{
FILE* pf = fopen("MAX_SCORE.txt", "wb");//二进制只写打开文件
if (pf == NULL)
{
perror("fopen fail");//判断打开文件是否错误
return 0;
}
fwrite(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//更新最高分到文件
fclose(pf);//关闭文件
pf = NULL;//避免野指针
return 1;
}
3)DistorySnake(&ps);销毁蛇链表
//销毁链表--释放空间
void DistorySnake(pSnake* ps)
{
pSnakeNode del = (*ps)->_pSnakeHead;//找到头节点
pSnakeNode pcur = del->next;//找到销毁节点的下一个节点
while (pcur)//从头节点往后依次销毁
{
free(del);
del = pcur;
pcur = pcur->next;
}
(*ps)->_pSnakeHead = del = pcur = NULL;//避免野指针
*ps = NULL;//蛇总信息指针置空--避免野指针--这也是传入二级指针的原因,对这个指针进行修改
}
四、代码与程序展示
0.程序展示
1.text.c 文件
#include"snake.h"
//游戏逻辑
void game()
{
char ch;
do
{
system("cls");
Snake snake = { 0 };//创建贪吃蛇
GameStart(&snake);//初始化游戏
GameRun(&snake);//运行游戏
GameEnd(&snake);//结束游戏--善后工作--释放空间
set_pos(20, 15);
printf("再来一局吗?(Y/N)(y/n):");
ch = getchar();//消除缓冲区的回车字符
while (getchar() != '\n');//消除输入的多余字符
} while (ch == 'y' || ch == 'Y');
set_pos(0, row);//定位光标--打印结束语
}
//主函数逻辑
int main()
{
setlocale(LC_ALL, "");//本地化设置
srand((unsigned int)time(NULL));//生成随机数
game();//进入游戏
return 0;
}
2.snake.h 头文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
/*
控制台坐标--windous头文件中
typedef struct _COORD
{
short x;
short y;
}COORD;
*/
/*
Windows_API
GetStdHandle--获取控制台句柄
GetConsoleCursorInfo--获取光标信息
SetConsoleCursorInfo--设置光标信息
SetConsoleCursorPosition--设置光标位置
GetAsyncKeyState--获取按键状态
CONSOLE_CURSOR_INFO--光标结构体
*/
/*
设置控制台的相关属性
system("mode con cols=100 lines=30");//设置大小
system("title 贪吃蛇");//设置名字
system("pause");//暂停程序
system("color 74");//设置颜色:两位数---十位是背景色---个位是前景色
*/
/*
颜色对应表
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 浅绿色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
*/
/*
y = 2x
---------------------->x col
|
|
|
|
|
|
|
\/
y
row
*/
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define row 26//坐标轴定义
#define col 29
#define POS_X 24
#define POS_Y 5
#define WALL L'□'//中国地区编码方式 L'字符' 表示宽字符,即横宽两格
#define BODY L'●'
#define FOOD L'◆'
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)//获取按键状态
typedef enum DIRECTION//方向
{
UP,//上
DOWN,//下
LEFT,//左
RIGHT//右
}direction;
typedef enum GAME_STATUS//游戏状态
{
OK,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//吃自己
END_NORMAL//退出
}game_status;
typedef struct SnakeNode//蛇身体
{
int x, y;//坐标
struct SnakeNode* next;//指向下一个节点指针
}SnakeNode, * pSnakeNode;
typedef struct Snake//蛇的信息
{
pSnakeNode _pSnakeHead;//头节点
pSnakeNode _pFood;//食物
direction _dir;//移动方向
game_status _status;//游戏状态
int _food_weight;//一个食物的分数
int _score;//总得分
int _sleep_time;//移动速度--睡眠时间
int _MAX;//最高得分记录
}Snake, * pSnake;
//初始化游戏
void GameStart(pSnake ps);
//运行游戏
void GameRun(pSnake ps);
//移动
void SnakeMove(pSnake ps);
//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps);
//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps);
//文件保存最高分
int SaveScore(pSnake ps);
//文件下载最高分
int LoadScore(pSnake ps);
//定位光标
void set_pos(short x, short y);
3.snake.c 文件
#include"snake.h"
//隐藏光标
void HideCurSor()
{
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//获取光标信息
cursor_info.bVisible = false;//修改光标信息--透明
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);//设置光标信息--隐藏光标
}
//光标位置设置
void set_pos(short x, short y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
COORD pos = { x,y };//光标位置信息结构体--Windows已经定义好了
SetConsoleCursorPosition(houtput, pos);//定位光标
}
//1.打印环境 + 2.功能介绍
void WelcomeToGame()
{
set_pos(40, 14);//定位光标
wprintf(L"欢迎来到贪吃蛇小游戏\n");
set_pos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清屏
set_pos(25, 14);//定位光标
wprintf(L"用↑,↓,←,→来控制蛇的移动,按F3加速,按F4减速\n");
set_pos(40, 16);//定位光标
wprintf(L"加速可以获得更高的分数\n");
set_pos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//暂停程序
}
//3.绘制地图□
void CreatMap()
{
//上
for (int i = 0; i < col; i++)
wprintf(L"%lc", WALL);
//下
set_pos(0, row - 1);
for (int i = 0; i < col; i++)
wprintf(L"%lc", WALL);
//左
for (int i = 1; i < row - 1; i++)
{
set_pos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 1; i < row - 1; i++)
{
set_pos(2 * col - 2, i);
wprintf(L"%lc",WALL);
}
}
void SnakePrintf(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
while (pcur)
{
set_pos(pcur->x, pcur->y);//光标定位
wprintf(L"%lc", BODY);
pcur = pcur->next;//找到下一个身体节点
}
}
//4.创建蛇
void CreatSnake(pSnake ps)
{
pSnakeNode pcur = NULL;//蛇身节点指针
for (int i = 0; i < 5; i++)//初始化身体5节长
{
pcur = (pSnakeNode)malloc(sizeof(SnakeNode));//动态空间申请
if (pcur == NULL)
{
perror("CreatSnake()::malloc()");//空间申请是否出现错误判断
return;
}
pcur->next = NULL;//下一个节点
pcur->x = POS_X + 2 * i;//记录初始化位置坐标
pcur->y = POS_Y;
//头插法插入链表
if (ps->_pSnakeHead == NULL)//头节点是否存在的判断,即第一个申请的节点作为头节点
{
ps->_pSnakeHead = pcur;
}
else//头节点已经存在,对下一个节点进行指向
{
pcur->next = ps->_pSnakeHead;
ps->_pSnakeHead = pcur;
}
}
SnakePrintf(ps);//打印蛇函数
//蛇总信息初始化
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//ms
ps->_status = OK;
ps->_MAX = 0;
LoadScore(ps);//读取文件保存的最高分
}
///5.创建食物
void CreatFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % (2 * col - 5) + 2;//随机生成x坐标--在墙体内--【2,54】--56,57为墙
y = rand() % (row - 2) + 1;//随机生成y坐标--在墙体内--【1,24】--25为墙
} while (x % 2);//保证生成的x坐标是偶数
pSnakeNode pcur = ps->_pSnakeHead;
while (pcur)//判断食物是否与蛇体重叠
{
if (x == pcur->x || y == pcur->y)
{
goto again;//重叠则重新生成食物
}
pcur = pcur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//申请食物空间
if (pFood == NULL)
{
perror("CreatFood()::malloc()");//判断空间申请是否出现错误
return;
}
//食物节点也用的是蛇身节点结构体
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
set_pos(pFood->x, pFood->y);//光标定位
wprintf(L"%lc", FOOD);//打印食物
ps->_pFood = pFood;//记录食物到蛇总信息
}
//初始化游戏
void GameStart(pSnake ps)
{
/*
0.设置窗口大小光标隐藏
1.打印环境
2.功能介绍
3.绘制地图
4.创建蛇
5.创建食物
6.设置游戏的相关信息
*/
//0.设置窗口大小光标隐藏 + 6.设置游戏的相关信息
system("mode con cols=100 lines=30");//设置大小
system("title 贪吃蛇");//设置名字
HideCurSor();
//1.打印环境 + 2.功能介绍
WelcomeToGame();
//3.绘制地图□
CreatMap();
//4.创建蛇
CreatSnake(ps);
//5.创建食物
CreatFood(ps);
}
//打印提示信息
void PrintHelp()
{
set_pos(64, 14);
wprintf(L"%ls\n", L"不能穿墙,不能咬到自己");
set_pos(64, 15);
wprintf(L"%ls\n", L"用↑,↓,←,→来控制蛇的移动");
set_pos(64, 16);
wprintf(L"%ls\n", L"按F3加速,按F4减速");
set_pos(64, 17);
wprintf(L"%ls\n", L"按ESC退出,按空格暂停");
}
//暂停游戏
void Pause()
{
while (1)
{
Sleep(200);//ms
if (KEY_PRESS(VK_SPACE))break;//当空格再次按下时继续游戏
}
}
//判断是否吃到食物
int is_Food(pSnakeNode pn, pSnake ps)
{
if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)//头节点坐标与食物坐标比较
{
return 1;
}
return 0;
}
//清除屏幕上的蛇--方便移动后的打印
void ClearSnake(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead;//找到头节点
while (pcur)
{
set_pos(pcur->x, pcur->y);
printf(" ");//打印两格空格
pcur = pcur->next;
}
}
//吃到食物
void EatFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
ClearSnake(ps);//清楚屏幕上的蛇
pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
ps->_pSnakeHead = pn;//头节点移动到的位置
//吃食物,身体变长--尾节点位置不变,释放食物空间
free(ps->_pFood);//释放食物申请的空间
ps->_pFood = NULL;//避免野指针
CreatFood(ps);//重新创建食物
ps->_score += ps->_food_weight;//加分
}
//没有食物
void NoFood(pSnakeNode pn, pSnake ps)//pn是头移动到的位置
{
ClearSnake(ps);//清楚屏幕上的蛇
pn->next = ps->_pSnakeHead;//头移动到的节点变成头节点--所以第二个节点就是原来的头节点
ps->_pSnakeHead = pn;//头节点移动到的位置
//没有食物,身体不变长,要改变尾节点位置,并释放原来尾节点的空间
pSnakeNode pcur = ps->_pSnakeHead;
pSnakeNode del = pcur->next;
while (del->next)//循环找到原来尾节点
{
pcur = del;
del = del->next;
}
free(del);
del = pcur->next = NULL;//防止野指针--将原倒数第二个节点设为尾节点
}
//移动
void SnakeMove(pSnake ps)
{
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("CreatSnake()::malloc()");
return;
}
switch (ps->_dir)//判断方向,并对移动到的下一个节点的坐标进行设置
{
case UP:
pNextNode->x = ps->_pSnakeHead->x;
pNextNode->y = ps->_pSnakeHead->y - 1;
break;
case DOWN:
pNextNode->x = ps->_pSnakeHead->x;
pNextNode->y = ps->_pSnakeHead->y + 1;
break;
case LEFT:
pNextNode->x = ps->_pSnakeHead->x - 2;
pNextNode->y = ps->_pSnakeHead->y;
break;
case RIGHT:
pNextNode->x = ps->_pSnakeHead->x + 2;
pNextNode->y = ps->_pSnakeHead->y;
break;
}
//移动后判断是否吃到食物
if (is_Food(pNextNode, ps))//吃食物
{
EatFood(pNextNode, ps);
}
else//没食物
{
NoFood(pNextNode, ps);
}
SnakePrintf(ps);//再打印蛇身产生移动
}
//判断是否撞墙
void KillByWall(pSnake ps)
{
if (ps->_pSnakeHead->x == 0 || ps->_pSnakeHead->x == 56 ||
ps->_pSnakeHead->y == 0 || ps->_pSnakeHead->y == 25)
{
ps->_status = KILL_BY_WALL;//撞墙则改变游戏状态为 KILL_BY_WALL
}
}
//判断是否吃到自己
void KillBySelf(pSnake ps)
{
pSnakeNode pcur = ps->_pSnakeHead->next;//找到头节点
while (pcur)
{
if (pcur->x == ps->_pSnakeHead->x && pcur->y == ps->_pSnakeHead->y)
{
ps->_status = KILL_BY_SELF;//吃到自己则改变游戏状态为 KILL_BY_SELF
break;
}
pcur = pcur->next;
}
}
//运行游戏
void GameRun(pSnake ps)
{
//打印提示信息
PrintHelp();
do
{
set_pos(64, 6);
printf("历史最高分数:%d\n", ps->_MAX);
set_pos(64, 10);
printf("当前得分:%d\n", ps->_score);
set_pos(64, 11);
printf("当前食物分数为:%02d\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))//退出
{
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))//加速
{
if (ps->_sleep_time > 80)//速度上限
{
ps->_sleep_time -= 30;//减少睡眠时间--即打印移动后的蛇的间隔实间
ps->_food_weight += 2;//加速--食物分值提高
}
}
else if (KEY_PRESS(VK_F4))//减速
{
if (ps->_sleep_time < 320)//速度下限
{
ps->_sleep_time += 30;//增加睡眠时间--即打印移动后的蛇的间隔实间
ps->_food_weight -= 2;//减速--食物分值降低
}
}
SnakeMove(ps);//移动蛇
ps->_MAX = ps->_score > ps->_MAX ? ps->_score : ps->_MAX;//判断最高分是否更新
KillByWall(ps);//判断是否撞墙
KillBySelf(ps);//判断是否吃到自己
Sleep(ps->_sleep_time);//睡眠时间
} while (ps->_status == OK);//判断游戏状态
}
//更新最高分到文件
int SaveScore(pSnake ps)
{
FILE* pf = fopen("MAX_SCORE.txt", "wb");//二进制只写打开文件
if (pf == NULL)
{
perror("fopen fail");//判断打开文件是否错误
return 0;
}
fwrite(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//更新最高分到文件
fclose(pf);//关闭文件
pf = NULL;//避免野指针
return 1;
}
//读取文件保存的最高分
int LoadScore(pSnake ps)
{
FILE* pf = fopen("MAX_SCORE.txt", "rb");//二进制只读打开文件
if (pf == NULL)
{
perror("fopen fail");//判断打开文件是否出现错误
return 0;
}
fread(&ps->_MAX, sizeof(ps->_MAX), 1, pf);//读取最高分信息
fclose(pf);//关闭文件
pf = NULL;//避免野指针
return 1;
}
//销毁链表--释放空间
void DistorySnake(pSnake* ps)
{
pSnakeNode del = (*ps)->_pSnakeHead;//找到头节点
pSnakeNode pcur = del->next;//找到销毁节点的下一个节点
while (pcur)//从头节点往后依次销毁
{
free(del);
del = pcur;
pcur = pcur->next;
}
(*ps)->_pSnakeHead = del = pcur = NULL;//避免野指针
*ps = NULL;//蛇总信息指针置空--避免野指针--这也是传入二级指针的原因,对这个指针进行修改
}
void WhichCause(pSnake ps)
{
set_pos(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;
}
}
//结束游戏--善后工作--释放空间
void GameEnd(pSnake ps)
{
WhichCause(ps);//游戏结束原因
SaveScore(ps);//更新最高分到文件
DistorySnake(&ps);//销毁蛇链表
}