前言
在B站跟着顽石老师学习了Easy X图形编程及飞机大战游戏,老师讲课非常有趣,十分推荐,唯一缺憾的就是飞机大战的视频是好几年前的并且感觉没有更新完,好多功能没有实现,不过也有好有坏,倒逼着我去自己思考动手实现后面的一些功能,飞机大战视频p8游戏的优化和BUG处理最后将近10分钟老师调试代码处理BUG对于我这个初学者有很大的帮助。由于时间紧张原因并没有完成飞机大战所有功能,剩下的一些细节功能后续有时间会继续改进。
【C/C++/EasyX】学编程,做游戏,小白快速入门图形编程,零基础入门到精通,学习就是这么快乐_哔哩哔哩_bilibili
C语言项目:飞机大战【最全教程】超详细教程带你从 0 开始做出飞机游戏!_哔哩哔哩_bilibili
代码文件和图片音乐资源在我的Gitee的C/C++项目仓库里。
03_02_飞机大战 · 路远Oo/C-C++项目 - 码云 - 开源中国 (gitee.com)
一、前置知识准备
1.Easy X图形库
针对C++的图形库,基于Windows图形编程,将Windows下的复杂程序过程进行封装,将Windows下的编程过程隐藏,给用户提供一个简单熟悉的接口。用户对于图形库中函数的调用,最终都会由Windows的底层API实现。可以用于编写小游戏、图形学、图像学、分形学、粒子系统、物理模拟等各种场景。 通过Easy X提供的函数,可以实现游戏窗口的绘制,并在窗口内进行图形和文字绘制及图像处理,可以通过鼠标消息函数和键盘消息函数对窗口内部的物体进行控制。 EasyX基础入门——这一篇就够啦-CSDN博客
2.图片
2.1图片存储 图片在计算机中由一个个像素点组成,以二进制的方式存储。 在RGB格式中,每个像素点用一个int整型类型表示,共4个字节32位比特位。第一个字节不存储数据用作占位符,剩下的三个字节各对应一个原色(红绿蓝),每一个原色取值范围为0~255。红色RGB(255,0,0),绿色RGB(0,255,0),蓝色RGB(0,0,255);白色RGB(255,255,255),黑色RGB(0,0,0)。 2.2掩码图 掩码是一串二进制代码对目标字段进行位与运算,屏蔽当前的输入位。将源码与掩码经过按位运算或逻辑运算得出新的操作数。 图像的掩码操作是指通过掩码核算子重新计算图像中各个像素的值,掩码核算子刻画邻域像素点对新像素值的影响程度,同时根据掩码算子中权重因子对原像素点进行加权平均。 2.3透明贴图技术 用掩码图和原图做一个颜色的二进制操作。 Easy X使用三元光栅操作实现透明贴图,将源图像与目标图像的像素进行位的逻辑运算(与或非)。 详解透明贴图和三元光栅操作 - CodeBus
二、飞机大战游戏思路
1.窗口制作
根据背景图尺寸利用窗口函数initgraph()绘制默认窗口;设置IMAGE对象,通过loadimage()函数把图片加载进程序,putimage()函数把图片放入指定位置(用来放置背景图片以及游戏所需的飞机子弹等素材的图片)。
initgraph(WIDTH, HEIGHT, SHOWCONSOLE);
//例如
IMAGE bk;//背景图片
loadimage(&bk, "./images/background.jpg");
putimage(0, 0, &bk);
2.数据及参数
创建结构体存储玩家飞机、子弹、敌机的各种属性:坐标点、长度、宽度、是否存活、数量、血量等信息。
struct plane//玩家飞机
{
int x;
int y;
bool live;//是否存活
int width;
int height;
int hp;//血量
int type;//敌机类型 big small
}player,enemy[ENEMY_NUM];
3.素材载入
用一个2个空间数组定义飞机的图片,(其他素材同理)分别加载飞机的掩码图和原图,就可以在窗口输出无背景的素材图片。
//例如
IMAGE img_role[2];//玩家图片
//加载玩家图片
loadimage(&img_role[0], "./images/planeNormal_1.jpg");//掩码图
loadimage(&img_role[1], "./images/planeNormal_2.jpg");//原图
//绘制玩家飞机
//透明贴图技术:用掩码图和原图做一个颜色的二进制操作
putimage(player.x, player.y, &img_role[0], NOTSRCERASE);
putimage(player.x, player.y, &img_role[1], SRCINVERT);
4.初始化
根据不同角色设置不同的参数。
//例如
//玩家初始化
player.x = WIDTH / 2;
player.y = HEIGHT - 120;
player.live = true;
5.素材移动
通过键盘操作可以控制玩家飞机移动,子弹自动向上移动和敌机自动向下移动。 第一种方式:_getch()函数 需要头文件conio.h。_getch()是阻塞函数,和scanf一样,没有输入就会卡住主程序等待输入,不是C语言的标准函数。 配合_kbhit()检测是否有键盘按下,加上switch语句添加操控的具体的按键。 第二种方式:GetAsyncKeyState()函数 需要头文件windows.h,但是由于EasyX包含了windows头文件,所以无须自己包含。非阻塞函数,非常流畅。 如果要检测字母按键,必须用大写,这样大小写都可以检测到,小写一个都检测不到;或者使用定义好的宏VK_UP、VK_DOWN、VK_LEFT、VK_RIGHT等。
//例如
//通过获取上键或W键使玩家向上移动
if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W'))//VK_UP系统定义的宏
{
if (player.y > 0)//边界处理
{
player.y -= speed;
}
}
6.双缓冲绘图
基于C语言的游戏开发的动态画面往往是不断的刷新显示区来实现的,即不断地输入和清空。画面较大的情况下会有闪屏或不流畅。 Easy X提供BeginBatchDraw函数,这个函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出。以实现批绘图功能,可以消除闪烁。
BeginBatchDraw();
while (1)
{
//绘制游戏图像的函数
FlushBatchDraw();
//游戏实现的代码
}
EndBatchDraw();
7.创造子弹和敌机
使子弹和敌机能够出现在屏幕上,并且不断更新(屏幕里子弹能同时存在的数目为初始化子弹数组中设置的值,敌机同理)。通过循环不断创建。
//例如
//创建子弹
for (int i = 0; i < BULLET_NUM; i++)
{
if (!bull[i].live)
{
bull[i].x = player.x + (PLAN_WIDTH / 2) - (BULLET_WIDTH / 2);
bull[i].y = player.y;
bull[i].live = true;
break;//创建一个跳出
}
}
8.子弹打飞机
子弹的图片范围触及到敌机的图片范围,敌机HP-1。击落敌机后创造敌机时需要重置HP。玩家击落敌机,根据敌机HP获得分数。
void shootPlane()
{
for (int i = 0; i < ENEMY_NUM; i++)
{
if (!enemy[i].live)
{
continue;
}
for (int j = 0; j < BULLET_NUM; j++)
{
if (!bull[j].live)
{
continue;
}
if (bull[j].x > enemy[i].x && bull[j].x<enemy[i].x + enemy[i].width
&& bull[j].y>enemy[i].y && bull[j].y < enemy[i].y + enemy[i].height)
{
bull[j].live = false;
enemy[i].hp--;
}
}
if (enemy[i].hp <= 0)
{
if (enemy[i].type == BIG)
{
player.score += BIG_PH;
}
else
{
player.score +=SMALL_PH;
}
enemy[i].live = false;
//printf("%d", player.score);//测试函数
}
}
}
9.定时器
封装一个定时器用来控制子弹的射击速度以及敌机的下降速度。
bool Timer(int ms, int id)
{
static DWORD t[10];
if (clock() - t[id] > ms)
{
t[id] = clock();
return true;
}
return false;
}
10. 玩家被攻击
玩家撞到敌机,玩家HP-1,敌机HP-1。玩家撞毁敌机同样增加分数。代码原理同子弹打飞机相似。玩家血量小于10则游戏结束。
11. 游戏文字信息
在屏幕左上角显示玩家姓名、玩家血量、玩家得分,并实时更新血量值和得分值。
//例如
settextstyle(20, 10, "黑体");//设置字体
settextcolor(BLACK);//设置颜色
setbkmode(NULL);//清空文字背景
outtextxy(0, 0, "玩家姓名:路远");
12. 播放背景音乐
//打开音乐,播放音乐 alias取别名 repeat重复播放
mciSendString(_T("open ./images/game_music.mp3 alias BGM"), NULL, 0, NULL);//播放音乐
mciSendString(_T("play BGM repeat"), NULL, 0, NULL);//循环播放
13. 未实现的功能
没有实现子弹射击、击中敌机、与敌机碰撞等声音,有待完善。
三、飞机大战具体代码
1.game.h
#pragma once
#include <stdio.h>
#include <graphics.h> //图形库,帮助新手快速入门图形编程 easyx
#include <mmsystem.h>//包含多媒体设备接口头文件,一定放在graphics.h下面
#pragma comment(lib,"Winmm.lib")//加载静态库
#include <conio.h>//_getch()的头文件,不是C语言的标准函数
#include <time.h>
enum Size//窗口的高度和宽
{
//窗口尺寸
WIDTH = 591,
HEIGHT = 864,
//玩家飞机尺寸
PLAN_WIDTH = 117,
PLAN_HEIGHT = 120,
//子弹尺寸
BULLET_WIDTH = 19,
BULLET_HEIGHT = 24,
//子弹数目
BULLET_NUM = 15,
//敌机数目
ENEMY_NUM = 10,
//敌机尺寸
BIG,
SMALL,
BIG_WIDTH = 104,
BIG_HEIGHT = 148,
SMALL_WIDTH = 52,
SMALL_HEIGHT = 39,
//敌机血量
BIG_PH = 3,
SMALL_PH = 1
};
#define PLAYER_SPEED 1 //玩家移动速度
#define ENEMY_SPEED 1//敌机移动速度
#define BULLET_SPEED 2//子弹移动速度
//加载图片函数
void loadImg();
//游戏图片的绘制函数
void gameDraw();
//玩家飞机初始化 子弹初始化
void gameInit();
//角色移动,获取键盘消息 子弹移动
void playerMove(int speed);
//创建子弹
void creatBullet();
//子弹移动
void bulletMove(int speed);
//设置敌机
void enemyHp(int i);
//创造敌机
void creatEnemy();
//敌机移动
void enemyMove(int speed);
//封装定时器
bool Timer(int ms, int id);
//打飞机
void shootPlane();
//玩家被攻击
void Attacked();
//游戏文字信息
void textDraw();
//播放背景音乐
void BGM();
2.game.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
//把图片加载进程序
IMAGE bk;//背景图片
IMAGE img_role[2];//玩家图片
IMAGE img_bullet[2];//子弹图片
IMAGE img_enemy[2][2];//敌机图片
IMAGE img_dieplane[2];//玩家飞机死亡图片
void loadImg()
{
//加载背景图片 591 * 864
loadimage(&bk, "./images/background.jpg");
//加载玩家图片
loadimage(&img_role[0], "./images/planeNormal_1.jpg");//掩码图
loadimage(&img_role[1], "./images/planeNormal_2.jpg");//原图
//加载子弹图片
loadimage(&img_bullet[0], "./images/bullet1.jpg");//掩码图
loadimage(&img_bullet[1], "./images/bullet2.jpg");//原图
//加载敌机的图片
loadimage(&img_enemy[0][0], "./images/enemy_1.jpg");//掩码图
loadimage(&img_enemy[0][1], "./images/enemy_2.jpg");//原图
loadimage(&img_enemy[1][0], "./images/enemyPlane1.jpg");//掩码图
loadimage(&img_enemy[1][1], "./images/enemyPlane2.jpg");//原图
//加载玩家飞机死亡图片
loadimage(&img_dieplane[0], "./images/planeExplode_1.jpg");//掩码图
loadimage(&img_dieplane[1], "./images/planeExplode_2.jpg");//原图
}
struct plane//玩家飞机
{
int x;
int y;
bool live;//是否存活
int width;
int height;
int hp;//血量
int type;//敌机类型 big small
int score;//得分
}player,enemy[ENEMY_NUM];
struct bullet//子弹
{
int x;
int y;
bool live;//是否存活
}bull[BULLET_NUM];
bool isExpolde = false; // 飞机是否爆炸
void gameDraw()
{
//把背景图沾到窗口上
putimage(0, 0, &bk);
//绘制玩家飞机
//透明贴图技术:用掩码图和原图做一个颜色的二进制操作
if (!isExpolde)
{
putimage(player.x, player.y, &img_role[0], NOTSRCERASE);
putimage(player.x, player.y, &img_role[1], SRCINVERT);
}
else
{
putimage(player.x, player.y, &img_dieplane[0], NOTSRCERASE);
putimage(player.x, player.y, &img_dieplane[1], SRCINVERT);
Sleep(10000);//游戏结束
}
//绘制子弹
for (int i = 0; i < BULLET_NUM; i++)
{
if (bull[i].live)
{
putimage(bull[i].x, bull[i].y, &img_bullet[0], NOTSRCERASE);
putimage(bull[i].x, bull[i].y, &img_bullet[1], SRCINVERT);
}
}
//绘制敌机
for (int i = 0; i < ENEMY_NUM; i++)
{
if (enemy[i].live)
{
if (enemy[i].type == BIG)
{
putimage(enemy[i].x, enemy[i].y, &img_enemy[1][0], NOTSRCERASE);
putimage(enemy[i].x, enemy[i].y, &img_enemy[1][1], SRCINVERT);
}
else
{
putimage(enemy[i].x, enemy[i].y, &img_enemy[0][0], NOTSRCERASE);
putimage(enemy[i].x, enemy[i].y, &img_enemy[0][1], SRCINVERT);
}
}
}
}
void gameInit()
{
loadImg();
//玩家初始化
player.x = WIDTH / 2;
player.y = HEIGHT - 120;
player.live = true;
player.hp = 10;
player.score = 0;
printf("%d", player.hp);
//子弹初始化
for (int i = 0; i < BULLET_NUM; i++)
{
bull[i].x = 0;
bull[i].y = 0;
bull[i].live = false;
}
//敌机初始化
for (int i = 0; i < ENEMY_NUM; i++)
{
enemy[i].live = false;
enemyHp(i);
}
}
void playerMove(int speed)
{
//玩家移动
#if 0
//检测是否有键盘按下,有则返回值,否则返回假
if (_kbhit())//<conio.h>里的
{
//第一种方式
//_getch() 阻塞函数,和scanf一样,没有输入就会卡住主程序等待输入,不是C语言的标准函数
char key = _getch();
switch (key)
{
case 'w':
case 'W':
player.y -= speed;
break;
case 's':
case 'S':
player.y += speed;
break;
case 'a':
case 'A':
player.x -= speed;
break;
case 'd':
case 'D':
player.x += speed;
break;
}
}
#elif 1
//第二种方式
//使用windows函数获取键盘输入 GetAsyncKeyState 非阻塞函数,非常流畅
//小键盘的上下左右 如果要检测字母按键,必须用大写,这样大小写都可以检测到,小写一个都检测不到
if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W'))//VK_UP系统定义的宏
{
if (player.y > 0)//边界处理
{
player.y -= speed;
}
}
if (GetAsyncKeyState(VK_DOWN) || GetAsyncKeyState('S'))
{
if (player.y < HEIGHT - PLAN_HEIGHT)
{
player.y += speed;
}
}
if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A'))
{
if (player.x + (PLAN_WIDTH / 2) > 0)
{
player.x -= speed;
}
}
if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D'))
{
if (player.x < WIDTH - (PLAN_WIDTH / 2))
{
player.x += speed;
}
}
//printf("(%d,%d)", player.x, player.y);//测试函数
#endif//0
}
void creatBullet()
{
for (int i = 0; i < BULLET_NUM; i++)
{
if (!bull[i].live)
{
bull[i].x = player.x + (PLAN_WIDTH / 2) - (BULLET_WIDTH / 2);
bull[i].y = player.y;
bull[i].live = true;
break;//创建一个跳出
}
}
}
void bulletMove(int speed)
{
//子弹射击频率
if (GetAsyncKeyState(VK_SPACE) && Timer(300, 1))//300毫秒
{
creatBullet();
}
for (int i = 0; i < BULLET_NUM; i++)
{
if (bull[i].live)
{
bull[i].y -= speed;
if (bull[i].y < 0)
{
bull[i].live = false;
}
}
}
}
void enemyHp(int i)
{
int flag = rand() % 10;
if (flag >= 0 && flag <= 3)//产生大飞机
{
enemy[i].type = BIG;
enemy[i].hp = BIG_PH;
enemy[i].width = 104;
enemy[i].height = 149;
}
else//小飞机
{
enemy[i].type = SMALL;
enemy[i].hp = SMALL_PH;
enemy[i].width = 52;
enemy[i].height = 39;
}
}
void creatEnemy()
{
for (int i = 0; i < ENEMY_NUM; i++)
{
if (!enemy[i].live)
{
enemy[i].live = true;
enemy[i].x = rand() % (WIDTH - (BIG_WIDTH + SMALL_WIDTH) / 2);
enemy[i].y = 0;
enemyHp(i);//重置敌机HP
break;
}
}
}
void enemyMove(int speed)
{
for (int i = 0; i < ENEMY_NUM; i++)
{
if (enemy[i].live)
{
enemy[i].y += speed;
if (enemy[i].y > HEIGHT)
{
enemy[i].live = false;
}
}
}
}
bool Timer(int ms, int id)
{
static DWORD t[10];
if (clock() - t[id] > ms)
{
t[id] = clock();
return true;
}
return false;
}
void shootPlane()
{
for (int i = 0; i < ENEMY_NUM; i++)
{
if (!enemy[i].live)
{
continue;
}
for (int j = 0; j < BULLET_NUM; j++)
{
if (!bull[j].live)
{
continue;
}
if (bull[j].x > enemy[i].x && bull[j].x<enemy[i].x + enemy[i].width
&& bull[j].y>enemy[i].y && bull[j].y < enemy[i].y + enemy[i].height)
{
bull[j].live = false;
enemy[i].hp--;
}
}
if (enemy[i].hp <= 0)
{
if (enemy[i].type == BIG)
{
player.score += BIG_PH;
}
else
{
player.score +=SMALL_PH;
}
enemy[i].live = false;
//printf("%d", player.score);//测试函数
}
}
}
void Attacked()
{
for (int i = 0; i < ENEMY_NUM; i++)
{
if (!enemy[i].live)
{
continue;
}
if (player.x > enemy[i].x && player.x<enemy[i].x + enemy[i].width
&& player.y>enemy[i].y && player.y < enemy[i].y + enemy[i].height)
{
enemy[i].hp--;
player.hp--;
//printf("%d", player.hp);//测试函数
if (enemy[i].hp <= 0)
{
if (enemy[i].type == BIG)
{
player.score += BIG_PH;
}
else
{
player.score += SMALL_PH;
}
enemy[i].live = false;
//printf("%d", player.score);//测试函数
}
if (player.hp <= 0)
{
isExpolde = true;
}
}
}
}
void textDraw()
{
char hp[10];
char sco[10];
sprintf(hp, "%d", player.hp);
sprintf(sco, "%d", player.score);
settextstyle(20, 10, "黑体");//设置字体
settextcolor(BLACK);//设置颜色
setbkmode(NULL);//清空文字背景
outtextxy(0, 0, "玩家姓名:路远");
outtextxy(0, 25, "玩家血量:");
outtextxy(100, 25, hp);
outtextxy(0, 50, "玩家得分:");
outtextxy(100, 50, sco);
}
void BGM()
{
//打开音乐,播放音乐 alias取别名 repeat重复播放
mciSendString(_T("open ./images/game_music.mp3 alias BGM"), NULL, 0, NULL);//播放音乐
mciSendString(_T("play BGM repeat"), NULL, 0, NULL);//循环播放
//错误代码无法播放声音
//mciSendString("open ./images/game_music.mp3 BGM", NULL, 0, NULL);//向多媒体设备接口(mci)发送(send)一个字符串(string)
//mciSendString("play BGM repeat", NULL, 0, NULL);
}
3.test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
int main()
{
//创建一个图形窗口
initgraph(WIDTH, HEIGHT, SHOWCONSOLE);//SHOWCONSOLE 在输出图形函数的同时,把printf函数窗口也显示出来,方便对比界面看数据
gameInit();
BGM();
//双缓冲绘图
BeginBatchDraw();
while (1)
{
gameDraw();
textDraw();
FlushBatchDraw();
playerMove(PLAYER_SPEED);
bulletMove(BULLET_SPEED);
if (Timer(500, 0))
{
creatEnemy();
}
if (Timer(50, 2))
{
enemyMove(ENEMY_SPEED);
}
shootPlane();
Attacked();
//printf("路远来了\n");
}
EndBatchDraw();
return 0;
}