我的个人博客:谋仁·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

用C语言实现飞机大战小游戏相关推荐

  1. C语言实现飞机大战小游戏

    先看一下效果: 初始游戏界面具有多个选项: 且具有多个可变设置项,方便个性化调整: 多个模式进行选择: 正式游戏界面,操作流畅,界面简单,且有操作提示: 使用函数封装代码和逻辑控制衔接每个界面,实现连 ...

  2. C语言写飞机大战小游戏(有音乐背景和图片)

    大家好 , 我是逼哥 , 记得每天好好学习 , 天天向上 , 尤其是大学生 . 不要荒废学业. 首先说明 , 我使用的开发环境是  vs2017  , 有些函数方法可能不通用 ,大家可以百度下其他方法 ...

  3. C语言—飞机大战小游戏

    哈工大经典C语言大作业-飞机大战小游戏,源码如下,已经通过编译获得评分19+ (满分20)当时还是太菜了呜呜呜. 可以给大家参考一下,好像本来是加了音乐的,但是你们可能没有对应的音乐MP3文件,所以如 ...

  4. 基于Java语言在窗体上实现飞机大战小游戏

    全套资料下载地址:https://download.csdn.net/download/sheziqiong/85594271 项目介绍 飞机大战:用 Java 语言在窗体上实现飞机大战小游戏,运行程 ...

  5. 华为官方解析开源鸿蒙 OpenHarmony 3.1关键特性画布,教你如何完成飞机大战小游戏

    华为技术有限公司的江英杰为大家揭晓了关于开源鸿蒙 OpenHarmony 3.1 Beta 版中的一个关键特性,也就是 ArkUI 开发框架中的 canvas 画布. 据介绍,canvas 是 Ark ...

  6. canvas绘制“飞机大战”小游戏,真香

    canvas是ArkUI开发框架里的画布组件,常用于自定义绘制图形.因为其轻量.灵活.高效等优点,被广泛应用于UI界面开发中. 本期,我们将为大家介绍canvas组件的使用. 目录 一.canvas介 ...

  7. 【python】飞机大战小游戏练习

    飞机大战小游戏练习 一.前提准备 二.制作步骤 1.库的导入与初始化 2.窗口操作 3.键盘按键监听相关操作 4.添加游戏背景 5.加载玩家飞机 6.获取玩家飞机矩阵 三.完整代码编写 游戏背景类编写 ...

  8. python 飞机大战小游戏

    飞机大战小游戏,这里需要下载pygame模块 这是需要的素材,需要的自取: 上代码: import time import pygame from pygame.locals import *#检测事 ...

  9. 点击list view中一行内容可以在combox中显示_java版飞机大战小游戏详细教程(零基础小白也可以分分钟学会!)...

    一:游戏展示 飞机大战小游戏我们都玩过,通过移动飞机来打敌机,这里给大家展示一下游戏成果:呜呜呜由于gif只能上传5M大小,所以就不能给大家展示操作了,如果大家有兴趣可以自己自己做出来再玩哟. 这里面 ...

最新文章

  1. 又一数据挖掘赛事,在校生专属,翼支付杯来了(直通实习机会)
  2. 性能压测诡异的Requests/second 响应刺尖问题
  3. thinkphp 查找表并返回结果
  4. 在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?
  5. 【栈】【232. 用栈实现队列】【简单】
  6. Codeforces Round #108 (Div. 2)
  7. Openresty编写Lua代码一例
  8. 最新版python如何安装qt5_Python3 搭建Qt5 环境的方法示例
  9. Python基础(注释/算数运算符/变量类型/拼接字符串)
  10. 利用双向注意流进行机器理解
  11. 关于开源软件的十个问题(下篇)
  12. 用代码查询ASCII码和Unicode码表序号
  13. 计算机质量检测技术,计算机技术在建材质量检测中的应用分析
  14. 打印机与计算机无法进行通讯,打印时电脑提示:“打印机不能与计算机进行通讯”是为什么啊?是电脑出现问题还是打印机出现问题了?...
  15. Astah绘制UML图形
  16. 逆水寒官方网站服务器,《逆水寒》2019年7月4日更新公告
  17. 【AliOS Studio】AliOS Studio初体验
  18. sudo apt update时 E: 仓库 “http://mirrors.ustc.edu.cn/ros/ubuntu jammy Release” 没有 Release
  19. Photoshop 2023 (ps 2023)
  20. 多商户商城系统功能拆解39讲-平台端营销-砍价记录

热门文章

  1. 字符设备驱动——(应用控制)
  2. dw为什么保存了看不到图片了_100%保存QQ闪照
  3. VirtualBox虚拟机安装教程实用小技巧
  4. EOS开发技术资料汇总
  5. java 英雄联盟回合战斗_如何赢得一场英雄联盟的战斗新手入门篇
  6. 叮咚智能音箱怎样与台式计算机,播放音乐 - 叮咚智能音箱怎么用_叮咚智能音箱使用详细说明...
  7. 叮咚代shuaV1.0PJ版(去授权)
  8. 调用支付JSAPI缺少参数:appid
  9. 中国Linux联盟 - 圣诞节寄语
  10. 移动端/模拟器内安装完代理对应证书后仍警告安全证书有问题的解决方案