我的个人博客:谋仁·Blog
该项目已上传至GitHub:点击跳转
文章目录
- 摘要
- 运行环境
- 整体功能思维导图
- 效果预览
- 具体功能的实现
- 图形界面:EasyX
- EasyX图形库简介
- EasyX图形库的一些基本功能(该项目用到的)
- 菜单界面
- 玩法介绍界面
- 游戏界面
- 玩家图片的透明背景输出
- 玩家的移动
- 敌机的产生与移动
- 攻击系统
- 玩家的血量控制
- 游戏评分及结束界面
- 背景音乐的播放
- 源代码
- 一些细节&技巧
- icon图标的制作与插入
- 游戏的素材收集
- 封面的平面设计
- 易错集
- 存在的缺陷
- 参考资料
摘要
这是一个用C语言实现的基于EasyX图形库的飞机大战小游戏,很有意思的小项目。对初学者很友好哦!快来看一下吧!
运行环境
Windows10+Visual Studio 2019+EasyX_20210730
整体功能思维导图
效果预览
- 菜单界面(此时鼠标指在GO!按钮,按钮发生变色以反馈用户)
- 玩法界面(跳出弹窗介绍游戏规则)
- 进入游戏界面(敌机在窗体最上端随机出现,玩家移动/发射子弹)
- 游戏结束
具体功能的实现
图形界面:EasyX
EasyX图形库简介
- EasyX Graphics Library 是针对 Visual C++ 的免费绘图库,因其学习成本低、易上手、应用范围广、功能丰富等特点广受欢迎。
- 我们学习C语言面对着黑框,枯燥又乏味。想要做一些图形编程,但很多图形库学习难度大,学习门槛高,如:Win32,OpenlGI等。这时候我们就可以使用EasyX图形库来做一些图形编程,既简单又有趣。
EasyX图形库的一些基本功能(该项目用到的)
-
如何让一张图片显现出来?分三步:
-
绘制窗体–initgraph
例1:绘制一个宽×高为1522×787(该项目的窗口大小,单位:像素)的窗口。
initgraph(1522, 787);
例2:绘制一个宽×高为1522×787的窗口,同时显示控制台窗口。
initgraph(1522, 787, EW_SHOWCONSOLE);
显示控制台窗口便于调试。
-
加载图片–loadimage
例:将菜单界面背景图加载出来。
IMAGE menuBackground;//存放游戏菜单界面背景图 loadimage(&menuBackground, "./资源/menuBackground.png");//加载背景图
-
粘贴图片–putimage
例:将加载好的菜单界面背景图片显示出来。
putimage(0, 0, &menuBackground);
注:前两个参数分别表示横坐标、纵坐标。这里的坐标轴是以窗体左上角为原点。横坐标是操作对象的左上角到窗体左边的垂直距离,纵坐标是操作对象到窗体上边的垂直距离。
-
-
关于颜色
-
已经预定义的颜色
常量 值 颜色 -------- -------- -------- BLACK 0 黑 BLUE 0xAA0000 蓝 GREEN 0x00AA00 绿 CYAN 0xAAAA00 青 RED 0x0000AA 红 MAGENTA 0xAA00AA 紫 BROWN 0x0055AA 棕 LIGHTGRAY 0xAAAAAA 浅灰 DARKGRAY 0x555555 深灰 LIGHTBLUE 0xFF5555 亮蓝 LIGHTGREEN 0x55FF55 亮绿 LIGHTCYAN 0xFFFF55 亮青 LIGHTRED 0x5555FF 亮红 LIGHTMAGENTA 0xFF55FF 亮紫 YELLOW 0x55FFFF 黄 WHITE 0xFFFFFF 白
-
自定义颜色–RGB
光学三原色:红绿蓝。调整三种颜色的比例可以合成任意颜色。RGB(红,绿,蓝)三个部分分别是0~255值。为调节到想要的颜色,可以借助电脑自带画图软件编辑颜色中找;也可以使用QQ截图,指针瞄准指定颜色后按C键复制RGB值。
-
-
图形的输出
-
例:画一个虚线为轮廓且连接处为圆形的圆(本项目按钮样式)
setlinestyle(PS_DASH | PS_ENDCAP_ROUND, 宽度像素 ); setfillcolor(fillColor);//填充色 setlinecolor(lineColor);//轮廓线的颜色 fillcircle(x, y, radius);//画圆(横坐标,纵坐标,半径)
-
-
文本的输出
/*********************** *输入:(int类型)水印文本横坐标、(int类型)文本纵坐标、(int类型)文本字体尺寸、文本颜色 *输出:空 *作用:在任意位置生成任意大小、颜色的文本充当水印 ************************/ void WaterMark(int textX,int textY, int textSize,COLORREF textColor) {setbkmode(TRANSPARENT);//背景透明使文本背景不再是黑色settextstyle(textSize, 0, "隶书");//字体格式;settextcolor(textColor);//字体颜色outtextxy(textX, textY, "By 曹谋仁");//在(textX,textY)处显示“By 曹谋仁”文本 }
该函数就纯粹的体现了文本输出功能。
这里主要讲一下settextstyle函数
-
设置当前字体样式–settextstyle
该函数有四个重载,这里只介绍一下本项目中使用的这一种。
void settextstyle(int nHeight,int nWidth,LPCTSTR lpszFace);
nHeight–指定字符的高度(逻辑单位)。
nWidth–字符的平均宽度(逻辑单位)。如果为 0,则比例自适应。(注:上方水印文本输出函数中第二个参数为0即比例自适应后,就可以直接调节第一个参数来调节整体文本的大小。)
lpszFace–字体的种类,这里可以直接用中文加双引号来表示部分字体。
-
-
如何更流畅地动态绘图?
在设备上不断进行绘图操作时,会产生闪频现象。为了流畅的绘图,我们可以用下面两个函数处理。
BeginBatchDraw();//开始批量绘图 //这里放绘图代码 EndBatchDraw();//结束批量绘图
-
如何进行鼠标的操作?
-
存储鼠标信息的类型是ExMessage类型。故先建立记录鼠标信息的变量。
ExMessage mouse;//记录鼠标消息
-
获取当前鼠标信息,并立即返回–peekmessage函数。
if (peekmessage(&mouse, EM_MOUSE)) {//如果获取到鼠标的信息,则进行这里的操作... }
该函数也可以通过改变第二个参数来获取不同的信息:
标志 描述 EM_MOUSE 鼠标消息。 EM_KEY 按键消息。 EM_CHAR 字符消息。 EM_WINDOW 窗口消息。 -
检测鼠标上的操作。
if (peekmessage(&mouse, EM_MOUSE)) {//如果获取到鼠标的信息,则进行这里的操作if (mouse.message == WM_LBUTTONDOWN) {//按下鼠标左键时进入这里} }
当然,此函数不仅仅能检测到鼠标左键按下的信息,本项目关于鼠标只用到左键按下的操作,若想了解更详细→EasyX 文档 - ExMessage
-
-
键盘上的操作
该项目上用到的函数是:GetAsyncKeyState(键值);传入一个键值,若检测到按下则返回true。一些键值如下:
- 上:VK_UP 下:VK_DOWN 左:VK_LEFT 右:VK_RIGHT
- 如果是字母按键:‘字母的大写’。如果是字母小写只能检测到小写,如果是字母大写,则大小写均能检测到。
有关EasyX图形库的基本操作就介绍到这,这里主要是本项目中用到的一些功能。相比整个图形库所有功能而言实乃九牛之一毛,冰山之一角。如果想深入了解更多,请点击这里跳转→EasyX 文档
菜单界面
菜单界面除了最基本的图片或文本的输出外,最主要的就是怎么实现一个按钮。
所谓的按钮就是绘制一个图形,图形中绘制一个按钮上的文本。然后在这个带有文本的图形上添加鼠标左键的监测信息。至此,一个按钮所具备的最基本的特征都已经完成了。在本项目菜单界面的按钮上,为了更好的反馈用户,增加了当鼠标指着按钮时,按钮会发生变色。源代码如下:
/**************按钮信息************/
#define BUTTONNUM 3
//按钮顺序:{开始,离开,玩法,关闭}int buttonX[BUTTONNUM] = { 1042,1335,785 };
int buttonY[BUTTONNUM] = { 563,648,679};
int buttonR[BUTTONNUM] = { 145,93,85 };
int buttonTextSize[BUTTONNUM] = { 155,70,60 };
COLORREF buttonFillColor[BUTTONNUM] = { RGB(243, 113, 141) ,RGB(243, 113, 141) ,RGB(243, 113, 141) };
COLORREF buttonLineColor[BUTTONNUM] = { RGB(255, 225, 0) ,RGB(255, 225, 0) ,RGB(255, 225, 0) };
COLORREF buttonTextColor[BUTTONNUM] = { RGB(255, 225, 0) ,RGB(255, 225, 0) ,RGB(255, 225, 0) };
double buttonLineRate[BUTTONNUM] = { 0.1,0.1,0.1};
/***********************************/
struct CircleButton {int x;//圆心坐标int y;int r;//半径COLORREF fillColor;COLORREF lineColor;COLORREF textColor;int textSize;//字体大小double rate;//轮廓线粗细占半径的比例
}buttons[BUTTONNUM];
/***********************
*输入:按钮圆心横坐标,纵坐标,半径,填充色,轮廓色,文本内容,文本大小,轮廓线粗细占半径比例
*输出:空
*作用:产生任意位置、大小、填充颜色、轮廓颜色、文本的圆形按钮
************************/
void SingleButton(int x, int y,int radius , COLORREF fillColor, COLORREF lineColor, COLORREF textColor, const char* text,int textSize,double rate) {setbkmode(TRANSPARENT);//背景透明使文本背景不再是黑色//设置画线样式为宽度是半径的0.1倍的虚线,端点为圆形setlinestyle(PS_DASH | PS_ENDCAP_ROUND, (int) (rate*(double)radius) );setfillcolor(fillColor);//填充色setlinecolor(lineColor);//轮廓线的颜色fillcircle(x, y, radius);//画圆char word[50] = "";//用于接收输入的文本strcpy_s(word, text);//将输入的文本复制到Word中settextstyle(textSize, 0, "黑体");//字体格式int textX = x - textwidth(text) / 2;//位置居中int textY = y - textheight(text) / 2;settextcolor(textColor);//字体颜色outtextxy(textX, textY, text);//显示文本
}
/***********************
*输入:空
*输出:空
*作用:初始所有按钮信息
************************/
void ButtonInit() {for (int i = 0; i < BUTTONNUM; i++){buttons[i].x = buttonX[i];buttons[i].y = buttonY[i];buttons[i].r = buttonR[i];buttons[i].textSize = buttonTextSize[i];buttons[i].fillColor = buttonFillColor[i];buttons[i].lineColor = buttonLineColor[i];buttons[i].textColor = buttonTextColor[i];buttons[i].rate = buttonLineRate[i];}
}
/***********************
*输入:空
*输出:空
*作用:绘制出菜单界面中所有按钮
************************/
void DrawMenuButtons() {SingleButton(buttons[0].x, buttons[0].y, buttons[0].r, buttons[0].fillColor, buttons[0].lineColor,buttons[0].textColor, " GO!", buttons[0].textSize,buttons[0].rate);//绘制开始游戏按钮SingleButton(buttons[1].x, buttons[1].y, buttons[1].r, buttons[1].fillColor, buttons[1].lineColor,buttons[1].textColor, "离开", buttons[1].textSize, buttons[1].rate);//绘制退出游戏按钮SingleButton(buttons[2].x, buttons[2].y, buttons[2].r, buttons[1].fillColor, buttons[2].lineColor,buttons[2].textColor, "玩法", buttons[2].textSize, buttons[2].rate);//绘制玩法介绍按钮
}
/***********************
*输入:填充的新颜色,轮廓的新颜色,文本的新颜色
*输出:空
*作用:当鼠标指到菜单按钮时按钮进行变色以向用户反馈
************************/
void MouseOnMenuButtons(COLORREF newFillColor, COLORREF newLineColor, COLORREF newTextColor) {if (sqrt(pow((double)mouse.x - buttons[0].x, 2.0) + pow((double)mouse.y - buttons[0].y, 2.0)) <= buttons[0].r)SingleButton(buttons[0].x, buttons[0].y, buttons[0].r, newFillColor, newLineColor,newTextColor, " GO!", buttons[0].textSize, buttons[0].rate);//鼠标指开始按钮时的变色else if (sqrt(pow((double)mouse.x - buttons[1].x, 2.0) + pow((double)mouse.y - buttons[1].y, 2.0)) <= buttons[1].r)SingleButton(buttons[1].x, buttons[1].y, buttons[1].r, newFillColor, newLineColor,newTextColor, "离开", buttons[1].textSize, buttons[1].rate);//鼠标指离开按钮时的变色else if (sqrt(pow((double)mouse.x - buttons[2].x, 2.0) + pow((double)mouse.y - buttons[1].y, 2.0)) <= buttons[2].r)SingleButton(buttons[2].x, buttons[2].y, buttons[2].r, newFillColor, newLineColor,newTextColor, "玩法", buttons[2].textSize, buttons[2].rate);//鼠标指玩法按钮时的变色elseDrawMenuButtons();
}
使文本始终在按钮中央的几何计算:
以上是按钮的绘制,在添加鼠标左键检测时,(本项目圆形按钮)就是要保证鼠标光标的位置到圆心距离小于等于半径;
if (sqrt(pow((double)mouse.x - buttons[0].x, 2.0) + pow((double)mouse.y - buttons[0].y, 2.0)) <= buttons[0].r)PlayingGame();
由于像素坐标是整型的,所以为保证计算更加精确,要先将坐标转换成double类型。
玩法介绍界面
这部分主要有两部分组成:现将txt中内容读取到字符串中,再将字符串放在弹窗中显示出来。
/***********************
*输入:空
*输出:空
*作用:从文件中读取规则,产生一个有关规则介绍的弹窗
************************/
void RulesWindow() {HWND h = GetHWnd();//获取窗口句柄char ruleText[RULEMAX];FILE* fp; int k = 0;fopen_s(&fp,"./资源/Rules.txt","r");//打开存放规则文本的txt文件if (fp == NULL)exit(1);//打不开文件时直接停止运行else {if(fgets(ruleText,RULEMAX,fp)!=NULL)MessageBoxA(h, ruleText, "玩法简介", MB_OK);} fclose(fp);
}
游戏界面
玩家图片的透明背景输出
直接输出玩家飞机的图片的话,输出的样式是矩形的,影响美观。那么怎么能让计算机识别出背景并将其抠下来----掩码图和白底原图。
putimage(enemy[i].x, enemy[i].y, &enemyImg[2][0], NOTSRCERASE);
putimage(enemy[i].x, enemy[i].y, &enemyImg[2][1], SRCINVERT);
在同一位置先后粘贴掩码图和原图,就自然可以过滤掉背景。制作掩码图软件推荐:Photoshop。具体操作自行百度。
玩家的移动
当检测到相应方向按键后,玩家坐标向不同方位改变,一次改变多少像素来决定移动的速度。代码:
/***********************
*输入:(int类型)代表玩家飞机移动的速度
*输出:空
*作用:使玩家飞机移动
************************/
void MyPlaneMove(int speed) {//GetAsyncKeyState(_In_ int vKey);函数用于检测按键//且移动更加流畅,可斜着移动if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W')) {//大写W可同时表示W和wif(myPlane.y>0)//边界限制以防飞机移出界myPlane.y -= speed;//上移}if (GetAsyncKeyState(VK_DOWN) || GetAsyncKeyState('S')) {if(myPlane.y+ PLAYERHEIGHT<HEIGHT)myPlane.y += speed;//下移}if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')) {if(myPlane.x+ PLAYERWIDTH /2>0)myPlane.x -= speed;//左移}if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')) {if(myPlane.x+ PLAYERWIDTH / 2<WIDTH)myPlane.x += speed;//右移}//空格生成子弹//引入一定延迟防止按一下空格产生多个子弹,同时可以控制相邻子弹的密度if (GetAsyncKeyState(VK_SPACE) && Timer(150)) CreatBullet();
}
敌机的产生与移动
为方便对敌机的产生或消失的控制,在其结构体中添加bool live;true产生、false消失。产生坐标在窗体顶端即y=0;横坐标在可视范围内随机生成,这里用的rand()函数。
敌机产生后自动向下移动,即纵坐标+speed。(同玩家移动原理)。
代码:
/***********************
*输入:空
*输出:空
*作用:产生单个敌机
************************/
void CreatEnemy() {//敌机遍历for (int i = 0; i < ENEMYNUM; i++){if (!enemy[i].live) {//遍历到一个敌机的存活状态为false,生成该单个敌机switch (enemy[i].type) {//根据敌机类型决定血量case 0: enemy[i].hp = ENEMY0HP; break;case 1: enemy[i].hp = ENEMY1HP; break;case 2: enemy[i].hp = ENEMY2HP; break;case 3: enemy[i].hp = ENEMY3HP; break;}enemy[i].live = true;//改为存活//生成飞机位置在横轴上随机(范围:[0,WIDTH - enemy[i].enemyWidth]保证显示出完整的飞机)enemy[i].x = rand() % (WIDTH - enemy[i].enemyWidth);enemy[i].y = 0;//窗口最顶端上生成break;//生成单个飞机后跳出循环}}
}
/***********************
*输入:(int类型)表示敌机整体移动的速度(因为不同类型敌机移速不同)
*输出:空
*作用:使敌机移动
************************/
void EnemyMove(int speed) {for (int i = 0; i < ENEMYNUM; i++){if (enemy[i].live) {//敌机产生后要自动向下移动//两种移速方案
#if 1//现采取的方案:不同类型敌机用不同常数乘speed以区分出速度switch (enemy[i].type){// 0-->3 快-->慢 case 0:enemy[i].y += 5 * speed; break;case 1:enemy[i].y += 4 * speed; break;case 2:enemy[i].y += 3 * speed; break;case 3:enemy[i].y += 2 * speed; break;}
#elif 0//方案二:所有敌机速度随机(由于此方案移动不太流畅,未采用)enemy[i].y += (rand() % 10 + 1) * speed;
#endif//敌机完整地离开窗口后live恢复false以保证不断有敌机产生if (enemy[i].y - enemy[i].enemyHeight > HEIGHT){enemy[i].live = false;enemy[i].enemyDone = false;}}}
}
攻击系统
所谓攻击系统就是在飞机结构体中添加飞机的生命值,当玩家按空格绘制出一个子弹后,子弹自动向上移动,当子弹图片的区域与敌机图片区域有重叠(初中几何知识,在此不多赘述),则敌机Hp-1,子弹的live变为false即子弹打中敌机后消失。
/***********************
*输入:空
*输出:空
*作用:玩家飞机攻击系统
************************/
void Attack() {for (int i = 0; i < ENEMYNUM; i++)//遍历敌机{if (!enemy[i].live)continue;//跳过死亡敌机for (int j = 0; j < BULLETNUM; j++){//遍历子弹if (!bullet[j].live)continue;//跳过死亡的子弹//子弹与敌机一旦有重合区域则视为攻击有效(可用EDGE调整有效边缘)if ((bullet[j].x + BULLETWIDTH >= enemy[i].x -EDGE && bullet[j].x <= enemy[i].x + enemy[i].enemyWidth + EDGE)&& (bullet[j].y >= enemy[j].y -EDGE && bullet[j].y <= enemy[i].y + enemy[i].enemyHeight + EDGE)) {bullet[j].live = false;//攻击后子弹死亡enemy[i].hp--;//敌机减少一点血量}}if (enemy[i].hp <= 0)//敌机血量<=0后死亡enemy[i].live = false;}
}
玩家的血量控制
玩家掉血的条件是:一、敌方深入我方内部; 或 二、玩家飞机与敌机直接接触。
条件一:敌机.y>=窗口HEIGHT。条件二:玩家飞机图片与敌机图片有重合(同子弹与敌机的接触)。当触发扣血条件后玩家的hp-1,相应的左上角生命图片数量-1。
为避免同一敌机对玩家造成重复伤害,在结构体中加入:
bool enemyDone;//记录该敌机是否已经致使玩家扣血以防重复减血
初始时,enemyDone为false,即该敌机可以对玩家造成伤害。当同一敌机首次触发扣血条件后,enemyDone变为true,此时该敌机不能再对玩家造成伤害,直到完全离开窗口然后重新初始化。
游戏评分及结束界面
当玩家血量减到0,游戏结束。在这里用一个弹窗中断游戏的进行,并询问是否要再来一局。如果再来一局,则用goto语句跳转到游戏的开头,如果不再继续,则用stdlib.h里的exit() 函数退出程序。
该游戏的评分就是玩家坚持的时长,坚持时间越长,即得分越高。对游戏时间的计时这里用的time.h里的clock()函数。
/***********************
*输入:int类型 已经进行游戏的时间
*输出:unsigned int类型 如果游戏结束返回玩家对弹窗的选择,其他情况无意义
*作用:控制玩家什么时候减血或结束游戏
************************/
UINT PlayerBlood(int gameTime) {HWND h = GetHWnd();//获取窗口句柄for (int i = 0; i < ENEMYNUM; i++)//遍历敌机{ //如果某敌机存活并尚未致使玩家掉血if (enemy[i].live && !enemy[i].enemyDone) {//减血情况一:敌机深入我方内部if (enemy[i].y >= HEIGHT){myPlane.hp--;playerBlood--;enemy[i].enemyDone = true;}//减血情况二:玩家飞机与敌机直接接触if ((myPlane.x < enemy[i].x + enemy[i].enemyWidth) && (myPlane.x > enemy[i].x - PLAYERWIDTH)&& (myPlane.y < enemy[i].y + enemy[i].enemyHeight) && (myPlane.y + PLAYERHEIGHT > enemy[i].y)){myPlane.hp--;playerBlood--;enemy[i].enemyDone = true;}}}char chTime[15] = "";//接收转换成字符串类型后的游戏时间_itoa_s(gameTime,chTime,15,10);//_itoa_s函数将int类型转换成字符串类型//拼接成一个字符串char string1[100] = "游戏结束!太厉害了!本局中您已经坚持了";char string2[] = "秒!是否再来一局?";strcat(string1, chTime);strcat(string1, string2);if (myPlane.hp <= 0)//血量掉完后跳出游戏结束弹窗{mciSendStringA("close ./资源/战斗BGM.mp3", 0, 0, 0);mciSendStringA("open ./资源/游戏结束BGM.mp3", 0, 0, 0);mciSendStringA("play ./资源/游戏结束BGM.mp3", 0, 0, 0);UINT choice = MessageBoxA(h, string1, "游戏结束", MB_YESNO);return choice;}return 1;//其他路径中返回一个不影响YES/NO的值
}
背景音乐的播放
-
#include <mmsystem.h>//多媒体播放接口头文件 #pragma comment (lib,"winmm.lib")//加载静态库(用于播放音乐)
-
打开并播放音乐。
mciSendStringA("open ./资源/战斗BGM.mp3", 0, 0, 0);//打开游戏界面BGM mciSendStringA("play ./资源/战斗BGM.mp3 repeat", 0, 0, 0);//播放游戏界面BGM
源代码
/********************************************************* 程序目的:用C语言做一个飞机大战小游戏* 编译环境:visual studio 2019,EasyX_20210730* 作 者:曹谋仁(个人Blog:https://oceanbloom.github.io/)* 发布日期:2021/9/19********************************************************/#define _CRT_SECURE_NO_WARNINGS//防止对strcat()安全警告
#include <graphics.h>
#include <stdio.h>
#include <stdlib.h> // exit() 函数
#include <time.h>
#include <math.h>
#include <mmsystem.h>//多媒体播放接口头文件
#pragma comment (lib,"winmm.lib")//加载静态库(用于播放音乐)#define MYPLANEBLOOD 10
#define STARTDELAY 2000//开局敌机出没前的延迟(单位:ms)
#define RULEMAX 500//规则文本最大字数
#define BULLETNUM 100//一梭子弹的数量
#define ENEMYNUM 30//一波敌机的数量
#define EDGE 2//用于调整子弹命中敌机的有效边缘范围(单位:像素)//四种敌机血量宏定义
#define ENEMY0HP 2
#define ENEMY1HP 3
#define ENEMY2HP 4
#define ENEMY3HP 5#pragma region 图片资源尺寸
/************所有图片资源的尺寸************/
#define WIDTH 1522//窗口宽
#define HEIGHT 787//窗口高#define PLAYERWIDTH 97//玩家飞机图片宽
#define PLAYERHEIGHT 75//玩家飞机图片高#define BLOODWIDTH 39//生命值图片宽和高
#define BLOODHEIGHT 39#define BULLETWIDTH 30//玩家子弹图片宽
#define BULLETHEIGHT 60//玩家子弹图片高#define EWIDTH0 59//0号敌机图片宽
#define EHEIGHT0 42//0号敌机图片高#define EWIDTH1 80//1号敌机图片宽
#define EHEIGHT1 70//1号敌机图片高#define EWIDTH2 99//2号敌机图片宽
#define EHEIGHT2 75//2号敌机图片高#define EWIDTH3 125//3号敌机图片宽
#define EHEIGHT3 81//3号敌机图片高
/****************************************/
#pragma endregion#pragma region 按钮信息
/**************按钮信息************/
#define BUTTONNUM 3
//按钮顺序:{开始,离开,玩法,关闭}int buttonX[BUTTONNUM] = { 1042,1335,785 };
int buttonY[BUTTONNUM] = { 563,648,679};
int buttonR[BUTTONNUM] = { 145,93,85 };
int buttonTextSize[BUTTONNUM] = { 155,70,60 };
COLORREF buttonFillColor[BUTTONNUM] = { RGB(243, 113, 141) ,RGB(243, 113, 141) ,RGB(243, 113, 141) };
COLORREF buttonLineColor[BUTTONNUM] = { RGB(255, 225, 0) ,RGB(255, 225, 0) ,RGB(255, 225, 0) };
COLORREF buttonTextColor[BUTTONNUM] = { RGB(255, 225, 0) ,RGB(255, 225, 0) ,RGB(255, 225, 0) };
double buttonLineRate[BUTTONNUM] = { 0.1,0.1,0.1};
/***********************************/
#pragma endregionstruct Plane {int x; //横坐标int y; //纵坐标bool live; //是否存活int type; //飞机的类型,此处指几号敌机int hp; //血量,血量为0后死亡bool enemyDone;//记录该敌机是否已经致使玩家扣血以防重复减血int enemyWidth; //敌机图片宽int enemyHeight; //敌机图片高
}myPlane,bullet[BULLETNUM],enemy[ENEMYNUM];
//玩家飞机,存放子弹数据,存放敌机数据struct CircleButton {int x;//圆心坐标int y;int r;//半径COLORREF fillColor;COLORREF lineColor;COLORREF textColor;int textSize;//字体大小double rate;//轮廓线粗细占半径的比例
}buttons[BUTTONNUM];int playerBlood = MYPLANEBLOOD;//玩家血量
ExMessage mouse;//记录鼠标消息
IMAGE menuBackground;//存放游戏菜单界面背景图
IMAGE playingBackground;//存放游戏中背景图
IMAGE playerImg[2];//存放玩家飞机的图片
IMAGE playerBloodImg[2];//存放玩家生命图片
IMAGE bulletImg[2];//存放玩家子弹图片
IMAGE enemyImg[4][2];//存放敌机图片资源/***********************
*输入:按钮圆心横坐标,纵坐标,半径,填充色,轮廓色,文本内容,文本大小,轮廓线粗细占半径比例
*输出:空
*作用:产生任意位置、大小、填充颜色、轮廓颜色、文本的圆形按钮
************************/
void SingleButton(int x, int y,int radius , COLORREF fillColor, COLORREF lineColor, COLORREF textColor, const char* text,int textSize,double rate) {setbkmode(TRANSPARENT);//背景透明使文本背景不再是黑色//设置画线样式为宽度是半径的0.1倍的虚线,端点为圆形setlinestyle(PS_DASH | PS_ENDCAP_ROUND, (int) (rate*(double)radius) );setfillcolor(fillColor);//填充色setlinecolor(lineColor);//轮廓线的颜色fillcircle(x, y, radius);//画圆char word[50] = "";//用于接收输入的文本strcpy_s(word, text);//将输入的文本复制到Word中settextstyle(textSize, 0, "黑体");//字体格式int textX = x - textwidth(text) / 2;//位置居中int textY = y - textheight(text) / 2;settextcolor(textColor);//字体颜色outtextxy(textX, textY, text);//显示文本
}/***********************
*输入:空
*输出:空
*作用:初始所有按钮信息
************************/
void ButtonInit() {for (int i = 0; i < BUTTONNUM; i++){buttons[i].x = buttonX[i];buttons[i].y = buttonY[i];buttons[i].r = buttonR[i];buttons[i].textSize = buttonTextSize[i];buttons[i].fillColor = buttonFillColor[i];buttons[i].lineColor = buttonLineColor[i];buttons[i].textColor = buttonTextColor[i];buttons[i].rate = buttonLineRate[i];}
}/***********************
*输入:空
*输出:空
*作用:绘制出菜单界面中所有按钮
************************/
void DrawMenuButtons() {SingleButton(buttons[0].x, buttons[0].y, buttons[0].r, buttons[0].fillColor, buttons[0].lineColor,buttons[0].textColor, " GO!", buttons[0].textSize,buttons[0].rate);//绘制开始游戏按钮SingleButton(buttons[1].x, buttons[1].y, buttons[1].r, buttons[1].fillColor, buttons[1].lineColor,buttons[1].textColor, "离开", buttons[1].textSize, buttons[1].rate);//绘制退出游戏按钮SingleButton(buttons[2].x, buttons[2].y, buttons[2].r, buttons[1].fillColor, buttons[2].lineColor,buttons[2].textColor, "玩法", buttons[2].textSize, buttons[2].rate);//绘制玩法介绍按钮
}/***********************
*输入:填充的新颜色,轮廓的新颜色,文本的新颜色
*输出:空
*作用:当鼠标指到菜单按钮时按钮进行变色以向用户反馈
************************/
void MouseOnMenuButtons(COLORREF newFillColor, COLORREF newLineColor, COLORREF newTextColor) {if (sqrt(pow((double)mouse.x - buttons[0].x, 2.0) + pow((double)mouse.y - buttons[0].y, 2.0)) <= buttons[0].r)SingleButton(buttons[0].x, buttons[0].y, buttons[0].r, newFillColor, newLineColor,newTextColor, " GO!", buttons[0].textSize, buttons[0].rate);//鼠标指开始按钮时的变色else if (sqrt(pow((double)mouse.x - buttons[1].x, 2.0) + pow((double)mouse.y - buttons[1].y, 2.0)) <= buttons[1].r)SingleButton(buttons[1].x, buttons[1].y, buttons[1].r, newFillColor, newLineColor,newTextColor, "离开", buttons[1].textSize, buttons[1].rate);//鼠标指离开按钮时的变色else if (sqrt(pow((double)mouse.x - buttons[2].x, 2.0) + pow((double)mouse.y - buttons[1].y, 2.0)) <= buttons[2].r)SingleButton(buttons[2].x, buttons[2].y, buttons[2].r, newFillColor, newLineColor,newTextColor, "玩法", buttons[2].textSize, buttons[2].rate);//鼠标指玩法按钮时的变色elseDrawMenuButtons();
}/***********************
*输入:空
*输出:空
*作用:加载图片资源
************************/
void Loading() {//加载背景图loadimage(&playingBackground, "./资源/背景.png");//加载玩家掩码图+原图loadimage(&playerImg[1], "./资源/玩家.png");loadimage(&playerImg[0], "./资源/玩家(掩码图).png");//加载玩家生命图片loadimage(&playerBloodImg[1], "./资源/生命(原图).png");loadimage(&playerBloodImg[0], "./资源/生命(掩码图).png");//加载子弹掩码图+原图loadimage(&bulletImg[0], "./资源/子弹1(掩码图).png");loadimage(&bulletImg[1], "./资源/子弹1(原图).png");//加载敌机掩码图+原图loadimage(&enemyImg[0][0], "./资源/敌机0(掩码图).png");loadimage(&enemyImg[0][1], "./资源/敌机0(原图).png");loadimage(&enemyImg[1][0], "./资源/敌机1(掩码图).png");loadimage(&enemyImg[1][1], "./资源/敌机1(原图).png");loadimage(&enemyImg[2][0], "./资源/敌机2(掩码图).png");loadimage(&enemyImg[2][1], "./资源/敌机2(原图).png");loadimage(&enemyImg[3][0], "./资源/敌机3(掩码图).png");loadimage(&enemyImg[3][1], "./资源/敌机3(原图).png");
}/***********************
*输入:空
*输出:空
*作用:初始化敌机数据,飞机的类型按既定比率随机生成
************************/
void EnemyInit() {int ranNum;//随机数声明for (int i = 0; i < ENEMYNUM; i++)//敌机遍历{ranNum = rand() % 10;//0-9随机数if (ranNum <= 2) {//随机数为0、1、2时,初始为0号敌机enemy[i].hp = ENEMY0HP;//0号敌机血量enemy[i].type = 0;//0号敌机//0号敌机的宽和高enemy[i].enemyWidth = EWIDTH0;enemy[i].enemyHeight = EHEIGHT0;}else if (ranNum <= 5) {//随机数为3、4、5时,初始为1号敌机enemy[i].hp = ENEMY1HP;//1号敌机血量enemy[i].type = 1;//1号敌机//1号敌机的宽和高enemy[i].enemyWidth = EWIDTH1;enemy[i].enemyHeight = EHEIGHT1;}else if (ranNum <= 7) {//随机数为6、7时,初始为2号敌机enemy[i].hp = ENEMY2HP;//2号敌机血量enemy[i].type = 2;//2号敌机//2号敌机的宽和高enemy[i].enemyWidth = EWIDTH2;enemy[i].enemyHeight = EHEIGHT2;}else if (ranNum <= 9) {//随机数为8、9时,初始为3号敌机enemy[i].hp = ENEMY3HP;//3号敌机血量enemy[i].type = 3;//3号敌机//3号敌机的宽和高enemy[i].enemyWidth = EWIDTH3;enemy[i].enemyHeight = EHEIGHT3;}}
}/***********************
*输入:空
*输出:空
*作用:游戏初始化函数
************************/
void GameInit() {//玩家飞机初始位置为游戏窗口底部居中myPlane.x = (WIDTH - PLAYERWIDTH) / 2;myPlane.y = HEIGHT - PLAYERHEIGHT;myPlane.live = true;//存活状态:trueplayerBlood = MYPLANEBLOOD;myPlane.hp = playerBlood;//初始子弹for (int i = 0; i < BULLETNUM; i++){bullet[i].live = false;bullet[i].x = 0;bullet[i].y = 0;}for (int i = 0; i < ENEMYNUM; i++){//初始状态,所有敌机均未存活。随后逐个生成enemy[i].live = false;enemy[i].enemyDone = false;//初始时所有飞机都没有使玩家减血}EnemyInit();//初始敌机数据
}/***********************
*输入:空
*输出:空
*作用:产生单个敌机
************************/
void CreatEnemy() {//敌机遍历for (int i = 0; i < ENEMYNUM; i++){if (!enemy[i].live) {//遍历到一个敌机的存活状态为false,生成该单个敌机switch (enemy[i].type) {//根据敌机类型决定血量case 0: enemy[i].hp = ENEMY0HP; break;case 1: enemy[i].hp = ENEMY1HP; break;case 2: enemy[i].hp = ENEMY2HP; break;case 3: enemy[i].hp = ENEMY3HP; break;}enemy[i].live = true;//改为存活//生成飞机位置在横轴上随机(范围:[0,WIDTH - enemy[i].enemyWidth]保证显示出完整的飞机)enemy[i].x = rand() % (WIDTH - enemy[i].enemyWidth);enemy[i].y = 0;//窗口最顶端上生成break;//生成单个飞机后跳出循环}}
}/***********************
*输入:空
*输出:空
*作用:产生单个子弹
************************/
void CreatBullet() {for (int i = 0; i < BULLETNUM; i++)//遍历一梭子弹{if (!bullet[i].live) {//遍历到一个子弹的存活状态为false,生成该单个子弹bullet[i].live = true;//改为存活//产生的位置是玩家飞机顶部中间bullet[i].x = myPlane.x + PLAYERWIDTH / 2 - BULLETWIDTH / 2;bullet[i].y = myPlane.y - BULLETHEIGHT; break;//生成单个子弹后跳出循环}}
}/***********************
*输入:空
*输出:空
*作用:绘制游戏图像
************************/
void GameDraw() {int bloodX = 0;Loading();//加载图片资源//贴背景图putimage(0, 0, &playingBackground);//贴生命值图片for (int i = 0; i < playerBlood; i++){putimage(bloodX, 0, &playerBloodImg[0], NOTSRCERASE);putimage(bloodX, 0, &playerBloodImg[1], SRCINVERT);bloodX += BLOODWIDTH+4;}//贴玩家掩码图+原图if (myPlane.hp > 0) {putimage(myPlane.x, myPlane.y, &playerImg[0], NOTSRCERASE);putimage(myPlane.x, myPlane.y, &playerImg[1], SRCINVERT);}//贴生成子弹的图片for (int i = 0; i < BULLETNUM; i++){if (bullet[i].live) {putimage(bullet[i].x, bullet[i].y, &bulletImg[0], NOTSRCERASE);putimage(bullet[i].x, bullet[i].y, &bulletImg[1], SRCINVERT);}}//贴生成敌机的图片for (int i = 0; i < ENEMYNUM; i++){if (enemy[i].live == true) {switch (enemy[i].type) {case 0:putimage(enemy[i].x, enemy[i].y, &enemyImg[0][0], NOTSRCERASE);putimage(enemy[i].x, enemy[i].y, &enemyImg[0][1], SRCINVERT); break;case 1:putimage(enemy[i].x, enemy[i].y, &enemyImg[1][0], NOTSRCERASE);putimage(enemy[i].x, enemy[i].y, &enemyImg[1][1], SRCINVERT); break;case 2:putimage(enemy[i].x, enemy[i].y, &enemyImg[2][0], NOTSRCERASE);putimage(enemy[i].x, enemy[i].y, &enemyImg[2][1], SRCINVERT); break;case 3:putimage(enemy[i].x, enemy[i].y, &enemyImg[3][0], NOTSRCERASE);putimage(enemy[i].x, enemy[i].y, &enemyImg[3][1], SRCINVERT); break;}}}}/***********************
*输入:(int类型)延迟的时间,单位:ms
*输出:(bool类型)时间到->true; 否则->false
*作用:计时器
************************/
bool Timer(int delay) {static DWORD t1, t2;if (unsigned(t2 - t1) > unsigned(delay)) {t1 = t2;return true;}t2 = clock();return false;
}/***********************
*输入:(int类型)代表玩家飞机移动的速度
*输出:空
*作用:使玩家飞机移动
************************/
void MyPlaneMove(int speed) {//GetAsyncKeyState(_In_ int vKey);函数用于检测按键//且移动更加流畅,可斜着移动if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W')) {//大写W可同时表示W和wif(myPlane.y>0)myPlane.y -= speed;//上移}if (GetAsyncKeyState(VK_DOWN) || GetAsyncKeyState('S')) {if(myPlane.y+ PLAYERHEIGHT<HEIGHT)myPlane.y += speed;//下移}if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')) {if(myPlane.x+ PLAYERWIDTH /2>0)myPlane.x -= speed;//左移}if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')) {if(myPlane.x+ PLAYERWIDTH / 2<WIDTH)myPlane.x += speed;//右移}//空格生成子弹//引入一定延迟防止按一下空格产生多个子弹,同时可以控制相邻子弹的密度if (GetAsyncKeyState(VK_SPACE) && Timer(150)) CreatBullet();
}/***********************
*输入:(int类型)代表子弹移动的速度
*输出:空
*作用:使子弹移动
************************/
void BulletMove(int speed) {for (int i = 0; i < BULLETNUM; i++){if (bullet[i].live) {//存活子弹要自动向上移动bullet[i].y -= speed;//上移if (bullet[i].y + BULLETHEIGHT < 0)//子弹完全移出窗口后恢复false存活状态,以保持无限子弹bullet[i].live = false;}}
}/***********************
*输入:(int类型)表示敌机整体移动的速度(因为不同类型敌机移速不同)
*输出:空
*作用:使敌机移动
************************/
void EnemyMove(int speed) {for (int i = 0; i < ENEMYNUM; i++){if (enemy[i].live) {//敌机产生后要自动向下移动//两种移速方案
#if 1//现采取的方案:不同类型敌机用不同常数乘speed以区分出速度switch (enemy[i].type){// 0-->3 快-->慢 case 0:enemy[i].y += 5 * speed; break;case 1:enemy[i].y += 4 * speed; break;case 2:enemy[i].y += 3 * speed; break;case 3:enemy[i].y += 2 * speed; break;}
#elif 0//方案二:所有敌机速度随机(由于此方案移动不太流畅,未采用)enemy[i].y += (rand() % 10 + 1) * speed;
#endif//敌机完整地离开窗口后live恢复false以保证不断有敌机产生if (enemy[i].y - enemy[i].enemyHeight > HEIGHT){enemy[i].live = false;enemy[i].enemyDone = false;}}}
}/***********************
*输入:空
*输出:空
*作用:玩家飞机攻击系统
************************/
void Attack() {for (int i = 0; i < ENEMYNUM; i++)//遍历敌机{if (!enemy[i].live)continue;//跳过死亡敌机for (int j = 0; j < BULLETNUM; j++){//遍历子弹if (!bullet[j].live)continue;//跳过死亡的子弹//子弹与敌机一旦有重合区域则视为攻击有效(可用EDGE调整有效边缘)if ((bullet[j].x + BULLETWIDTH >= enemy[i].x -EDGE && bullet[j].x <= enemy[i].x + enemy[i].enemyWidth + EDGE)&& (bullet[j].y >= enemy[j].y -EDGE && bullet[j].y <= enemy[i].y + enemy[i].enemyHeight + EDGE)) {bullet[j].live = false;//攻击后子弹死亡enemy[i].hp--;//敌机减少一点血量}}if (enemy[i].hp <= 0)//敌机血量<=0后死亡enemy[i].live = false;}
}/***********************
*输入:int类型 已经进行游戏的时间
*输出:unsigned int类型 如果游戏结束返回玩家对弹窗的选择,其他情况无意义
*作用:控制玩家什么时候减血或结束游戏
************************/
UINT PlayerBlood(int gameTime) {HWND h = GetHWnd();//获取窗口句柄for (int i = 0; i < ENEMYNUM; i++)//遍历敌机{ //如果某敌机存活并尚未致使玩家掉血if (enemy[i].live && !enemy[i].enemyDone) {//减血情况一:敌机深入我方内部if (enemy[i].y >= HEIGHT){myPlane.hp--;playerBlood--;enemy[i].enemyDone = true;}//减血情况二:玩家飞机与敌机直接接触if ((myPlane.x < enemy[i].x + enemy[i].enemyWidth) && (myPlane.x > enemy[i].x - PLAYERWIDTH)&& (myPlane.y < enemy[i].y + enemy[i].enemyHeight) && (myPlane.y + PLAYERHEIGHT > enemy[i].y)){myPlane.hp--;playerBlood--;enemy[i].enemyDone = true;}}}char chTime[15] = "";//接收转换成字符串类型后的游戏时间_itoa_s(gameTime,chTime,15,10);//_itoa_s函数将int类型转换成字符串类型//拼接成一个字符串char string1[100] = "游戏结束!太厉害了!本局中您已经坚持了";char string2[] = "秒!是否再来一局?";strcat(string1, chTime);strcat(string1, string2);if (myPlane.hp <= 0)//血量掉完后跳出游戏结束弹窗{mciSendStringA("close ./资源/战斗BGM.mp3", 0, 0, 0);mciSendStringA("open ./资源/游戏结束BGM.mp3", 0, 0, 0);mciSendStringA("play ./资源/游戏结束BGM.mp3", 0, 0, 0);UINT choice = MessageBoxA(h, string1, "游戏结束", MB_YESNO);return choice;}return 1;//其他路径中返回一个不影响YES/NO的值
}/***********************
*输入:int类型水印横坐标、int类型水印纵坐标、int类型水印字体尺寸、水印颜色
*输出:空
*作用:在任意位置生成任意大小、颜色的文本充当水印
************************/
void WaterMark(int textX,int textY, int textSize,COLORREF textColor) {setbkmode(TRANSPARENT);//背景透明使文本背景不再是黑色settextstyle(textSize, 0, "隶书");//字体格式settextcolor(textColor);//字体颜色outtextxy(textX, textY, "By 曹谋仁");//显示文本
}/***********************
*输入:空
*输出:空
*作用:游戏菜单界面
************************/
void Menu() {ButtonInit();mciSendStringA("open ./资源/菜单BGM.mp3", 0, 0, 0);//打开菜单界面BGMloadimage(&menuBackground, "./资源/menuBackground.png");putimage(0, 0, &menuBackground);DrawMenuButtons();mciSendStringA("play ./资源/菜单BGM.mp3 repeat", 0, 0, 0);//播放音乐WaterMark(2, 763, 25, RGB(0, 0, 0));//在左下角显示水印
}/***********************
*输入:空
*输出:空
*作用:玩游戏中的全过程
************************/
void PlayingGame() {mciSendStringA("close ./资源/菜单BGM.mp3", 0, 0, 0);//关闭菜单界面BGMmciSendStringA("open ./资源/战斗BGM.mp3", 0, 0, 0);//打开游戏界面BGM
L0:GameInit();//初始化游戏mciSendStringA("play ./资源/战斗BGM.mp3 repeat", 0, 0, 0);//播放游戏界面BGMBeginBatchDraw();//开启批量绘制,使循环中图像显示流畅int playTime = 0;//用于记录已经进行游戏的时间,同时也是得分UINT endChoice;//记录结束窗口中按钮的选择DWORD startTime=clock(), endTime=clock();while (1) {//经过开局延迟时间后产生敌机,产生两个敌机之间间隔0.65秒if (Timer(650) && unsigned(endTime - startTime) > STARTDELAY) {CreatEnemy();}GameDraw();//绘图FlushBatchDraw();//刷新MyPlaneMove(22);//玩家飞机移动endChoice = PlayerBlood(playTime);if (endChoice == IDYES){mciSendStringA("close ./资源/游戏结束BGM.mp3", 0, 0, 0);goto L0;//再来一局后重新开始}if (endChoice == IDNO){mciSendStringA("close ./资源/战斗BGM.mp3", 0, 0, 0);//关闭游戏界面BGMmciSendStringA("close ./资源/游戏结束BGM.mp3", 0, 0, 0);exit(1);//结束游戏终止程序}BulletMove(12);//子弹移动EnemyMove(2);//敌机移动Attack();//攻击endTime = clock();playTime = ((int)endTime-(int)startTime) / 1000;}EndBatchDraw();//结束批量绘制
}/***********************
*输入:空
*输出:空
*作用:从文件中读取规则,产生一个有关规则介绍的弹窗
************************/
void RulesWindow() {HWND h = GetHWnd();//获取窗口句柄char ruleText[RULEMAX];FILE* fp; int k = 0;fopen_s(&fp,"./资源/Rules.txt","r");//打开存放规则文本的txt文件if (fp == NULL)exit(1);//打不开文件时直接停止运行else {if(fgets(ruleText,RULEMAX,fp)!=NULL)MessageBoxA(h, ruleText, "玩法简介", MB_OK);} fclose(fp);
}//主函数
int main() {initgraph(WIDTH, HEIGHT);//绘制窗口Menu();BeginBatchDraw();//开启批量绘制,使循环中图像显示流畅while (1) {FlushBatchDraw();//刷新if (peekmessage(&mouse, EM_MOUSE)) {//获取鼠标信息//菜单界面中,当鼠标指针移动到按钮上时会变色以反馈用户MouseOnMenuButtons(RGB(56, 199, 170), RGB(216, 120, 147), RGB(216, 120, 147));if (mouse.message == WM_LBUTTONDOWN) {//按下按钮时//点击"Go!"按钮if (sqrt(pow((double)mouse.x - buttons[0].x, 2.0) + pow((double)mouse.y - buttons[0].y, 2.0)) <= buttons[0].r)PlayingGame();//点击"离开"按钮if (sqrt(pow((double)mouse.x - buttons[1].x, 2.0) + pow((double)mouse.y - buttons[1].y, 2.0)) <= buttons[1].r){mciSendStringA("close ./资源/菜单BGM.mp3", 0, 0, 0);//关闭菜单界面BGMreturn 0;}//点击"玩法"按钮if (sqrt(pow((double)mouse.x - buttons[2].x, 2.0) + pow((double)mouse.y - buttons[2].y, 2.0)) <= buttons[2].r)RulesWindow();}}}EndBatchDraw();//结束批量绘制return 0;
}
一些细节&技巧
icon图标的制作与插入
-
找一张或画一张图片,如果想用背景是透明的,icon支持阿尔法透明通道,所以可以用Photoshop将背景做成透明通道。随后规范其尺寸大小,常用的有12×12、16×16、24×24、32×32、48×48等。
-
将制作好的JPG或PNG导入转换成ico格式。我用的网站是→在线ico图标转换工具
-
在visual studio中右栏资源文件中添加制作好的ico,添加成功后编译一次exe文件的图标就会变成指定图标了。(下面图片是我链接到桌面的)
游戏的素材收集
素材网站推荐→爱给网
封面的平面设计
推荐网站→Fotor 平面设计
易错集
-
由于EasyX图形库只针对C++,所以源文件后缀必须是cpp,.c文件会报错。
-
loadimage(&playingBackground, "./资源/背景.png");
报错:两个重载中没有一个可以转换所有参数类型。
原因:因字符集不对导致的参数有误。
解决方法:
- 方法一:项目→属性→常规→字符集→使用多字节字符集。
- 方法二:在字符串前面加上大写的L。
- 方法三:用TEXT(_T())把字符串包起起来。
-
在使用部分函数时(如:strcat()、fopen()、scanf()等函数)会有安全警告,导致无法正常运行。这是因为这些函数可能会导致数组溢出或者缓冲区溢出。
解决方法:
-
方法一:在最顶端加入一行:
#define _CRT_SECURE_NO_WARNINGS
就是告诉Visual Studio不要在警告,并继续使用该函数。
-
方法二:使用微软推荐的函数:如:scanf_s()、gets_s()、fgets_s()、strcpy_s()、strcat_s() 等。这些函数均比原先的安全,但是这些函数仅限于VS,在其他编译器中无效。
-
存在的缺陷
- (较严重的bug)当一直长按空格连续发射子弹时,就不会有新的敌机产生。
- 有关菜单中玩法介绍的弹窗中,有两个缺陷:
- 从txt文件中读取规则文本时,我用的是只读取第一行,所以全部规则介绍的文本都挤在这一行,看起来很不舒服。
- 目前我还不会对messagebox弹窗中的文本排版,所以弹窗中文本不同规则只以几个空格分隔,没有换行,看起来很乱。(\n、rn都试了还是不能换行,不知道为什么。求大佬指点~)
- 游戏玩法系统上比较单一,既没有关卡或BOSS,也没有技能或buff加持。
- 游戏进行中没有暂停功能,也没有调节背景音乐的功能。
- 目前游戏整体还很粗糙,还有很多细节需要去优化。比如子弹与敌机碰撞时、敌机或玩家死亡时、玩家发射子弹时看起来很生硬,都缺少音效和动画。当然,未完善更多细节也需要我学习更多新知识,以目前的水平暂时做不到的。
参考资料
- https://www.bilibili.com/video/BV1T3411r7dP
- https://docs.easyx.cn/zh-cn/intro