1.前言
这个版本使用了EasyX图形库,使动画效果更加丰富,更加接近真实的小游戏。当然这个“小游戏”并不是真正意义上的小游戏,只是个人对C语言的一些理解与应用。本人水平不高,通过博客来分享自己的学习成果,也算是一种复习。
如果大家不知道什么是EasyX图形库,或者说不知道怎么安装、怎么使用,大家可以去网络上搜索一下。安装过程非常简单,使用起来仅仅针对于贪吃蛇这个项目而言,只需要引头文件,和使用头文件里面的库函数。与#include <stdio.h>然后使用printf函数没什么区别。
需要注意的是,EasyX图形库之适用于C++,但我们可以用C语言的语法。意思就是说,我们需要创建的文件必须是cpp的格式,但我们可以用C的语法。
2.游戏效果
在1.0版本中,我们是在控制台窗口下运行的。并且运用到数组。通过数组的状态来描绘空地、食物、蛇的。并且黑框框影响美观、并不好看。并且因为用到了system("cls")清屏,所以屏幕会一卡一卡抽搐,非常影响视觉。
但是在2.0版本中,我们运用了EasyX图形库,这就使得我们不通过控制台那个黑框框来输出图像,而是用EasyX图形库里面的库函数。上下两个GIF动图都是60FPS,但是不难发现2.0版本无论是动画、还是灵敏度,都要优于1.0。
3.设计思路
3.1界面设置
我们先将需要的头文件写在head.h头文件底下。
#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>
随后我们在主函数main.cpp底下操作。
#include "head.h"
int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏//防闪退getchar();closegraph();return 0;
}
那么我们就得到:
3.2绘制一条蛇出来
在绘制蛇之前先讲一个知识点。
这个窗口是有 x、y轴的,并且值增加的方向跟箭头同向。意思就是说,越往右边,x越大;越往下面,y越大。
由此分析,蛇不能够像1.0版本那样用数组来描绘状态了,需要用坐标来描绘蛇。
我们在头文件head.h下定义蛇的结构体,因为需要使用坐标,也同时定义一个坐标结构体:
#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>#define NUM 200//默认蛇有200个坐标//坐标的结构体
struct Coor
{int x;int y;
};//蛇的结构体
struct Snake
{//蛇结构包括int len;//长度int direc;//方向Coor cr[NUM];//坐标//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};
现在蛇的结构已经弄好,接下来就要初始化,也就是把蛇放在哪个位置。
我们在源文件mian.cpp中进入初始化:
#include "head.h"
int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化//防闪退getchar();closegraph();return 0;
}
在头文件head.h中的部分函数声明与宏定义:
#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>#define NUM 200//默认蛇有200个坐标//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77//坐标的结构体
struct Coor
{int x;int y;
};//蛇的结构体
struct Snake
{//蛇结构包括int len;//长度int direc;//方向Coor cr[NUM];//坐标//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};void Init_Game();//初始化函数声明
现在需要在源文件function.cpp中配置初始化函数:
#include "head.h"Snake snake;//创建蛇的结构体变量
void Init_Game()
{snake.len = 3;//初始化长度snake.direc = RIGHT;//初始化方向//初始化蛇头坐标snake.cr[0].x = 100;snake.cr[0].y = 100;//蛇身坐标snake.cr[1].x = 90;snake.cr[1].y = 100;snake.cr[2].x = 80;snake.cr[2].y = 100;
}
现在有了蛇的详细信息,就要开始着手在窗口把蛇绘制出来了,我们从主函数进入绘制函数:
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化Draw_Game();//绘制//防闪退getchar();closegraph();return 0;
}
要在头文件中进行函数声明,但是这里省略。
现在需要在function.cpp中配置绘制函数:
void Draw_Game()
{for (int i = 0; i < snake.len; i++){if (i == 0)//如果是蛇头{setfillcolor(RED);//填充颜色为红色//绘制实心矩形,填充颜色为红色fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了//四个参数为: x坐标 y坐标 宽 高}elserectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形}
}
那么绘制出来的效果就是这样:
3.3蛇移动的实现
我们在初始化中,让蛇的移动方向为右。
现在我们需要分析蛇是如何移动的。
我们现在需要通过源文件main.cpp进入移动函数。注意:移动与绘制,是一个循环过程。
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化while (1){Move_Snake();//移动蛇Draw_Game();//绘制Sleep(100);}//防闪退getchar();closegraph();return 0;
}
在头文件head.h中要引用Sleep函数的头文件,以及移动蛇的函数声明。这里省略。
最后在源文件function.cpp中配置移动蛇函数:
void Move_Snake()
{cleardevice();//以当前背景色清屏for (int i = snake.len-1; i > 0; i--){snake.cr[i].x = snake.cr[i - 1].x;snake.cr[i].y = snake.cr[i - 1].y;}//数组下标是从 0~n-1 的,所以从i=snake.len-1开始。为什么i要>0而不是>=0//是因为在循环里面的语句中,有i-1的操作。最后一次是把snake.cr[0]的坐标给snake.cr[1],如果写>=0,就会产生把 snake.cr[-1]的坐标给snake.cr[0]//蛇头的坐标没有了,现在要产生新的蛇头坐标switch (snake.direc){case UP:snake.cr[0].y -= SIZE;break;case DOWN:snake.cr[0].y += SIZE;break;case LEFT:snake.cr[0].x -= SIZE;break;case RIGHT:snake.cr[0].x += SIZE;break;}
}
现在实现了蛇向右移动的功能,那么效果是这样的:
现在我们需要控制蛇的方向。我们从源文件main.cpp进入控制蛇的函数:
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化while (1){if (_kbhit())//如果键盘被敲击就进入控制方向函数Change_Move();//控制蛇else{Move_Snake();//移动蛇Draw_Game();//绘制Sleep(100);}}//防闪退getchar();closegraph();return 0;
}
我们需要在头文件head.h中定义_kbhit函数的头文件以及控制蛇的函数声明。这里省略。
现在要在源文件function.cpp中配置控制蛇函数:
void Change_Move()
{int key = 0;key = _getch();//接收键盘值switch (key){case UP:if (snake.direc != DOWN)//如果蛇正在往下,就不能改变方向。snake.direc = UP;break;case DOWN:if (snake.direc != UP)//如果蛇正在往上,就不能改变方向。snake.direc = DOWN;break;case LEFT:if (snake.direc != RIGHT)//如果蛇正在往右,就不能改变方向snake.direc = LEFT;break;case RIGHT:if (snake.direc != LEFT)//如果蛇正在往左,就不能改变防线snake.direc = RIGHT;break;}
}
那么实现的效果是这样的:
3.4食物的生成
食物的生成很简单:第一次初始化生成,我们可以在初始化函数中完成。后续就是蛇吃掉食物后再随机生成。
需要注意的是:我们的蛇是以10为单位移动的,并且每一节的大小都是10*10。我们的食物也要匹配蛇的大小。
判断食物被是否被吃掉非常简单,只要判断蛇头与食物的坐标是否重合就行。
但是我们难就难在定义食物。
在1.0版本中,我们用数组描绘食物的状态。
但是在本版本中,需要使用结构体,因为食物也包含坐标。
所以这里在头文件head.h中定义食物的结构体(包含上面省略的头文件和函数声明):
#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>
#include <windows.h>//Sleep头文件
#include <conio.h>//kbhit、getch头文件
#define NUM 200//默认蛇有200个坐标//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77#define SIZE 10
//坐标的结构体
struct Coor
{int x;int y;
};//蛇的结构体
struct Snake
{//蛇结构包括int len;//长度int direc;//方向Coor cr[NUM];//坐标//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};//食物的结构体
struct Food
{//因为食物它永远只有一个,所以不需要数组int x;int y;int eat;//确定是否被吃
};
void Init_Game();//初始化函数声明
void Draw_Game();//绘制蛇函数声明
void Move_Snake();//蛇移动函数声明
void Change_Move();//控制蛇函数声明
我们要在源文件function.cpp中对食物初始化:
#include "head.h"Snake snake;//创建蛇的结构体变量
Food food;//创建食物的结构体变量
void Init_Game()
{snake.len = 3;//初始化长度snake.direc = RIGHT;//初始化方向//初始化蛇头坐标snake.cr[0].x = 100;snake.cr[0].y = 100;//蛇身坐标snake.cr[1].x = 90;snake.cr[1].y = 100;snake.cr[2].x = 80;snake.cr[2].y = 100;food.eat = 1;//初始化食物是被吃的}
在绘制之前我们需要定义一下食物的坐标在哪,也就是需要在源文件main.cpp中进入创建食物函数:
定义好了食物的坐标,现在又要回到绘制函数把食物绘制出来:
void Draw_Game()
{for (int i = 0; i < snake.len; i++){if (i == 0)//如果是蛇头{setfillcolor(RED);//填充颜色为红色//绘制实心矩形,填充颜色为红色fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了//四个参数为: x坐标 y坐标 宽 高}elserectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形}//绘制食物setfillcolor(GREEN);//填充颜色为绿色fillroundrect(food.x, food.y, food.x + SIZE, food.y + SIZE, 5, 5);//圆角矩形//6个参数为: x坐标 y坐标 宽 高 这两个是圆角的程度
}
此时的运行效果就是这样的:
3.5吃食物以及蛇身加长
这个部分非常简单。
我们先从源文件main.cpp中进入判断是否吃到食物函数:
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化srand((unsigned int)time(NULL));while (1){if (_kbhit())//如果键盘被敲击就进入控制方向函数Change_Move();//控制蛇else{Produce_Food();//创建食物Eat_Food();//判断食物是否被吃Move_Snake();//移动蛇Draw_Game();//绘制Sleep(100);}}//防闪退getchar();closegraph();return 0;
}
我们需要在头文件head.h下进行函数声明。这里省略。
随后在源文件function.cpp中配置是否被吃函数:
void Eat_Food()
{if (snake.cr[0].x == food.x && snake.cr[0].y == food.y)//如果蛇头的位置是食物{food.eat = 1;//食物的状态更新为被吃snake.len += 1;//蛇身长度+1}
}
3.6判断蛇头是否撞墙或自己
这个模块与1.0版本一摸一样,判断函数都需要一个变量来接收返回值。
我们在源文件main.cpp中观察逻辑:
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化srand((unsigned int)time(NULL));int n = 0;while (1){if (_kbhit())//如果键盘被敲击就进入控制方向函数Change_Move();//控制蛇else{Produce_Food();//创建食物Eat_Food();//判断食物是否被吃n = GameOver();//判断蛇头是否撞墙或自己if (n == 1)//返回1则撞墙或自己{closegraph();//关闭窗口printf("游戏结束\n");//控制台打印break;//结束循环}else{Move_Snake();//移动蛇Draw_Game();//绘制Sleep(100);}}}//防闪退getchar();closegraph();return 0;
}
我们需要在头文件head.h下进行函数声明。这里省略。
然后就是在源文件funtion.cpp中配置函数:
int GameOver()
{if (snake.cr[0].x < 0 || snake.cr[0].y < 0 || snake.cr[0].x>640 || snake.cr[0].y>480)//如果撞墙{return 1;//返回1}for (int i = 1; i < snake.len; i++) {if (snake.cr[0].x == snake.cr[i].x && snake.cr[0].y == snake.cr[i].y)//如果撞自己return 1;//返回1}return 0;//否则返回0
}
4.完整代码
4.1头文件head.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <easyx.h>//EasyX图形库头文件
#include <stdio.h>
#include <windows.h>//Sleep头文件
#include <conio.h>//kbhit、getch头文件
//srand函数需要的头文件
#include <stdlib.h>
#include <time.h>#define NUM 200//默认蛇有200个坐标//宏定义各个方向。与1.0不同的是,本版本使用方向键控制的
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77#define SIZE 10
//坐标的结构体
struct Coor
{int x;int y;
};//蛇的结构体
struct Snake
{//蛇结构包括int len;//长度int direc;//方向Coor cr[NUM];//坐标//一条蛇有N节,每节都有自己的坐标,所以定义一个坐标数组
};//食物的结构体
struct Food
{//因为食物它永远只有一个,所以不需要数组int x;int y;int eat;//确定是否被吃
};
void Init_Game();//初始化函数声明
void Draw_Game();//绘制蛇函数声明
void Move_Snake();//蛇移动函数声明
void Change_Move();//控制蛇函数声明
void Produce_Food();//创建食物的函数声明
void Eat_Food();//食物是否被吃的函数声明
int GameOver();//判断是否撞墙或自己的函
4.2源文件main.cpp
#include "head.h"int main()
{initgraph(640, 480);//初始化窗口大小,现在为640*480分辨率setbkcolor(RGB(255, 148, 209));//设置背景色cleardevice();//使用背景色清屏Init_Game();//初始化srand((unsigned int)time(NULL));int n = 0;while (1){if (_kbhit())//如果键盘被敲击就进入控制方向函数Change_Move();//控制蛇else{Produce_Food();//创建食物Eat_Food();//判断食物是否被吃n = GameOver();//判断蛇头是否撞墙或自己if (n == 1)//返回1则撞墙或自己{closegraph();//关闭窗口printf("游戏结束\n");//控制台打印break;//结束循环}else{Move_Snake();//移动蛇Draw_Game();//绘制Sleep(100);}}}//防闪退getchar();closegraph();return 0;
}
4.3源文件function.cpp
#include "head.h"Snake snake;//创建蛇的结构体变量
Food food;//创建食物的结构体变量
void Init_Game()
{snake.len = 3;//初始化长度snake.direc = RIGHT;//初始化方向//初始化蛇头坐标snake.cr[0].x = 100;snake.cr[0].y = 100;//蛇身坐标snake.cr[1].x = 90;snake.cr[1].y = 100;snake.cr[2].x = 80;snake.cr[2].y = 100;food.eat = 1;//初始化食物是被吃的}void Draw_Game()
{for (int i = 0; i < snake.len; i++){if (i == 0)//如果是蛇头{setfillcolor(RED);//填充颜色为红色//绘制实心矩形,填充颜色为红色fillrectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//这里的SIZE我在头文件head.h中宏定义为10了//四个参数为: x坐标 y坐标 宽 高}elserectangle(snake.cr[i].x, snake.cr[i].y, snake.cr[i].x + SIZE, snake.cr[i].y + SIZE);//绘制非实心矩形}//绘制食物setfillcolor(GREEN);//填充颜色为绿色fillroundrect(food.x, food.y, food.x + SIZE, food.y + SIZE, 5, 5);//圆角矩形//6个参数为: x坐标 y坐标 宽 高 这两个是圆角的程度
}void Move_Snake()
{cleardevice();//以当前背景色清屏for (int i = snake.len-1; i > 0; i--){snake.cr[i].x = snake.cr[i - 1].x;snake.cr[i].y = snake.cr[i - 1].y;}//数组下标是从 0~n-1 的,所以从i=snake.len-1开始。为什么i要>0而不是>=0//是因为在循环里面的语句中,有i-1的操作。最后一次是把snake.cr[0]的坐标给snake.cr[1],如果写>=0,就会产生把 snake.cr[-1]的坐标给snake.cr[0]//蛇头的坐标没有了,现在要产生新的蛇头坐标switch (snake.direc){case UP:snake.cr[0].y -= SIZE;break;case DOWN:snake.cr[0].y += SIZE;break;case LEFT:snake.cr[0].x -= SIZE;break;case RIGHT:snake.cr[0].x += SIZE;break;}
}void Change_Move()
{int key = 0;key = _getch();//接收键盘值switch (key){case UP:if (snake.direc != DOWN)//如果蛇正在往下,就不能改变方向。snake.direc = UP;break;case DOWN:if (snake.direc != UP)//如果蛇正在往上,就不能改变方向。snake.direc = DOWN;break;case LEFT:if (snake.direc != RIGHT)//如果蛇正在往右,就不能改变方向snake.direc = LEFT;break;case RIGHT:if (snake.direc != LEFT)//如果蛇正在往左,就不能改变防线snake.direc = RIGHT;break;}
}void Produce_Food()
{int func = 0;//用来判断食物的坐标是否与蛇的坐标重合if (food.eat == 1){while (1){food.x = rand() % 64 * 10;//rand()%64的区间在0~63,再*10可实现坐标以10为间隔生成坐标 0 10 20...food.y = rand() % 48 * 10;//rand()%48的区间在0~46,再*10可实现坐标以10为间隔生成坐标 0 10 20...for (int i = 0; i < snake.len; i++){if (food.x == snake.cr[i].x && food.y == snake.cr[i].y){func = 1;//如果但凡有坐标重合,func改变为1,表重合break;//退出for循环}}if (func == 0)//如果没有重合{food.eat = 0;//食物就要变为未被吃的状态break;}}}
}void Eat_Food()
{if (snake.cr[0].x == food.x && snake.cr[0].y == food.y)//如果蛇头的位置是食物{food.eat = 1;//食物的状态更新为被吃snake.len += 1;//蛇身长度+1}
}int GameOver()
{if (snake.cr[0].x < 0 || snake.cr[0].y < 0 || snake.cr[0].x>640 || snake.cr[0].y>480)//如果撞墙{return 1;//返回1}for (int i = 1; i < snake.len; i++) {if (snake.cr[0].x == snake.cr[i].x && snake.cr[0].y == snake.cr[i].y)//如果撞自己return 1;//返回1}return 0;//否则返回0
}