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


我的个人博客:谋仁·Blog
该项目已上传至GitHub:点击跳转



摘要

这是一个用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);//鼠标指玩法按钮时的变色
	else
		DrawMenuButtons();
}

使文本始终在按钮中央的几何计算:

以上是按钮的绘制,在添加鼠标左键检测时,(本项目圆形按钮)就是要保证鼠标光标的位置到圆心距离小于等于半径;

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和w
		if(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 endregion

struct 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);//鼠标指玩法按钮时的变色
	else
		DrawMenuButtons();
}

/***********************
*输入:空
*输出:空
*作用:加载图片资源
************************/
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;//存活状态:true
	playerBlood = 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和w
		if(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);//打开菜单界面BGM
	loadimage(&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);//关闭菜单界面BGM
	mciSendStringA("open ./资源/战斗BGM.mp3", 0, 0, 0);//打开游戏界面BGM
L0:GameInit();//初始化游戏
	mciSendStringA("play ./资源/战斗BGM.mp3 repeat", 0, 0, 0);//播放游戏界面BGM
	BeginBatchDraw();//开启批量绘制,使循环中图像显示流畅
	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);//关闭游戏界面BGM
			mciSendStringA("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);//关闭菜单界面BGM
					return 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加持。
  • 游戏进行中没有暂停功能,也没有调节背景音乐的功能。
  • 目前游戏整体还很粗糙,还有很多细节需要去优化。比如子弹与敌机碰撞时、敌机或玩家死亡时、玩家发射子弹时看起来很生硬,都缺少音效和动画。当然,未完善更多细节也需要我学习更多新知识,以目前的水平暂时做不到的。

参考资料

猜你喜欢

转载自blog.csdn.net/ZBC010/article/details/120401202