我的个人博客:谋仁·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(该项目的窗口大小,单位:像素)的窗口。

      1
      initgraph(1522, 787);

      例2:绘制一个宽×高为1522×787的窗口,同时显示控制台窗口。

      1
      initgraph(1522, 787, EW_SHOWCONSOLE);

      显示控制台窗口便于调试。

    • 加载图片–loadimage

      例:将菜单界面背景图加载出来。

      1
      2
      IMAGE menuBackground;//存放游戏菜单界面背景图
      loadimage(&menuBackground, "./资源/menuBackground.png");//加载背景图
    • 粘贴图片–putimage

      例:将加载好的菜单界面背景图片显示出来。

      1
      putimage(0, 0, &menuBackground);

      注:前两个参数分别表示横坐标、纵坐标。这里的坐标轴是以窗体左上角为原点。横坐标是操作对象的左上角到窗体左边的垂直距离,纵坐标是操作对象到窗体上边的垂直距离。

  • 关于颜色

    • 已经预定义的颜色

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      常量			值			颜色
      -------- -------- --------
      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值。

  • 图形的输出

    • 例:画一个虚线为轮廓且连接处为圆形的圆(本项目按钮样式)

      1
      2
      3
      4
      setlinestyle(PS_DASH | PS_ENDCAP_ROUND, 宽度像素 );
      setfillcolor(fillColor);//填充色
      setlinecolor(lineColor);//轮廓线的颜色
      fillcircle(x, y, radius);//画圆(横坐标,纵坐标,半径)
  • 文本的输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /***********************
    *输入:(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

      该函数有四个重载,这里只介绍一下本项目中使用的这一种。

      1
      void settextstyle(int nHeight,int nWidth,LPCTSTR lpszFace);

      nHeight–指定字符的高度(逻辑单位)。

      nWidth–字符的平均宽度(逻辑单位)。如果为 0,则比例自适应。(注:上方水印文本输出函数中第二个参数为0即比例自适应后,就可以直接调节第一个参数来调节整体文本的大小。)

      lpszFace–字体的种类,这里可以直接用中文加双引号来表示部分字体。

  • 如何更流畅地动态绘图?

    在设备上不断进行绘图操作时,会产生闪频现象。为了流畅的绘图,我们可以用下面两个函数处理。

    1
    2
    3
    BeginBatchDraw();//开始批量绘图
    //这里放绘图代码
    EndBatchDraw();//结束批量绘图
  • 如何进行鼠标的操作?

    • 存储鼠标信息的类型是ExMessage类型。故先建立记录鼠标信息的变量。

      1
      ExMessage mouse;//记录鼠标消息
    • 获取当前鼠标信息,并立即返回–peekmessage函数。

      1
      2
      3
      4
      if (peekmessage(&mouse, EM_MOUSE)) 
      {//如果获取到鼠标的信息,则进行这里的操作
      ...
      }

      该函数也可以通过改变第二个参数来获取不同的信息:

      标志 描述
      EM_MOUSE 鼠标消息。
      EM_KEY 按键消息。
      EM_CHAR 字符消息。
      EM_WINDOW 窗口消息。
    • 检测鼠标上的操作。

      1
      2
      3
      4
      5
      6
      if (peekmessage(&mouse, EM_MOUSE)) 
      {//如果获取到鼠标的信息,则进行这里的操作
      if (mouse.message == WM_LBUTTONDOWN) {
      //按下鼠标左键时进入这里
      }
      }

      当然,此函数不仅仅能检测到鼠标左键按下的信息,本项目关于鼠标只用到左键按下的操作,若想了解更详细→EasyX 文档 - ExMessage

  • 键盘上的操作

    该项目上用到的函数是:GetAsyncKeyState(键值);传入一个键值,若检测到按下则返回true。一些键值如下:

    • 上:VK_UP 下:VK_DOWN 左:VK_LEFT 右:VK_RIGHT
    • 如果是字母按键:’字母的大写’。如果是字母小写只能检测到小写,如果是字母大写,则大小写均能检测到。

有关EasyX图形库的基本操作就介绍到这,这里主要是本项目中用到的一些功能。相比整个图形库所有功能而言实乃九牛之一毛,冰山之一角。如果想深入了解更多,请点击这里跳转→EasyX 文档

菜单界面

菜单界面除了最基本的图片或文本的输出外,最主要的就是怎么实现一个按钮。

所谓的按钮就是绘制一个图形,图形中绘制一个按钮上的文本。然后在这个带有文本的图形上添加鼠标左键的监测信息。至此,一个按钮所具备的最基本的特征都已经完成了。在本项目菜单界面的按钮上,为了更好的反馈用户,增加了当鼠标指着按钮时,按钮会发生变色。源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**************按钮信息************/
#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();
}

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

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

