文章目录
前言
渴望知晓自由为何物的蛇蛇,适合刚进击完c语言,要拿去交大作业的童鞋。
总体说明:
首先引用easyx图形库换掉c语言单调的黑幕控制台输出,引入conio.h的_getch()获取键盘输入实现游戏快捷读取键盘操作,免去了回车键。
其余内容实现包括:设置游戏主页面菜单,可供选择三种游戏模式,规则玩法创新,如可发射口水(子弹)击中食物、碰撞减积分累计可判断死亡、操作方向与实际相反的乱向模式、既长身高长短又长体重胖瘦的人性化贪吃蛇、速度调节、暂停调节、皮肤更换(无氪金版)等等。
主界面还设有:皮肤设置(更改游戏内贪吃蛇皮肤)、查看帮助、游戏分数查看、退出游戏等选项,通过键盘操作自由切换界面。
(一)环境设置
通过c语言编写,使用的编译器是Dev-C++,VS在安装图形库下应该也可以运行。
主要是不想老是看见c语言那个黑色控制台,所以下载了针对C/C++的图形库easyx
不过也只涉及到一部分内容,具体的安装与操作见官网。(需要引入图形库才可运行)
(二)初步展示及思维导图
首先是游戏菜单界面,如下:
此外就是三种游戏模式、皮肤设置、查看帮助、游戏分数、退出游戏。
游戏规则写在帮助界面,如下:
游戏截图,如下:
关于游戏的思维导图,如下:
(三)放码过来
代码长度适合水代码的计算机大作业
Talk is cheap, show you the code.
自我感觉就是不断修改数据+绘图+修改数据+绘图+修改数据+绘图+…
1.头文件与宏定义
因为easyx设置的窗口类似于数学x-y坐标,不过y轴倒立,所以宏定义wide与high在设置窗口时用到
即函数initgraph(wide,high),而窗口大小与后续坐标设置相关。(强迫症注意)
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <graphics.h>
#include <conio.h>
#define maxsize 500//蛇的最大长度
#define wide 640//窗口数据
#define high 480
#define Speed 10//速度
#define R 5//绘图半径
2.枚举、结构体、全局变量设置
基本数据会通过结构体数组、全局变量等储存,与思维导图结合,在命名上尽量合理规范
enum direction {
up,down,left,right,
};//移动方向
enum skin {
black,red,green,blue,colors
};//皮肤选择
enum state {
menu,startgame,skin,help,out,deadgame,gamedata,messdirgame,
};//游戏状态
typedef struct position {
int x;
int y;
} Pos;//坐标(虽然POINT自带x、y,但还是想自己设置个)
struct snake {
int size;
int direction;
int speed;
int skin;
Pos pos[maxsize];
} snake;//蛇的基本数据
struct food {
int x;
int y;
int big;
bool flag;
} food;//食物的基本数据
struct attack {
Pos xy;
int dir;
bool exist;
} attack;//攻击子弹的基本数据
int gamestate=menu;//定义游戏状态
int hittime=0;//定义蛇的碰撞次数,用于后续判断蛇的死亡
3.函数声明与主函数
在主函数中当然是先设置窗口与初始化数据,随后一个循环,暴力 框住这坨函数让她们不断运行,实现数据不断更新、图片不断绘制
所以游戏暂停的灵感就是又进入一个while循环并且暂时不出来。
不同界面的切换,其实就是获取游戏状态(gamestate)(结合枚举定义)后,if判断是否绘制这个函数,再循环,再判断。
虽然看着这么大坨,但与思维导图结合,加上自己编写的习惯,还是挺清楚的。
在debug时,可以在initgraph(wide,high)括号内加上SHOWCONSOLE调出控制台检查错误,不然就会出现"We don’t know what is wrong tonight”的情况
void drawmenu();//绘制菜单
void initgame();//初始化游戏部分数据
void initfood();//初始化食物数据
void control();//获取键盘信息,实现控制
void movesnake();//移动蛇的数据处理
void moveattack();//移动子弹的数据处理
void eatfood();//吃到食物的数据处理
void deadsnake();//判断蛇死亡的数据处理
void drawgame();//主要的绘制函数,游戏进行的绘制
void drawtext();//游戏过程的文字信息绘制
void drawhelp();//查看帮助的界面设置,属于绘制部分
void drawSkin();//皮肤的的界面设置,属于绘制与数据的两部分
void drawgamedata();//存放最高分的界面设置,属于绘制部分
void drawEsc();//设置退出界面,属于绘制部分
int readscore();//读取文件
void writescore(int* score);//将分数写入文件
int main() {
initgraph(wide,high);//窗口设置
initgame();
while(1) {
control();
movesnake();
eatfood();
deadsnake();
drawmenu();
drawgame();
drawSkin();
drawhelp();
drawgamedata();
drawEsc();
Sleep(80);//延时
}
return 0;
}
4.函数模块功能
(1).初始化蛇与食物
关于蛇的绘制,就是几个圆圈(方块也行)依序在坐标系上排列,采用for+结构体数组,设置初始的蛇身数据,顺便也设置了出现的初始位置。
食物的绘制需要运用随机数,同时注意坐标数据控制在窗口大小范围内出现,并用布尔类型标记食物存在。
注意:设置Speed不可过大,因为移动的是像素点,速度过大可能导致蛇头圆点直接跨过食物圆范围,造成视觉上穿过食物而不吃食物的情况,可增大食物圆半径、调节Sleep()延时、调节speed等达到效果。(问就是错过)
(同理加速的时候需要设置上限)
void initgame() {
hittime=0;
snake.size=3;//蛇的信息
snake.speed=Speed;
snake.direction=right;
for(int i=0; i<snake.size; i++) {
snake.pos[i].x=40-i*20+wide/2;
snake.pos[i].y=10+high/2;
}
initfood();
}
void initfood() {
srand(GetTickCount());
food.x=(rand()%(wide-60))+15;
food.y=(rand()%(high-60))+15;
food.big=R*2;
food.flag=true;
}
(2).吃食
无论蛇头吃食,还是口水 子弹吃食,都是判断坐标是否在范围内,进而增加积分(本蛇通过计算size长度换算分数),同时设置食物状态false,再次initfood()设置食物的基础数据
void eatfood() {
if(food.flag&&snake.pos[0].x>=food.x-food.big
&&snake.pos[0].x<=food.x+food.big
&&snake.pos[0].y>=food.y-food.big
&&snake.pos[0].y<=food.y+food.big) {
food.flag=false;
snake.size++;
}
if(food.flag&&attack.xy.x>=food.x-food.big
&&attack.xy.x<=food.x+food.big
&&attack.xy.y>=food.y-food.big
&&attack.xy.y<=food.y+food.big) {
food.flag=false;
snake.size+=3;
attack.exist=false;
}
if(!food.flag) {
initfood();
}
}
(3).键盘控制
主要通过_getch()函数自动读取键盘字符,省去回车键。
对于方向判断,考虑游戏状态是乱向模式,增加个if来判断就行。
加速时设置个上限,原因见上。
技能键就是设置子弹存在(true)并调用移动子弹的函数。
暂停键就是进入个循环,再套一个_getch()来实现返回菜单、继续游戏(break)、退出游戏等操作(其实就是更改gamestate的值,在主函数不断循环的if语句判断下切换绘制的界面)
void control() {
if(_kbhit()) { //检测是否有按键,有==真
switch(_getch()) { //72 80 75 77 上下左右键值
case 'w':
case 'W':
case 72 :
if(snake.direction!=down)
if(gamestate==messdirgame&&snake.direction!=up)
snake.direction=down;
else
snake.direction=up;
break;
case 's':
case 'S':
case 80 :
if(snake.direction!=up)
if(gamestate==messdirgame&&snake.direction!=down)
snake.direction=up;
else
snake.direction=down;
break;
case 'a':
case 'A':
case 75 :
if(snake.direction!=right)
if(gamestate==messdirgame&&snake.direction!=left)
snake.direction=right;
else
snake.direction=left;
break;
case 'd':
case 'D':
case 77 :
if(snake.direction!=left)
if(gamestate==messdirgame&&snake.direction!=right)
snake.direction=left;
else
snake.direction=right;
break;
case 'j':
case 'J':
if(snake.size>=2)
if(!attack.exist) {
snake.size--;
attack.exist=true;
attack.xy.x=snake.pos[0].x;
attack.xy.y=snake.pos[0].y;
attack.dir=snake.direction;
moveattack();
}
break;
case 'e':
case 'E':
snake.speed += 1;
if (snake.speed >= 20)snake.speed = 20;//加速太多有时吃不到,因为速度太快,原点直接跳过食物范围
break;
case 'q':
case 'Q':
snake.speed -= 1;
break;
case ' ':
while(1) {
setbkmode(TRANSPARENT);//设置文字透明
settextcolor(BLACK);
settextstyle(40,0,"黑体");
outtextxy(170,200,"按空格继续游戏");
settextcolor(WHITE);
settextstyle(20, 0, "华文琥珀");
outtextxy(25,340, "r/R,返回菜单");
outtextxy(25,380, "H/h键 查看帮助");
outtextxy(25,420, "O/o 退出游戏");
switch(_getch()) {
case ' ':
return;
case 'h':
case 'H':
gamestate = help;
return;
case 'r':
case 'R':
gamestate=menu;
return;
case 'o':
case 'O':
gamestate=out;
return;
}
}
}
}
}
(4).移动蛇与蛇的口水
移动蛇就是将蛇身最后一节的坐标设置为前一节蛇身的坐标,再控制移动蛇头,达到丝滑的移动效果,而不是矢量位移的蛇。蛇头的移动就是在control()函数获取移动方向后,相对的加减坐标。
口水 子弹移动同理,不过速度要更大,不然与蛇头重合,方向也不能直接使用蛇头方向(蛇头方向不断变化),在发射口水时,将蛇头方向赋值给口水子弹结构体即可。
(不然出现口水跟着蛇头跳舞的情况,别问我怎么这么清楚)
创新规则:口水(子弹)一次发射积分-1,击中食物积分+3,一次只能发射一把口水,积分<-2(只剩个头)无法吐口水。
void movesnake() {
for(int i=snake.size-1; i>0; i--) {//身体移动
snake.pos[i]=snake.pos[i-1];
}
switch(snake.direction) {//头移动
case up:
snake.pos[0].y-=snake.speed;
if(snake.pos[0].y-R<=0) {
snake.pos[0].y=high;
}
break;
case down:
snake.pos[0].y+=snake.speed;
if((snake.pos[0].y+R)>=high) {
snake.pos[0].y=0;
}
break;
case left:
snake.pos[0].x-=snake.speed;
if(snake.pos[0].x-R<=0) {
snake.pos[0].x=wide;
}
break;
case right:
snake.pos[0].x+=snake.speed;
if(snake.pos[0].x+R>=wide) {
snake.pos[0].x=0;
}
break;
}
}
void moveattack() {
if(attack.dir==up) {
attack.xy.y-=snake.speed*2;
} else if(attack.dir==down) {
attack.xy.y+=snake.speed*2;
} else if(attack.dir==left) {
attack.xy.x-=snake.speed*2;
} else if(attack.dir==right) {
attack.xy.x+=snake.speed*2;
}
if(attack.xy.x+R>=wide) attack.exist=false;
if(attack.xy.x-R<=0) attack.exist=false;
if(attack.xy.y+R>=high) attack.exist=false;
if(attack.xy.y-R<=0) attack.exist=false;
}
(5).绘制游戏界面与文字说明
当获取的游戏状态(gamestate)符合要求时,执行绘制游戏的函数,这里用到一点easyx的知识
画图就是:
setbkcolor(RED);//or RGB() 设置背景色
cleardevice();//用当前颜色清空屏幕
setfillcolor(BLACK);//设置填充色
solidcircle(x,y,r);//用上述颜色画圆,参数分别为横、纵坐标,半径
画食物、子弹一个道理。其中BeginBatchDraw()与EndBatchDraw()用于防止闪屏出现,功能差不多就是等这坨中间的函数执行完,再统一画在屏幕上,明白原理后知道不是每处画图都需要用到,滥用可能出现想画的效果画不出来的情况。
(别问我为什么这么清楚)
画文字就是:
setbkmode(TRANSPARENT);//设置透明,视情况需要
settextcolor(BLACK);//文字颜色
settextstyle(20,0,"仿宋");//文字大小与格式
outtextxy(540,390,"遥遥无妻ing");//设置出现坐标与内容
注意图形库仅支持字符输出,整型可通过sprintf打印到字符串即可。
此模块函数如下:
void drawgame() {
if(gamestate==startgame||gamestate==deadgame||gamestate==messdirgame) {
BeginBatchDraw(); //双缓冲绘图
setbkcolor(RGB(135,206,235));
cleardevice();//用当前颜色清空屏幕
//画蛇
for(int i=0; i<snake.size; i++) {
if(snake.skin==black)
setfillcolor(BLACK);
else if(snake.skin==red)
setfillcolor(RED);
else if(snake.skin==green)
setfillcolor(GREEN);
else if(snake.skin==blue)
setfillcolor(BLUE);
else if(snake.skin==colors) {
srand(GetTickCount());
for(int i=0; i<256; i+=5)
setfillcolor(RGB(rand() % 256, rand() % 256, rand() % 256));
}
solidcircle(snake.pos[i].x,snake.pos[i].y,R+(snake.size-3)/10);
}
//画食物
if(food.flag) {
setfillcolor(RED);
solidcircle(food.x,food.y,food.big);
}
//画子弹
if(attack.exist) {
moveattack();
setfillcolor(YELLOW);
solidcircle(attack.xy.x,attack.xy.y,R);
}
drawtext();
EndBatchDraw();
}
}
void drawtext() {
int highestscore=readscore();
char score[3],speed[3],str[4],hit[3];
sprintf(score,"%d",snake.size-3);
sprintf(speed,"%d",snake.speed);
sprintf(str,"%d",highestscore);
sprintf(hit,"%d",hittime);
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(20,0,"仿宋");
outtextxy(540,390,"j键 发射");
outtextxy(540,420,"空格暂停");
outtextxy(480,450,"by yang junyao");
outtextxy(5, 450, "分数:");
outtextxy(65, 450, score);
outtextxy(100, 450, "速度:");
outtextxy(160, 450, speed);
outtextxy(200, 450, "最高分:");
outtextxy(280, 450, str);
if(gamestate==deadgame||gamedata==messdirgame) {
outtextxy(320, 450, "撞头次数:");
outtextxy(420, 450, hit);
}
if(snake.size-3>highestscore) {
writescore(&snake.size);
}
}
(6).菜单、帮助、皮肤、最高分数、退出游戏界面绘制
在明白了前面几个函数模块运行原理后,其余界面的绘制无非就是画图、画文字、键盘获取游戏状态、调整结构体的数据内容。(这块适合氺代码。。。 )
注意:exit(0)与closegraph()都可用于结束游戏,只是一个结束进程,一个关闭窗口。(感觉结果上相差不大)
其中贴图操作用于设置背景图片:
IMAGE img;
loadimage(&img,"051.jpg");
putimage(0,0,&img);
注意:图片大小要考虑窗口大小,不然可能无法显示(别问我为什么这么清楚 )
void drawmenu() {
if(gamestate==menu) {
initgame();//确保每次返回,游戏重开
IMAGE img;
loadimage(&img,"051.jpg");
putimage(0,0,&img);
setbkmode(TRANSPARENT);
settextcolor(RED);
settextstyle(60,0,"华文琥珀");
outtextxy(130,40,"进击的贪吃蛇");
settextcolor(WHITE);
settextstyle(20, 0, "华文琥珀");
outtextxy(230,150, "空格键 自由游戏模式");
outtextxy(230,200, "D/d键 死亡游戏模式");
outtextxy(230,250, "N/n键 乱向死亡模式");
outtextxy(230,300, "S/s键 设置皮肤");
outtextxy(230,350, "H/h键 查看帮助");
outtextxy(230,400, "M/m键 游戏分数");
outtextxy(230,450, "O/o键 退出游戏");
switch(_getch()) {
case ' ':
gamestate = startgame;
break;
case 'd':
case 'D':
gamestate=deadgame;
break;
case 'n':
case 'N':
gamestate=messdirgame;
break;
case 'h':
case 'H':
gamestate=help;
break;
case 'o':
case 'O':
gamestate=out;
break;
case 's':
case 'S':
gamestate=skin;
break;
case 'm':
case 'M':
gamestate=gamedata;
break;
}
}
}
void drawhelp() {
if(gamestate==3) {
setbkcolor(RGB(159,182,205));
cleardevice();
settextcolor(BLACK);
settextstyle(20,0,"黑体");
outtextxy(50,30,"游戏中,空格键暂停 e/E键加速 q/Q键减速");
outtextxy(50,70,"蛇的胖瘦随长度的一定增加而增加");
outtextxy(50,110,"蛇长度>1时,j键发射口水,每次发射长度-1,击中食物+3");
outtextxy(50,190,"自由模式不会撞死,死亡模式会撞死");
outtextxy(50,230,"乱向死亡模式方向控制错乱且会撞死");
outtextxy(50,270,"每次撞死积分减-5,积分<0,game over");
settextcolor(WHITE);
settextstyle(20, 0, "华文琥珀");
outtextxy(460,400, "r/R,返回菜单");
outtextxy(460,440, "o/O键 退出游戏");
switch(_getch()) {
case 'r':
case 'R':
gamestate=menu;
break;
case 'o':
case 'O':
gamestate=out;
break;
}
}
}
void drawgamedata() {
if(gamestate==gamedata) {
setbkcolor(RGB(159,182,205));
cleardevice();
settextcolor(BLACK);
settextstyle(20,0,"华文琥珀");
outtextxy(100,130,"游戏最高分:");
outtextxy(240,400, "r/R,返回菜单");
int highest=readscore();
char str[4];
sprintf(str,"%d",highest);
outtextxy(100, 170, str);
switch(_getch()) {
case 'r':
case 'R':
gamestate=menu;
break;
}
}
}
void drawEsc() {
if(gamestate==out) {
setbkcolor(RGB(159,182,205));
cleardevice();
settextcolor(BLACK);
settextstyle(30,0,"华文琥珀");
outtextxy(230,180,"n/N 返回菜单");
outtextxy(230,250,"y/Y 确认退出");
switch(_getch()) {
case 'n':
case 'N':
gamestate=menu;
break;
case 'y':
case 'Y':
exit(0);//closegraph();
break;
}
}
}
void drawSkin() {
if(gamestate==skin) {
setbkcolor(RGB(159,182,205));
cleardevice();
settextcolor(BLACK);
settextstyle(20,0,"华文琥珀");
outtextxy(100,130,"选择皮肤后,自动返回菜单:");
outtextxy(230,180,"z键 black");
outtextxy(230,220,"x键 red");
outtextxy(230,260,"c键 green");
outtextxy(230,300,"v键 blue");
outtextxy(230,340,"b键 colors");
switch(_getch()) {
case 'z':
case 'Z':
snake.skin=black;
gamestate=menu;
break;
case 'x':
case 'X':
snake.skin=red;
gamestate=menu;
break;
case 'c':
case 'C':
snake.skin=green;
gamestate=menu;
break;
case 'v':
case 'V':
snake.skin=blue;
gamestate=menu;
break;
case 'b':
case 'B':
snake.skin=colors;
gamestate=menu;
break;
}
}
}
(7).蛇的死亡判断
撞死很简单 ,判断坐标到达边界或者与蛇身重合即可,根据这个思路,还可以画几面墙(或地雷),判断坐标与墙(或地雷)重合,算一次死亡(至少几十行大作业代码氺过去,但我懒 )。
死亡后的界面设置同上述原理。
创新规则:用撞一次减五分,如果分数<0则死亡的规则,代替一次性死亡,增加可玩性。同时在游戏界面显示这些游戏数据。
void deadsnake() {
if(gamestate==deadgame||gamestate==messdirgame) {
for(int i=1; i<snake.size; i++) {
if(snake.pos[0].x==snake.pos[i].x&&snake.pos[0].y==snake.pos[i].y) {
snake.size-=5;
hittime++;//撞车次数
if(snake.size-3<0)
while(1) {
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(40,0,"黑体");
outtextxy(170,200,"游戏结束 你撞到自己了");
settextcolor(WHITE);
settextstyle(30, 0, "华文琥珀");
outtextxy(240,280, "r/R,返回菜单");
switch(_getch()) {
case 'r':
case 'R':
gamestate=menu;
return;
}
}
}
}
if(snake.pos[0].x==wide||snake.pos[0].x==0||snake.pos[0].y==high||snake.pos[0].y==0) {
snake.size-=5;
hittime++;//撞车次数
if(snake.size-3<0)
while(1) {
setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(40,0,"黑体");
outtextxy(170,200,"游戏结束 你撞墙了");
settextcolor(WHITE);
settextstyle(30, 0, "华文琥珀");
outtextxy(240,280, "r/R,返回菜单");
switch(_getch()) {
case 'r':
case 'R':
gamestate=menu;
return;
}
}
}
}
}
(8).文件读写最高分数
(这部分可用全局变量代替,用文件只是为了占个大作业知识点 ,不过文件的操作确实不大熟)
int readscore() {
FILE *fp=NULL;
int highest;
char str[5];
fp=fopen("game data.txt","r");//只读的方式打开
fread(str,sizeof(char),sizeof(str),fp);//读取文件内容
fclose(fp);
return atoi(str);//返回整型数值
}
void writescore(int* score) {
FILE *fp=NULL;
fp=fopen("game data.txt","w+");
fprintf(fp,"%d",*score-3);
fclose(fp);
}
照着这个写法其他像简单的飞机大战、坦克大战、球球大冒险、超级玛丽啥的也可以试试。后面有时间再用其他语言试试。英语太差以后再去学GitHub。