利用DevC++以及EGE图形库写出一款C语言飞机大战小游戏
前言:
上学期期末大作业利用C语言写过一个极其简陋的飞机大战(只有黑洞洞的终端窗口,至于飞机,额,也是一言难尽),暑假闲来无事,准备利用图形库来扩充扩充游戏内容,使其更加具有可玩性和趣味性。效果图如下,也算是有了个看的过去的UI界面吧。当然程序还有些许小小我解决不了的bug困扰,但我个人觉得只是练手的话,完成度也可以了。图片素材来源于网络,侵权删。
正文:
开发环境及工具:
本C语言项目(或许不能称之为纯C语言项目了,后缀名要使用.cpp)所使用的开发工具为DevC++,其实之前一直接触的都是Clion以及vscode,但由于要使用到图形库,这两者图形库的配置也都较为复杂,vs的体量又太大,所以决定尝试使用DevC++(不得不讲DevC++的自动补齐以及提示都有些鸡肋,不建议拿来进行较大项目的创建)。本项目所使用的图形库为EGE,具体下载安装配置教程也可上网自行查阅。可能更主流的是EasyX图形库?但DevC++能查到的更多是EGE图形库的使用。
DevC++下载:https://sourceforge.net/projects/orwelldevcpp/
EGE配置使用:https://blog.csdn.net/cnds123/article/details/109982100
https://blog.csdn.net/qq_39151563/article/details/100161986
我本人是使用的上面的教程配置的,当然好像又有新版的EGE了,也可以参考下面的,EGE专栏里的代码以及例子也可以看看。
使用过程中遇到困难也可参考官方文档:http://xege.org/manual/
设计思路:
准备工作都已经做完,想必你也可以在你的电脑上使用DevC++和EGE图形库了,接下来就是发挥你编码才能的时刻了。我的思路也很简单,玩家能操作飞机的移动以及子弹的发射,躲避敌机以及敌人的攻击,最终战胜boss即为胜利。有些地方也参考了网上别人的一些实现逻辑。
开始界面:
既然都使用到图形库了,不如就顺势加一个开始界面,丰富游戏的功能。如开始时的示意图所示,有三个按键一样的方框,分别是“开始游戏”“游戏说明”“退出游戏”。那么,如何将这些按键真正的实现呢?其实实现起来也远没有想象中那么困难。首先就是要获取按键所处的位置,即按键区域的x、y坐标的范围,也就是左上角以及右下角的x、y坐标,当然EGE已经为我们准备好了该功能。可以参考这里http://xege.org/manual/tutorial/14.htm 我已经获取好了位置信息并将其记下,我们就可以利用这些坐标实现点击跳转啦。当然,在此之前,你得学会如何获取图像,并将图像显示在屏幕上,官方文档会对你有帮助的。
但我引入了一个judge变量,因为发现跳转至不同页面后,若不加以判断,原先按键位置仍会发挥作用。以下便是这一部分的代码实现。
while(judge == 0 && mousemsg()){ // 一开始的欢迎界面 mouse_msg msg = getmouse();
// 可以开始时调用一下ege自带的鼠标信息相关的函数,获取一些按钮的位置,接下来就可以根据按钮所占位置模拟按钮的效果了 if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 157 && msg.y < 210){judge = 1;play(); // 点击按钮,开始游戏 }else if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 241 && msg.y < 294){judge = 2; // 不利用judge的话,原本欢迎界面中存在的按键部位仍会发挥作用 information(); // 点击按钮,游戏说明 }else if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 320 && msg.y < 372){end(); // 点击按钮,退出游戏 }}
游戏实现:
首先是对于道具(如我方战机、敌方战机、子弹、buff)的控制,我们既要不断获得其x、y坐标,也可能要获得其状态,是否活着又或者是否被使用掉。这时,结构体便显得尤为重要了。
// 利用结构体封装我方飞机各种属性
typedef struct plane{
// x、y是对坐标的描述 int x;int y;
// blood是剩余的血量 int blood;
// 飞机的宽度和高度 int width;int height;
// 飞机的状态 bool is_or_no;
} Plane;
//定义一个Plane类型的变量myplane,作为玩家操控的战机
Plane myplane;
//定义多个Plane类型的变量,作为出现的敌机
Plane enemyplane[NUM_ENEMY];
//创建一个boss
Plane bossplane;
//buff基本行为与敌机类似,直接套用一个Plane类型
Plane mybuff;
//再次利用结构体封装子弹属性
typedef struct bullet{
// x、y表示子弹的坐标 int x;int y;
// 表示子弹的状态 bool is_or_no;
} Bullet;
//我方子弹
Bullet mybullet[NUM_MY_BUL];
//敌方子弹
Bullet enemybullet[NUM_ENEMY_BUL];
毕竟是一个小型的项目,所以必然代码的体量会大一些,就需要封装多种多样的函数了,函数的使用可以使各部分功能清晰明了。以下我将选取几个具有代表性的讲讲。
//初始化设计游戏界面
void design();
//点击开始游戏,进入真正的游戏界面
void play();
//初始化各战力单位
void initPlayer();
//通过键盘操作飞机移动
void move();
//产生子弹
void bulletcreate();
//子弹移动
void bulletmove();
//敌机的产生
void enemycreate();
//敌机的移动
void enemymove();
//子弹击中敌机
void hitenemy();
//产生boss
void bosscreate();
//boss的移动
void bossmove();
//没有对抗有啥意思,boss发射子弹
void bossaction();
//boss子弹移动
void bossbulletmove();
//击中我方飞机
void hitme();
//游戏buff产生
void buffcreate();
//游戏buff的移动
void buffmove();
//游戏buff获得以及产生作用
void buffget();
//游戏失败
void losegame();
//游戏胜利
void wingame();
//循环进行
void mainloop();
//游戏没有声音太无聊?
void music_haha();
//一些游戏相关的信息
void information();
// 游戏结束
int end();
mainloop()
这是我们整个游戏的一个最主要函数,操纵着各个功能的入口,前面也讲了,就是开始界面,就是利用鼠标的按动来操作。
void mainloop(){for( ; is_run(); delay_fps(60)){while(judge == 0 && mousemsg()){ // 一开始的欢迎界面 mouse_msg msg = getmouse();
// 可以开始时调用一下ege自带的鼠标信息相关的函数,获取一些按钮的位置,接下来就可以根据按钮所占位置模拟按钮的效果了 if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 157 && msg.y < 210){judge = 1;play(); // 点击按钮,开始游戏 }else if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 241 && msg.y < 294){judge = 2; // 不利用judge的话,原本欢迎界面中存在的按键部位仍会发挥作用 information(); // 点击按钮,游戏说明 }else if(msg.is_left() && msg.is_down() && msg.x > 140 && msg.x < 260 && msg.y > 320 && msg.y < 372){end(); // 点击按钮,退出游戏 }}
// 游戏信息页面退出功能实现 while(judge == 2 && mousemsg()){mouse_msg msg = getmouse();if(msg.is_left() && msg.is_down() && msg.x > 24 && msg.x < 67 && msg.y > 23 && msg.y < 35){cleardevice();putimage(0, 0, 400, 711, img_back, 0, 0, 800, 1422);judge = 0;}}
// 过关失败 while(judge == 3 && mousemsg()){mouse_msg msg = getmouse();img11 = newimage();getimage(img11, "./失败.jpeg", 0, 0);putimage(0, 0, 400, 711, img11, 0, 0, 400, 711);if(msg.is_left() && msg.is_down()){cleardevice();putimage(0, 0, 400, 711, img_back, 0, 0, 800, 1422);judge = 0; }}
// 过关成功 while(judge == 4 && mousemsg()){mouse_msg msg = getmouse();img12 = newimage();getimage(img12, "./胜利.jpeg", 0, 0);putimage(0, 0, 400, 711, img12, 0, 0, 400, 711);if(msg.is_left() && msg.is_down()){cleardevice();putimage(0, 0, 400, 711, img_back, 0, 0, 800, 1422);judge = 0; }}if(music.GetPlayStatus() == MUSIC_MODE_STOP){music.Play(0);}}
}
play()
重要性仅次于mainloop(),点击“开始游戏”按钮,你就已经进入游戏中了。具体说明可以参见我的注释。这里我最值得一提的就是视觉滚动效果的实现,我的思路是拼接两张一样的图片,当一张图片的底部即将露出时,另一张便填补空缺,利用他们y坐标的变化,带动图片变化,就给人视觉上以图片场景运动的效果。
void play(){
// 拼接了两张一样的图片,形成一个滚动前进的视觉效果,这里初始化两张图片的y坐标 int img2_y = 0;int img2_yy = -711;initPlayer(); //调用初始化函数,初始化游戏的各项参数
// 获得所需的图片 getimage(img2, "./图2.jpeg", 0, 0);getimage(img3, "./我的战机.png", 0, 0);getimage(img4, "./我的战机zero.png", 0, 0);getimage(img5, "./敌机.png", 0, 0);getimage(img6, "./敌机boss.png", 0, 0);getimage(img7, "./子弹1.png", 0, 0);getimage(img8, "./子弹2.png", 0, 0);getimage(img9, "./子弹3.png", 0, 0);getimage(img10, "./buff.png", 0, 0);for ( ; is_run(); delay_fps(60) ){cleardevice();
// 背景重复 if(img2_y == 711){img2_y = -711;}if(img2_yy == 711){img2_yy = -711;}
// for(int i = 0; i < NUM_MY_BUL; i++){
// if(mybullet[i].y == (-80) * i){
// mybullet[i].y = myplane.y;
// mybullet[i].x = myplane.x;
// }
// }putimage(0, img2_y, 400, 711, img2, 0, 0, 400, 711);putimage(0, img2_yy, 400, 711, img2, 0, 0, 400, 711);
// buff加持时间,否则一直加持 static DWORD t5, t6;if(myplane.is_or_no && t6 - t5 > 8000){myplane.is_or_no = false;t5 = t6;}t6 = clock();
// 根据我方飞机状态贴相对应的图 if(!myplane.is_or_no){putimage_withalpha(NULL, img3, myplane.x, myplane.y);}else{putimage_withalpha(NULL, img4, myplane.x, myplane.y);}
// putimage_withalpha(NULL, img3, myplane.x, myplane.y);
// 根据子弹以及我方飞机状态贴对应的图 for(int i = 0; i < NUM_MY_BUL; i++){if(mybullet[i].is_or_no && !myplane.is_or_no){putimage_withalpha(NULL, img7, mybullet[i].x, mybullet[i].y);}if(mybullet[i].is_or_no && myplane.is_or_no){putimage_withalpha(NULL, img8, mybullet[i].x, mybullet[i].y);}}
// for(int i = 0; i < NUM_MY_BUL; i++){
// mybullet[i].y -= 6;
// }
// 根据普通敌机状态贴图 for(int i = 0; i < NUM_ENEMY; i++){if(enemyplane[i].is_or_no){putimage_withalpha(NULL, img5, enemyplane[i].x, enemyplane[i].y);}}
// 背景滚动,利用y坐标的增加 img2_y += 1;img2_yy += 1;
// 调用move()函数,控制飞机的运动 move();
// 调用函数,子弹的子弹运动 bulletmove();
// 控制敌机产生的速度 static DWORD t3, t4;if(t4 - t3 > 1000){enemycreate();t3 = t4;}t4 = clock();enemymove(); //敌机的移动
// 达到条件,创建boss if(score >= 400 && !bossplane.is_or_no){bosscreate();}
// boss的移动 bossmove();static DWORD t7, t8;if(t8 - t7 > 8000){bossaction();t7 = t8;}t8 = clock();bossbulletmove();
// 调用函数,创建buff buffcreate();
// buff图标的移动 buffmove();
// 获取到buff buffget();
// 贴图,boss和buff if(bossplane.is_or_no){putimage_withalpha(NULL, img6, bossplane.x, bossplane.y);}if(mybuff.is_or_no){putimage_withalpha(NULL, img10, mybuff.x, mybuff.y);}for(int i = 0; i < NUM_ENEMY_BUL; i++){if(enemybullet[i].is_or_no){putimage_withalpha(NULL, img9, enemybullet[i].x, enemybullet[i].y);}}
// 调用函数,击中敌机 hitenemy();hitme();setcolor(EGERGB(0x0, 0xFF, 0x0));setfont(12, 0, "宋体");setbkmode(TRANSPARENT);char s[6];char t[6];char m[6];sprintf(s, "%d", score);sprintf(t, "%d", myplane.blood);sprintf(m, "%d", bossplane.blood);outtextxy(0, 0, "得分"); outtextxy(30, 0, s);outtextxy(0, 15, "血量"); outtextxy(30, 15, t);outtextxy(0, 30, "boss");outtextxy(30, 30, m);
// if(mybuff.is_or_no){
// outtextxy(0, 45, "yes");
// }
// else{
// outtextxy(0, 45, "no");
// }
// if(myplane.is_or_no){
// outtextxy(0, 60, "yes");
// }
// else{
// outtextxy(0, 60, "no");
// }
// 调用函数,游戏失败 losegame();
// 调用函数,游戏成功 wingame();
// 保证音乐的循环播放,但是似乎会产生一些延迟 if(music.GetPlayStatus() == MUSIC_MODE_STOP){music.Play(0);}
// 判断游戏失败或者胜利并跳出游戏循环 if(judge == 3 || judge == 4){break;}}
}
initPlayer();
每一个游戏,初始化都是不可缺少的。这既能保证每次游戏开始时的一致性,又能避免奇奇怪怪的bug,甚至还会提升代码运行的效率。当然,这一部分视个人想法不同,可以初始化不同的数值。
void initPlayer(){
// 初始化的各个参数 judge = 0;dx = 1;score = 0; myplane.x = 150;myplane.y = 540;myplane.blood = 2000;myplane.width = 80;myplane.height = 129;myplane.is_or_no = false;for(int i = 0; i < NUM_MY_BUL; i++){mybullet[i].x = -60;mybullet[i].y = -60;mybullet[i].is_or_no = false;}for(int i = 0; i < NUM_ENEMY; i++){enemyplane[i].is_or_no = false;enemyplane[i].blood = 200;enemyplane[i].width = 50;enemyplane[i].height = 89;} for(int i = 0; i < NUM_ENEMY_BUL; i++){enemybullet[i].is_or_no = false;}bossplane.is_or_no = false;bossplane.blood = 20000;bossplane.width = 240;bossplane.height = 199; mybuff.is_or_no = false;mybuff.width = 36;mybuff.height = 42; img2 = newimage();img3 = newimage();img4 = newimage();img5 = newimage();img6 = newimage();img7 = newimage();img8 = newimage();img9 = newimage();img10 = newimage();
}
move()
想要使你的飞机受你的控制?那就不可避免得与键盘做交互了,以下代码即可让你随心所欲控制你的飞机啦。
void move(){
// 键盘交互,利用用户按键控制飞行以及炮弹的发射
// 上移 if(GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W')){if(myplane.y > 0){myplane.y -= 8;}}
// 下移 if(GetAsyncKeyState(VK_DOWN) || GetAsyncKeyState('S')){if(myplane.y + 129 < 711){myplane.y += 8; }}
// 左移 if(GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')){if(myplane.x > -30){myplane.x -= 8;}}
// 右移 if(GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')){if(myplane.x < 350){myplane.x += 8;}}
// 发射子弹 static DWORD t1 = 0, t2 = 0;if(GetAsyncKeyState(VK_SPACE) && t2 - t1 > 200){bulletcreate();t1 = t2;}t2 = GetTickCount();
}
enemycreate()
敌机的制造以及boss、buff的产生都几乎遵循一样的逻辑。所以就以敌机为例来演示好了。关键的就是其状态的改变以及产生的位置,我这里是根据我背景的宽度选定了340,这当然不是一个好的编程习惯,你或许可以用一个常量去替换这里的具体数值。
void enemycreate(){for(int i = 0; i < NUM_ENEMY; i++){if(!enemyplane[i].is_or_no){enemyplane[i].is_or_no = true;enemyplane[i].x = rand()%(340);enemyplane[i].y = -10;break;}}
}
bossmove()
普通敌机以及子弹基本为单一坐标的递增或递减,也没什么可谈的,就以boss的移动来举例说明吧。我认为一个独特的地方是他的一个撞壁回弹的运动效果,这样就能保持boss一直在上方左右来回地进行运动。
void bossmove(){if(bossplane.is_or_no){if(bossplane.y <= 0){bossplane.y += 1;}
// 控制其左右运动 bossplane.x += dx;if(bossplane.x < -120){dx = 1;}if(bossplane.x >= 280){dx = -1;}}
}
hitenemy()
子弹碰到敌机当然得产生效果。这里一样,通过坐标及状态来进行判断是否造成伤害。自己进行飞机大战项目时也可以根据自己所选择地敌机的大小以及子弹的大小进行合理调控。同样的原理也可以适用于敌方飞机与我方飞机是否碰撞,敌方攻击是否对我造成伤害。
void hitenemy(){for(int i = 0; i < NUM_ENEMY; i++){if(!enemyplane[i].is_or_no){continue;}for(int j = 0; j < NUM_MY_BUL; j++){if(!mybullet[j].is_or_no){continue;}if(mybullet[j].x + 25 > enemyplane[i].x && (mybullet[j].x + 40) < (enemyplane[i].x + enemyplane[i].width + 25)&& mybullet[j].y > enemyplane[i].y && mybullet[j].y < enemyplane[i].y + enemyplane[i].height){mybullet[j].is_or_no = false;enemyplane[i].blood -= 100; }}
// 敌机被击毁,参数重置以保证产生新的敌机if(enemyplane[i].blood <= 0){
// 每击毁一架敌机分数增长20 score += 20;enemyplane[i].is_or_no = false;enemyplane[i].blood = 200;}}
// 对于boss造成伤害 if(bossplane.blood >= 0 && bossplane.is_or_no){for(int j = 0; j < NUM_MY_BUL; j++){if(!mybullet[j].is_or_no){continue;}if(mybullet[j].x + 25 > bossplane.x && (mybullet[j].x + 40) < (bossplane.x + bossplane.width + 25)&& mybullet[j].y > bossplane.y && mybullet[j].y < bossplane.y + bossplane.height){mybullet[j].is_or_no = false;bossplane.blood -= 100; }}}
}
end()
最后的最后,当然是退出游戏,结束进程。我们不仅要把之前创建的图片销毁掉还要将音乐以及窗口关闭,这样才能保证不会有内存泄漏等情况的出现哦~~
int end(){
// 关闭音乐,销毁图片,关闭窗口 music.Close(); delimage(img1);delimage(img2); delimage(img3);delimage(img4); delimage(img5);delimage(img6); delimage(img7);delimage(img8);delimage(img9);delimage(img10); delimage(img11);delimage(img12); delimage(img_back);closegraph();
// 加上这句话后才能结束进程,这个bug可能导致多次编译运行后Makefile.win报错
// 单单return 0不知道为什么应用程序关闭了但进程没有 exit(0);return 0;
}
总结:
该项目目前还有一点小bug我无从下手解决,但总体的功能也基本实现,效果也算是达到预期吧,当然你也可以在此基础上拓展更多功能,更多关卡。项目名字取得是夏日清凉版飞机大战,子弹是水果、背景是沙滩感觉炎炎夏日也确实很清凉。希望以上内容对你的C语言小项目开发能有所帮助。