C语言编写贪吃蛇游戏
完成了一下c语言入门完成的必修课——贪吃蛇游戏。边学边做也搞了一天,所以记录一下自己的学习过程吧!
工具
因为我本来是用vscode进行编码的,发现不可以,查询才发现,vscode采用mingw,而制作图形界面的easyx使用的是支持的是vs的一些内容。
而我只是想制作贪吃蛇,对easyx没太多要求,能运行就行,加上安装vs并不复杂,并且需easyx装到vs里极其方变,下载然后打开,在install界面选择vs即可。就装了一个vs。
但使用过程只能说相当自闭。
vs:/zh-hans/vs/
easyx:/
游戏框架
需要一条可以根据按键运动的蛇,只需要使得蛇头在按键的作用下左右上下移动即可,蛇的其他部分只需要移动到前一节蛇的位置。需要不断出现的食物,当蛇碰到食物时,食物消失并刷新出新食物。当蛇碰到自己时游戏结束。
需要准备的元素:方向常量;蛇的结构体;食物的结构体
函数设定:初始化函数;绘图函数;按键控制函数;移动函数;吃食物判定函数;死亡判定;主函数;
最终结果:
游戏初始
可以先定义一个最大长度:
#define SNAKE_NUM 500//蛇的最大节数
struct Snake {int size;//蛇的长度int dir;//蛇的方向int speed;// 蛇的速度int score;POINT coor[SNAKE_NUM];//POINT是定义好的一个结构体,内含long x;long y}snake;
其中方向,需要进行枚举:
enum DIR {//表示方向UP,DOWN,LEFT,RIGHT,};
这与#define UP #define LEFT 是等价的,就是定义一些常量为了后续使用
而后是定义食物的结构:
struct FOOD {int x;int y;int r;bool flag; //食物是否被吃DWORD color;//食物颜色}food;
因为只有一个food所以没有必要用POINT进行二次引用了、
定义好了结构体后就要对其进行初始化了,即对这些内容进行赋值操作:
void GameInit(){initgraph(640, 480);snake.size = 3;snake.speed = 10;snake.dir = RIGHT;//给定一个方向snake.score= 0;//将蛇的位置先赋值了for (int i = snake.size - 1; i >= 0 ; i--) {snake.coor[i].x = 40 - 10 * i;snake.coor[i].y = 10;}//初始化食物srand(time(0));//记得需要#include <time.h>food.x = rand() % 640;food.y = rand() % 480;food.color = RGB(rand() % 256, rand() % 256, rand() % 256);food.r = rand() % 10 + 5;food.flag = true;}
记得一定要记好哪一个是宽度哪一个是高度,因为这个小细节改bug至少一个小时
绘图过程
绘图就是将移动蛇的过程动态化,这里需要说明的是,我们需要采用双缓冲绘图:
双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度。
如果不使用双缓冲绘图,那么很容易造成闪烁效果:
void GameDraw() {BeginBatchDraw();//双缓冲绘图setbkcolor(RGB(28,115,119));//设置背景颜色cleardevice();//清屏操作//绘制蛇setfillcolor(YELLOW);for (int i = 0; i < snake.size; i++) {solidcircle(snake.coor[i].x, snake.coor[i].y, 5);}EndBatchDraw();//绘制食物setfillcolor(food.color);if (food.flag) {solidcircle(food.x, food.y, food.r);}//写上分数char temp[20] = "";_stprintf_s(temp, "分数:%d",snake.score);outtextxy(20, 20, temp);EndBatchDraw();}
这里倒没什么好说的,唯一值得说一下的就是在outtextxy有蛮多限制的。
会报一个错误:
error C2665: “outtextxy”: 2 个重载中没有一个可以转换所有参数类型
为了使temp是在outtextxy是可接受的函数参数,需要在头文件的上方加上
#undef UNICODE
#undef _UNICODE
即将这个项目以 MBCS 编码编译
而我当时由于使用sprintf输入temp,vs不指出错误,只是单纯无法运行,去参考了easyx文档才改成了_stprintf_s解决了这个问题。废了相当长时间,简直拳头硬了。
蛇的移动
解决了绘图问题,我们就要开始真正移动我们的蛇了:
首先我们应该根据键盘操作确定蛇的移动方向:
键盘操作函数
void keyControl() {//判断有没有按键if (_kbhit()) {//头文件#include <conio.h>switch (_getch()) {case 'w':case 72:if (snake.dir != DOWN)snake.dir = UP; break;case 's':case 80:if (snake.dir != UP)snake.dir = DOWN; break;case 'a':case 75:if (snake.dir != RIGHT)snake.dir = LEFT; break;case 'd':case 77:if (snake.dir != LEFT)snake.dir = RIGHT; break;case ' '://游戏暂停while (1) {if (_getch() == ' ')return;}}}}
_kbhit()就是按键的判断,如果没有进行按键,则返回一个0值。故若有按键,则执行if中的内容。
话说我还是感觉离谱,因为要使用easyx才安装了vs,但是一些基本操作比如getchar他非得改成_getch,叹息。
加了一个无法朝相反方向移动的判定,即如果向前运动则不能直接改变方向为向后。
case后的数字代表小键盘操作。
额外加了游戏暂停判定。如果键入空格,则进入死循环中直到再次键入空格。
移动蛇函数:
void snakemove() {for (int i = snake.size - 1; i > 0; i--) {snake.coor[i] = snake.coor[i - 1];}switch (snake.dir) {case UP: {snake.coor[0].y -= snake.speed;if (snake.coor[0].y <= 0) {//超出上界snake.coor[0].y = 480;}break;}case DOWN: {snake.coor[0].y += snake.speed;if (snake.coor[0].y >= 480) {snake.coor[0].y = 0;}break;}case LEFT: {snake.coor[0].x -= snake.speed;if (snake.coor[0].x <=0) {snake.coor[0].x = 640;}break;}case RIGHT: {snake.coor[0].x += snake.speed;if (snake.coor[0].x >= 640) {snake.coor[0].x = 0;}break;}}}
我们可以直到,移动蛇其实就是移动蛇头,而后再将蛇身的每一节的位置改变为他的前一节。
第一个for循环就是为了第二个操作。
移动蛇头到边界时就使蛇从反方向出现,也可以当蛇碰到边界后死亡,这没什么问题。(但是食物生成偶尔在边界,我测试时为了不增加难度就改成了这样。手残党悲哀)
吃食物过程
void eatfood(){if (food.flag && (snake.coor[0].x >= food.x - food.r && snake.coor[0].x <= food.x + food.r) &&(snake.coor[0].y >= food.y - food.r && snake.coor[0].y <= food.y + food.r)) {food.flag = false;snake.size++;snake.score += 5;}//食物消失则重新初始化if (!food.flag){srand(GetTickCount());//GetTickCount()获取系统开机到现在所需毫秒数food.x = rand() % 640;food.y = rand() % 480;food.color = RGB(rand() % 256, rand() % 256, rand() % 256);food.r = rand() % 10 + 10;food.flag = true;}}
吃食物也很好理解,即蛇头处于食物的范围中即可。
让我有点自闭的是,如果我再重新生成食物的时候仍把随机种子设为time(0),食物的y坐标始终变化不大,基本可以不操作也可以吃到食物。最终我也没拍出这个bug,只能参考其他人,改成了GetTickCount()这个函数。
游戏终止过程
就是蛇吃到它自己,则终止。
void EatSelf(){for (int i = 1; i < snake.size; i++){if (snake.coor[0].x == snake.coor[i].x && snake.coor[0].y == snake.coor[i].y){outtextxy(200, 200, "Game over!");int a = _getch(); //保持停顿exit(0);}}}
为什么要用int a = _getch(); 这句话呢,因为vs需要一个承载_getch()返回值的变量,我真是服了。
最后加上主函数就可以了
int main() {GameInit();while (1) {GameDraw();snakemove();keyControl();eatfood();EatSelf();Sleep(50);}int b = _getch();closegraph();return 0;}
哦对了,需要说明一下Sleep,是使得执行过程停顿x毫秒,如果不休眠的话蛇向右运动的速度我们反应不过来,当然你也可以把休眠时间缩短,效果如图:
md不玩了,辣鸡游戏(并不)
那么最后展示一下整体代码吧:
整体代码
#undef UNICODE#undef _UNICODE#include <stdio.h>#include <graphics.h>#include <conio.h>#include <stdlib.h>#include <time.h>#define SNAKE_NUM 500//蛇的最大节数enum DIR {//表示方向UP,DOWN,LEFT,RIGHT,};//蛇的结构struct Snake {int size;//蛇的长度int dir;//蛇的方向int speed;// 蛇的速度int score;POINT coor[SNAKE_NUM];//POINT是定义好的一个结构体,内含long x;long y}snake;//食物的结构struct FOOD {int x;int y;int r;bool flag; //食物是否被吃DWORD color;//食物颜色}food;//数据初始化void GameInit() {//init初始化graph图形窗口 SHOWCONSOLE显示控制台initgraph(640, 480/*,SHOWCONSOLE*/ );snake.size = 3;snake.speed = 10;snake.dir = RIGHT;snake.score= 0;for (int i = snake.size - 1; i >= 0 ; i--) {snake.coor[i].x = 40 - 10 * i;snake.coor[i].y = 10;}//初始化食物srand(time(0));//GetTickCount()获取系统开机到现在所需毫秒数food.x = rand() % 640;food.y = rand() % 480;food.color = RGB(rand() % 256, rand() % 256, rand() % 256);food.r = rand() % 10 + 5;food.flag = true;}void GameDraw() {//双缓冲绘图BeginBatchDraw();//设置背景颜色setbkcolor(RGB(28,115,119));cleardevice();//绘制蛇setfillcolor(YELLOW);for (int i = 0; i < snake.size; i++) {solidcircle(snake.coor[i].x, snake.coor[i].y, 5);}//绘制食物EndBatchDraw();setfillcolor(food.color);if (food.flag) {solidcircle(food.x, food.y, food.r);}char temp[20] = "";_stprintf_s(temp, "分数:%d",snake.score);outtextxy(20, 20, temp);EndBatchDraw();}//移动蛇void snakemove() {for (int i = snake.size - 1; i > 0; i--) {snake.coor[i] = snake.coor[i - 1];}switch (snake.dir) {case UP: {snake.coor[0].y -= snake.speed;if (snake.coor[0].y <= 0) {//超出上界snake.coor[0].y = 480;}break;}case DOWN: {snake.coor[0].y += snake.speed;if (snake.coor[0].y >= 480) {snake.coor[0].y = 0;}break;}case LEFT: {snake.coor[0].x -= snake.speed;if (snake.coor[0].x <=0) {snake.coor[0].x = 640;}break;}case RIGHT: {snake.coor[0].x += snake.speed;if (snake.coor[0].x >= 640) {snake.coor[0].x = 0;}break;}}}//吃食物void eatfood(){if (food.flag && (snake.coor[0].x >= food.x - food.r && snake.coor[0].x <= food.x + food.r) &&(snake.coor[0].y >= food.y - food.r && snake.coor[0].y <= food.y + food.r)) {food.flag = false;snake.size++;snake.score += 5;}//食物消失则重新初始化if (!food.flag){srand(GetTickCount());//GetTickCount()获取系统开机到现在所需毫秒数food.x = rand() % 640;food.y = rand() % 480;food.color = RGB(rand() % 256, rand() % 256, rand() % 256);food.r = rand() % 10 + 10;food.flag = true;}}//通过按键改变蛇的移动方向void keyControl() {//判断有没有按键if (_kbhit()) {//头文件#include <conio.h>switch (_getch()) {case 'w':case 72:if (snake.dir != DOWN)snake.dir = UP; break;case 's':case 80:if (snake.dir != UP)snake.dir = DOWN; break;case 'a':case 75:if (snake.dir != RIGHT)snake.dir = LEFT; break;case 'd':case 77:if (snake.dir != LEFT)snake.dir = RIGHT; break;case ' '://游戏暂停while (1) {if (_getch() == ' ')return;}}}}void EatSelf(){for (int i = 1; i < snake.size; i++){if (snake.coor[0].x == snake.coor[i].x && snake.coor[0].y == snake.coor[i].y){outtextxy(200, 200, "Game over!");int a = _getch(); //保持停顿exit(0);}}}int main() {GameInit();while (1) {GameDraw();snakemove();keyControl();eatfood();EatSelf();Sleep(50);}int b = _getch();closegraph();return 0;}
总结
第一次完成一个小项目,乐趣还是很充分的,对c语言学习也更有兴趣了。
另一方面,做一个东西才发现自己的知识的薄弱,和平常做的各种题目的感觉完全不一样,太多太多你需要去学习的东西了,也就有了更大动力去学习吧。
但还是有一些遗憾,到底没有实现bgm的播放,死活放不出声音来,我也就没把代码逻辑放到上面了。
希望随着学习的深入能够回来再改改?
看有大佬15行写出一个贪吃蛇?我比他十倍还多一点,还是要继续学习呀,加油吧!