1
2
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中内容读取到字符串中,再将字符串放在弹窗中显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/***********************
*输入:空
*输出:空
*作用:从文件中读取规则,产生一个有关规则介绍的弹窗
************************/
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);
}

游戏界面

玩家图片的透明背景输出

直接输出玩家飞机的图片的话,输出的样式是矩形的,影响美观。那么怎么能让计算机识别出背景并将其抠下来—-掩码图和白底原图。

1
2
putimage(enemy[i].x, enemy[i].y, &enemyImg[2][0], NOTSRCERASE);
putimage(enemy[i].x, enemy[i].y, &enemyImg[2][1], SRCINVERT);

在同一位置先后粘贴掩码图和原图,就自然可以过滤掉背景。制作掩码图软件推荐:Photoshop。具体操作自行百度。

玩家的移动

当检测到相应方向按键后,玩家坐标向不同方位改变,一次改变多少像素来决定移动的速度。代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/***********************
*输入:(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。(同玩家移动原理)。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/***********************
*输入:空
*输出:空
*作用:产生单个敌机
************************/
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即子弹打中敌机后消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/***********************
*输入:空
*输出:空
*作用:玩家飞机攻击系统
************************/
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。

为避免同一敌机对玩家造成重复伤害,在结构体中加入:

1
bool enemyDone;//记录该敌机是否已经致使玩家扣血以防重复减血

初始时,enemyDone为false,即该敌机可以对玩家造成伤害。当同一敌机首次触发扣血条件后,enemyDone变为true,此时该敌机不能再对玩家造成伤害,直到完全离开窗口然后重新初始化。

游戏评分及结束界面

当玩家血量减到0,游戏结束。在这里用一个弹窗中断游戏的进行,并询问是否要再来一局。如果再来一局,则用goto语句跳转到游戏的开头,如果不再继续,则用stdlib.h里的exit() 函数退出程序。

该游戏的评分就是玩家坚持的时长,坚持时间越长,即得分越高。对游戏时间的计时这里用的time.h里的clock()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/***********************
*输入: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的值
}

背景音乐的播放

  • 库文件

    1
    2
      #include <mmsystem.h>//多媒体播放接口头文件
    #pragma comment (lib,"winmm.lib")//加载静态库(用于播放音乐)
  • 打开并播放音乐。

    1
    2
    mciSendStringA("open ./资源/战斗BGM.mp3", 0, 0, 0);//打开游戏界面BGM
    mciSendStringA("play ./资源/战斗BGM.mp3 repeat", 0, 0, 0);//播放游戏界面BGM

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
/********************************************************
* 程序目的:用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文件会报错。

  • 字符集方面

    1
    loadimage(&playingBackground, "./资源/背景.png");

    报错:两个重载中没有一个可以转换所有参数类型。

    原因:因字符集不对导致的参数有误。

    解决方法:

    • 方法一:项目→属性→常规→字符集→使用多字节字符集。
  • 方法二:在字符串前面加上大写的L。

    • 方法三:用TEXT(_T())把字符串包起起来。
  • 在使用部分函数时(如:strcat()、fopen()、scanf()等函数)会有安全警告,导致无法正常运行。这是因为这些函数可能会导致数组溢出或者缓冲区溢出。

    解决方法:

    • 方法一:在最顶端加入一行:

      1
      #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加持。
  • 游戏进行中没有暂停功能,也没有调节背景音乐的功能。
  • 目前游戏整体还很粗糙,还有很多细节需要去优化。比如子弹与敌机碰撞时、敌机或玩家死亡时、玩家发射子弹时看起来很生硬,都缺少音效和动画。当然,未完善更多细节也需要我学习更多新知识,以目前的水平暂时做不到的。

参考资料