【C++】利用游戏壳实现飞机大战(设计类图、开发实现)

news2024/11/26 2:23:49

文章目录

    • 飞机大战
      • 飞机大战类图分析
        • 背景类(CBackGround)
          • 成员属性
          • 成员函数
        • 程序类(CPlaneApp)
          • 成员属性
          • 成员函数
        • 玩家类(CPlayer)
          • 成员属性
          • 成员函数
        • 炮弹类(CGunner)
          • 成员属性
          • 成员函数
        • 炮弹盒子类 (CGunnerBox)
          • 成员属性
          • 成员函数
        • 敌人飞机类(CFoe)
          • 成员属性
          • 成员函数
        • 大飞机类(CFoeBig)
          • 成员函数
        • 中飞机类(CFoeMid)
          • 成员函数
        • 小飞机类(CFoeSma)
          • 成员函数
        • 敌人飞机盒子类(CFoeBox)
          • 成员属性
          • 成员函数
        • 完整类图
      • 飞机大战开发实现
        • 开发准备
          • 1.创建项目
          • 2.在项目文件夹中创建管理类的文件夹
          • 3.创建vs中的虚拟目录
        • 正式开发
          • 用PlaneApp继承GameFrame
            • 头文件中的操作
            • 源文件中的操作
          • 背景类开发
            • 头文件中的操作
            • 源文件中的操作
          • 实现显示背景
            • 流程
            • 头文件
            • 源文件
            • 窗口
          • 玩家类开发
            • 创建类以及初始化
            • 显示玩家飞机
            • 玩家飞机移动
            • 在程序中完成玩家飞机相关的操作
          • 炮弹类开发
            • 创建类
            • 初始化
            • 显示
            • 炮弹移动
          • 炮弹盒子类开发
            • 创建类
            • 显示所有炮弹
            • 移动所有炮弹
            • 实现玩家飞机发射炮弹
            • 在程序中实现发射炮弹
          • 敌人飞机父类开发
            • 创建类
            • 源文件定义方法
            • 敌人飞机移动
          • 敌人飞机子类开发
            • 大飞机
            • 中飞机
            • 小飞机
          • 敌人飞机盒子类开发
            • 头文件
            • 源文件
            • 析构函数回收两个链表
            • 显示所有敌人飞机
            • 移动所有敌人飞机
          • 在程序中实现敌人飞机的显示和移动
            • 头文件
            • 显示
            • 移动
            • 运行效果
          • 敌人飞机与玩家飞机碰撞
            • 原理
            • 实现
          • 敌人飞机与炮弹碰撞
            • 原理
            • 实现
          • 在APP中实现碰撞的效果
            • 设置定时器
            • 实现敌人飞机与玩家飞机相撞
            • StopTimer()函数中去停止所有定时器
            • 实现炮弹击中敌人飞机
            • 实现敌人飞机的爆炸效果
            • 显示分数板和增加分数
        • 结束

飞机大战

飞机大战类图分析

背景类(CBackGround)

它与程序类是组合关系。

成员属性
  • IMAGE m_img; //图片
  • int m_x; //放在窗口的横坐标
  • int m_y; //放在窗口的纵坐标
成员函数
  • void InitBack(); //逻辑初始化
  • void ShowBack(); //重绘贴图
  • void MoveBack(int step); //移动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qo2bZKAS-1685237139188)(C:\Users\jiaji\AppData\Roaming\Typora\typora-user-images\image-20230515100941191.png)]

程序类(CPlaneApp)

他需要作为一个子类来继承游戏壳(CGameFrame)。

成员属性
  • CBackGround m_back; //背景
  • CPlayer m_player; //玩家飞机
  • CGunnerBox m_gunBox; //炮弹盒子
  • CFoeBox m_foeBox; //敌人飞机盒子
  • int m_score; //分数 (属于自己的成员属性)
  • IMAGE m_scoreBoard; //分数板子图片
成员函数

由于继承了游戏壳,所以要将游戏壳中的虚函数在此处实现

  • virtual void On_Init(); //程序初始化
  • virtual void On_Paint(); //程序重绘
  • virtual void On_Close(); //关闭程序
  • virtual void AddMsgMap(); //添加消息映射表,根据实际用到的消息添加
  • void ShowScore(); //显示分数
  • void SetTimer(); //设定定时器
  • void StopTimer(); //停止定时器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pEwOdqp-1685237139190)(C++.assets/image-20230515131434779.png)]

玩家类(CPlayer)

他与程序类是组合关系。

他与炮弹类是依赖关系。

成员属性
  • IMAGE m_img; //原图
  • IMAGE m_imgMask; //屏蔽图
  • int m_x; //横坐标
  • int m_y; //纵坐标
成员函数
  • void InitPlayer(); //初始化

  • void ShowPlayer(); //显示贴图

  • void MovePlayer(int direct,int step); //移动 由于要捕获键盘输入的信息才能确定移动的方向,所以要有一个int类型的参数。

  • CGunner* SendGunner(); //发射炮弹 返回值应为炮弹类的指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jmeioBbI-1685237139190)(C:\Users\jiaji\AppData\Roaming\Typora\typora-user-images\image-20230515102751779.png)]

炮弹类(CGunner)

成员属性
  • IMAGE m_img; //图片
  • int m_x;
  • int m_y;
成员函数
  • void InitGunner(int x,int y); //坐标根据玩家飞机坐标决定
  • void ShowGunner(); //显示
  • void MoveGunner(int step); //移动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z8sGj3xX-1685237139190)(C:\Users\jiaji\AppData\Roaming\Typora\typora-user-images\image-20230515103658657.png)]

炮弹盒子类 (CGunnerBox)

创建炮弹盒子类是为了统一管理炮弹,因为在程序中不只有一枚炮弹,它与炮弹是聚合关系。

与程序类是组合关系。

成员属性
  • list<CGunner*> m_lstGun; //装炮弹的链表
成员函数

这里不用统一初始化,因为炮弹在被创建的时候就已经初始化过了

  • void ShowAllGunner(); //显示所有炮弹
  • void MoveAllGunner(); //移动所有炮弹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jpytiJp-1685237139191)(C:\Users\jiaji\AppData\Roaming\Typora\typora-user-images\image-20230515105024509.png)]

敌人飞机类(CFoe)

三种敌人飞机的父类。

他与炮弹类和玩家类都是依赖的关系

成员属性
  • IMAGE m_img;
  • int m_x; //此x是一个随机数
  • int m_y;
  • int m_blood; //血量
  • int m_showId; //控制 切换显示哪张图 (倒序id,为了统一回收飞机)当id为0时回收飞机
成员函数
  • virtual void InitFoe()=0; //函数里面的实现不一样要做成虚函数
  • virtual void ShowFoe()=0;
  • void MoveFoe(int step);
  • virtual bool IsHitPlayer(CPlayer& player)=0; //判断是否与玩家飞机碰撞,参数中用引用是因为玩家飞机只有一个且不是以指针形式存在的
  • virtual bool IsHitGunner(CGunner* pGun)=0; //判断是否被子弹击中 炮弹是new出来的,所以是以指针形式存在的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrFRqGDa-1685237139191)(C++.assets/image-20230515125552356.png)]

大飞机类(CFoeBig)

是敌人飞机类的子类

成员函数
  • virtual void InitFoe();
  • virtual void ShowFoe();
  • virtual bool IsHitPlayer(CPlayer& player);
  • virtual bool IsHitGunner(CGunner* pGun);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q5vs5Cnz-1685237139191)(C++.assets/image-20230515125610085.png)]

中飞机类(CFoeMid)

是敌人飞机类的子类

成员函数
  • virtual void InitFoe();
  • virtual void ShowFoe();
  • virtual bool IsHitPlayer(CPlayer& player);
  • virtual bool IsHitGunner(CGunner* pGun);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4E9fva0-1685237139191)(C++.assets/image-20230515125624346.png)]

小飞机类(CFoeSma)

是敌人飞机类的子类

成员函数
  • virtual void InitFoe();
  • virtual void ShowFoe();
  • virtual bool IsHitPlayer(CPlayer& player);
  • virtual bool IsHitGunner(CGunner* pGun);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-142V297Z-1685237139192)(C++.assets/image-20230515125640897.png)]

敌人飞机盒子类(CFoeBox)

因为敌人飞机也不止一个,所以要创建一个类来管理敌人飞机,他与敌人飞机类是聚合关系,与程序类是组合关系。

成员属性
  • list<CFoe*>m_lstFoe; //正常敌人飞机的链表 (可移动,可显示)
  • list<CFoe*>m_lstBoomFoe; //爆炸敌人飞机的链表 (原地爆炸,没有移动,显示爆炸效果)
成员函数
  • void ShowAllFoe();
  • void MoveAllFoe();

完整类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jFpTykjx-1685237139192)(C++.assets/image-20230515131849170.png)]

飞机大战开发实现

开发准备

1.创建项目

正常创建一个名为飞机大战的项目

2.在项目文件夹中创建管理类的文件夹
  • 创建名为GameFrame的文件夹并将游戏壳的头文件与主函数放进去
  • 创建名为PlaneApp的文件夹,并在其中创建名为PlaneApp的头文件与源文件
  • 创建名为BackGround的文件夹,并在其中创建名为BackGround的头文件与源文件
  • 创建名为GunnerBox的文件夹,这里也将炮弹(Gunner)一起放在这个文件夹里管理了,然后在其中创建名为GunnerBox的头文件与源文件和名为Gunner的头文件与源文件
  • 创建名为FoeBox的文件夹,并将有关敌人飞机的所有头文件与源文件都放在里面,包括Foe、FoeBig、FoeMid、FoeSma、FoeBox
  • 将有关的res资源文件解压到此文件夹中
  • 增加一个用于管理配置的文件夹config,并在其中创建一个名为config的头文件

以上创建头文件与原文件均是先创建一个文本文档,然后将其后缀名改为.h与.cpp即可

我们手动去建这些文件夹是为了更方便管理也提高了可读性,如果用vs去建那么默认就在工程所在文件夹中建出来了

全部整备完毕的文件夹应该如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nOcI5vFj-1685237139192)(C++.assets/image-20230515173156752.png)]

3.创建vs中的虚拟目录
  1. 将虚拟目录中的头文件与源文件删掉
  2. 根据刚才在项目所在文件夹中创建的文件夹来手动创建虚拟目录

创建虚拟目录的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2OHnrLz-1685237139192)(C++.assets/image-20230515173849899.png)]

  1. 创建之后的效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmwhTin5-1685237139192)(C++.assets/image-20230515174129997.png)]

  1. 通过添加现有项将文件夹中的头文件与源文件也添加到目录中

添加现有项的快捷键:Shift+Alt+A

正式开发

用PlaneApp继承GameFrame
头文件中的操作

先在PlaneApp的头文件中加上#pragma once 是为了说明当前这个头文件在其他源文件中只包含一份

#pragma once

将类图中设计好的成员属性和成员函数写在类中

由于程序类与背景类、玩家类、炮弹盒子类、敌人飞机盒子类都是组合关系,而现在还没有对那几个类进行开发,并且分数和分数板子目前也用不到,所以先将类中的成员属性注释掉,用的时候再解开。

补上构造析构函数的声明

继承CGameFrame 需要包含CGameFrame的头文件 由于现在他们所在的位置不是同一个路径下,所以要先往上找一层,找到GameFrame的路径,再在此路径中找到对应的头文件

…/ 上层目录 ./ 当前目录

为了验证子类中的虚函数是否重写了父类中的虚函数 我们在函数的末尾加上了一个关键字override,此关键字起到检查的作用。

目前PlaneApp的头文件:

#pragma once
#include "../GameFrame/GameFrame.h"

class CPlaneApp :public CGameFrame {
public:
	/*CBackGround m_back;
	CPlayer m_player;
	CGunnerBox m_gunBox;
	CFoeBox m_foeBox;
	int m_score;
	IMAGE m_scoreBoard;*/
public:
	CPlaneApp();
	~CPlaneApp();
public:
	virtual void On_Init() override;   //要求这个虚函数一定是重写父类的,而不是自己单独的虚函数
	virtual void On_Paint() override;
	virtual void On_Close() override;
	virtual void AddMsgMap() override;
	void ShowScore();
	void SetTimer();
	void StopTimer();
};
源文件中的操作

先在源文件中包含头文件

然后将头文件中的函数在源文件中定义,可使用快捷操作也可手动定义。

手动定义时别忘了在函数名前面加上类名作用域,并且去掉虚函数 virtual 和 override 关键字

在源文件中添加游戏壳的两个宏:CREAT_OBJECT(具体游戏类名) WID_PARAM(窗口参数)

那么此时编译就不会出现错误了

目前的源文件:

#include"PlaneApp.h"

CREAT_OBJECT(CPlaneApp)
WID_PARAM(600,600,400,50,L"飞机大战")


CPlaneApp::CPlaneApp() {}
CPlaneApp::~CPlaneApp() {}

void CPlaneApp::On_Init() {}
void CPlaneApp::On_Paint() {}
void CPlaneApp::On_Close() {}
void CPlaneApp::AddMsgMap() {}
void CPlaneApp::ShowScore() {}
void CPlaneApp::SetTimer() {}
void CPlaneApp::StopTimer() {}

背景类开发
头文件中的操作

创建CBackGround类并将类图中的属性与函数在里面声明出来,还有构造析构函数,要包含头文件<easyx.h>

#pragma once
#include<easyx.h>

class CBackGround {
public:
	IMAGE m_img;
	int m_x;
	int m_y;
public:
	CBackGround();
	~CBackGround();
public:
	void InitBack();
	void ShowBack();
	void MoveBack(int step);
};
源文件中的操作

将头文件中的函数在此定义

并将两个坐标成员属性在构造函数中初始化

在初始化函数InitBack中将背景图初始化,(::loadimage(取图片变量地址,L”工程所在的路径为相对起始路径”)),并将坐标初始化

在显示函数showBack中贴上背景图片(::putimage(横坐标,纵坐标,图片地址)

移动思路:窗口的高度为800,图片的高度为1600,为了让窗口能循环显示背景图,所以将背景图初始显示在窗口上方800处,当背景图的上边缘抵达窗口的上边缘时,图片再次回到初始处,如此往复形成一个循环的效果

将窗口的宽和高在配置头文件中创建出宏

#include"BackGround.h"
#include"../config/config.h"

CBackGround::CBackGround(): m_x(0),m_y(0){

}
CBackGround::~CBackGround() {

}

void CBackGround::InitBack() {
	::loadimage(&m_img, L"./res/背景.jpg");    //以工程所在的路径为相对起始路径

	m_x = 0;
	m_y = -BACK_HEIGHT;
}

void CBackGround::ShowBack() {
	::putimage(m_x, m_y, &m_img);
}

void CBackGround::MoveBack(int step) {
	m_y += step;
	if (m_y > 0) {
		m_y = -BACK_HEIGHT;
	}
}
实现显示背景
流程
  • 在PlaneApp头文件中包含背景类的头文件,然后将成员属性中的背景对象取消注释
  • 初始化:在App源文件的初始化函数中通过背景类对象调用背景的初始化,一个App中的初始化包含了他各个东西的初始化
  • 显示:在重绘函数中调用背景显示函数
  • 定时器:由于移动是定时去移动,所以需要创建定时器,因为在初始化时就已经看见移动了,所以在初始化之前就已经创建了。在SetTimer中创建定时器(::SetTimer(窗口句柄,定时器ID,定时器频率,回调函数)),由于定时器ID与定时器频率都为常量,所以也可以在配置头文件中去定义宏。
  • 增加定时器消息映射表:我们可以看出这个接收消息的方式是WINDOW接收,所以使用对应的接收宏,并且创建相关的处理函数

定时器原理定时器会定时产生WM_TIMER消息,然后增加一个对应的定时器消息映射表用于接收此类消息,然后调用对应的处理函数,处理函数中WPARAM会接收到定时器ID,最终在处理函数中根据定时器ID做相应的操作

增加消息映射表的方法:消息映射表是我们在游戏壳中创建的一个映射表,我们通过这个表的key找到消息类别和处理函数的结构体,然后通过回调函数去接收处理消息,我们为了方便根据具体游戏添加相应的消息映射表,我们留了一个接口。那么在游戏中添加消息映射表的方法为:创建一个处理函数(函数名一定要为On_消息ID的样式),然后在AddMsgMap中添加对应的宏==(INIT_MSGMAP(消息ID, 所属类别,具体游戏类类名) )==

  • 移动:在处理函数中通过识别WPARAM和定时器ID来确定操作,移动就是在这里实现,由于移动的步长也是常量,可以在配置头文件中定义宏
  • 最后在初始化函数中调用定时器函数

我的感觉是在AddMsgMap函数中实现接收消息,然后创建一个对应的函数来处理消息,通过WPARAM来接收定时器ID,LPARAM是指向回调函数的指针,暂时不需要,然后再通过定时器ID进行判断

头文件

在PlaneApp.h中加了一个处理消息的函数

	void On_WM_TIMER(WPARAM, LPARAM);
源文件

初始化函数:

void CPlaneApp::On_Init() {
	m_back.InitBack();



	this->SetTimer();
}

重绘函数:

void CPlaneApp::On_Paint() {
	m_back.ShowBack();
}

设置消息映射表:

void CPlaneApp::AddMsgMap() {
	INIT_MESSAGEMAP(WM_TIMER, EX_WINDOW, CPlaneApp)
}

设置定时器:

void CPlaneApp::SetTimer() {
	::SetTimer(m_hWnd  /*窗口句柄*/, BACK_MOVE_TIMERID/*定时器ID*/, BACK_MOVE_INTERVAL/*定时器频率*/, nullptr/*定时器回调函数*/);
}

操作消息:

void CPlaneApp::On_WM_TIMER(WPARAM w, LPARAM l) {
	switch (w)
	{
	case BACK_MOVE_TIMERID:
	{
		m_back.MoveBack(BACK_MOVE_STEP);
	}
	break;

	}
}
窗口

调试窗口:

WID_PARAM(600+16,800+39,400,50,L"飞机大战")

显示窗口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CuZlAHlS-1685237139193)(C++.assets/image-20230515205430413.png)]

玩家类开发
创建类以及初始化

将类图中的成员属性和成员函数粘贴到头文件中,并且添加构造析构函数

由于炮弹类还没有开发,所以先将发射炮弹的函数注释掉

#pragma once
#include <easyx.h>

class CPlayer {
public:
	IMAGE m_img;
	IMAGE m_imgMask;
	int m_x;
	int m_y;
public:
	CPlayer();
	~CPlayer();
public:
	void InitPlayer();
	void ShowPlayer();
	void MovePlayer(int direct, int step);
	//CGunner* SendGunner();
};

在源文件中对函数进行定义

坐标成员属性在构造函数中初始化,然后图片跟坐标再在玩家初始化函数中做具体初始化

将两张图片通过loadimage进行赋值,坐标要使飞机在背景的底部中间位置,所以横坐标就是北京宽度减去飞机宽度再除以2,高度就是背景高度减去飞机高度

#include "Player.h"
#include"../config/config.h"

CPlayer::CPlayer(): m_x(0),m_y(0){}
CPlayer::~CPlayer(){}

void CPlayer::InitPlayer(){
	::loadimage(&m_img, L".\\res\\playerplane.jpg");
	::loadimage(&m_imgMask, L".\\res\\playerplane-mask.jpg");
	m_x = (BACK_WIDTH- PLAYER_WIDTH)/2;
	m_y = BACK_HEIGHT- PLAYER_HEIGHT;
}

void CPlayer::ShowPlayer(){
}

void CPlayer::MovePlayer(int direct, int step){
}
显示玩家飞机

由于飞机不是方方正正的,它是不规则图形,所以我们有一个原图,还有一个屏蔽图,我们要将白边去掉

我们先贴屏蔽图,并让它的传输方式为位或,然后贴原图,传输方式为位与

贴图去白边原理:将图片转换为二进制,黑色二进制为0,白色二进制为1,先以位或方式贴屏蔽图,那么黑色部分得到的就是背景图(有1则1,因为黑色为全0,所以得到的二进制颜色就是背景色),白色部分得到的还是白色(因为白色为全1,所以得到的颜色也为全1),然后以位与方式贴原图,白色部分位与任何 颜色都为任何颜色,所以白边部分得到的还是背景色,飞机部分与下面屏蔽图白色部分得到的是飞机颜色,所以在显示的时候就只显示飞机部分了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yRsHLsb6-1685237139193)(C++.assets/image-20230519111123114.png)]

void CPlayer::ShowPlayer(){
	::putimage(m_x, m_y, &m_imgMask, SRCPAINT/*传输方式 位或*/);  //先屏蔽图 位或操作
	::putimage(m_x, m_y, &m_img, SRCAND/*传输方式 位与*/);  //再 原图 位与操作

}
玩家飞机移动

我们会向函数中传递方向键参数和移动的步伐

然后根据按下的方向键决定飞机向哪移动,在移动时会有一个判断,如果没有移动到边界,就可以继续移动,但是如果到了边界就不能继续移动了。我们首先想到的是用if来判断

比如说:

	if (direct == VK_UP) {
		if (m_y - step >= 0) {
			m_y -= step;
		}
		else {
			m_y = 0;
		}
	}

但是通过思考我们可以用三目运算符来实现

		m_y - step >= 0 ? m_y -= step : m_y = 0;

完整实现:

void CPlayer::MovePlayer(int direct, int step){
	if (direct == VK_UP) {
		m_y - step >= 0 ? m_y -= step : m_y = 0;
	}
	else if (direct == VK_DOWN) {
		m_y + step <= (BACK_HEIGHT - PLAYER_HEIGHT) ? m_y += step : m_y = (BACK_HEIGHT - PLAYER_HEIGHT);
	}
	else if (direct == VK_LEFT) {
		m_x - step >= 0 ? m_x -= step : m_x = 0;
	}
	else if (direct == VK_RIGHT) {
		m_x + step <= (BACK_WIDTH - PLAYER_WIDTH) ? m_x += step : m_x = (BACK_WIDTH - PLAYER_WIDTH);
	}
}
在程序中完成玩家飞机相关的操作

首先在头文件中将玩家飞机的头文件包含在里面,然后将玩家类型的成员属性取消注释,用于之后操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qh80EUEi-1685237139193)(C++.assets/image-20230519122122041.png)]

因为玩家飞机在程序刚开始就有了,所以玩家飞机的初始化要在程序初始化时就实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7mscq4B-1685237139194)(C++.assets/image-20230519122403681.png)]

显示玩家飞机:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fl2lDi9T-1685237139194)(C++.assets/image-20230519122412388.png)]

那么玩家飞机怎么移动呢,因为他不是定时自己去移动,而是在我们键盘按下后再去移动,所以我们还要添加一个键盘按下的消息映射表

先在添加消息映射表中添加一个键盘按下的消息映射表,然后在头文件中声明处理函数,源文件中定义处理函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7jwMREI-1685237139194)(C++.assets/image-20230519123647276.png)]

因为键盘按下属于键盘类别的消息,所以参数为BYTE

	void On_WM_KEYDOWN(BYTE);

定义时直接调用玩家飞机的移动函数,传递的第一个参数就是BYTE,第二个参数是步伐的大小,因为它是一个常量,所以可以在配置文件中去配置

void CPlaneApp::On_WM_KEYDOWN(BYTE key) {
	m_player.MovePlayer(key, PLAYER_MOVE_STEP);
}
#define PLAYER_MOVE_STEP       10

显示效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pKLCyX1o-1685237139194)(C++.assets/image-20230519123949671.png)]

但是我们发现在移动的时候会有顿挫感,那么怎么才能让移动更加灵活更加丝滑呢

分析原因:如果我们一下一下按方向键的话,那么他移动的频率就决定于手速,但是如果我们一直按下方向键,那么他的移动频率就由系统决定了,所以有顿挫感的原因就是系统发射消息的频率太低

那么怎么提高这个频率呢,我们肯定不能更改系统的发射频率,所以我们想到可以用定时器来解决,用定时器以很高的频率来检测玩家是否按下方向键

那么有了思路之后我们来设定定时器

	::SetTimer(m_hWnd, CHECK_MOVE_TIMERID, CHECK_MOVE_INTERVAL, nullptr);

然后在配置中设定检测ID和检测频率

#define CHECK_MOVE_TIMERID      2
#define CHECK_MOVE_INTERVAL    10

定时器设定完毕之后,我们就要在定时器处理函数中增加判断了

在处理函数中定时接收到检测的消息,然后定时检测是否按下方向键,并不是定时移动

那么怎么判断是否按下方向键呢,这里有一个对应方法(GetAsyncKeyState定时获取键盘的状态

例如判断是否按下了方向键上,如果按下返回非零值,然后就可以向玩家飞机移动函数中传递个向上的参数了,还有移动步伐

		if (::GetAsyncKeyState(VK_UP)) {  
			m_player.MovePlayer(VK_UP, PLAYER_MOVE_STEP);
		}

其他方向也是一样,但要注意的是,这里我们要写四个if,而不是if else,因为多个方向键可能会同时按下

	case CHECK_MOVE_TIMERID:
	{
		//定时检测是否按下方向键,并不是定时移动
		if (::GetAsyncKeyState(VK_UP)) {  //判断是否按下了方向键上,如果按下返回非零值
			m_player.MovePlayer(VK_UP, PLAYER_MOVE_STEP);
		}
		if (::GetAsyncKeyState(VK_DOWN)) {  //判断是否按下了方向键上,如果按下返回非零值
			m_player.MovePlayer(VK_DOWN, PLAYER_MOVE_STEP);
		}
		if (::GetAsyncKeyState(VK_LEFT)) {  //判断是否按下了方向键上,如果按下返回非零值
			m_player.MovePlayer(VK_LEFT, PLAYER_MOVE_STEP);
		}
		if (::GetAsyncKeyState(VK_RIGHT)) {  //判断是否按下了方向键上,如果按下返回非零值
			m_player.MovePlayer(VK_RIGHT, PLAYER_MOVE_STEP);
		}
	}
	break;

那么现在经过测试我们发现飞机就可以很灵活的移动了,处理键盘按下函数中的代码也就不再需要了,但是建议这个函数先保留,以后可能会有用

炮弹类开发
创建类

还是和之前一样,在头文件中创建类,然后将类图中设计好的成员复制到这里,再加上构造析构

#pragma once
#include<easyx.h>
class CGunner {
public:
	IMAGE m_img;
	int m_x;
	int m_y;
public:
	CGunner();
	~CGunner();
public:
	void InitGunner(int x, int y);
	void ShowGunner();
	void MoveGunner(int  step);
};

然后将成员函数在源文件中定义,坐标属性在构造函数初始化参数列表中初始化一下

#include "Gunner.h"

CGunner::CGunner():m_x(0),m_y(0){}

CGunner::~CGunner(){}

void CGunner::InitGunner(int x, int y){}

void CGunner::ShowGunner(){}

void CGunner::MoveGunner(int step){}

初始化

加载图片以及给坐标赋值,因为炮弹的坐标要根据玩家飞机的位置而定,所以让坐标等于传递的参数

void CGunner::InitGunner(int x, int y){
	::loadimage(&m_img, L".\\res\\gunner.jpg");
	m_x = x;
	m_y = y;
}
显示

显示这里与之前不一样了,之前我们是原图和屏蔽图两张图片进行操作,先位或贴屏蔽图再位与贴原图,但是现在就一张图片了,将原图与屏蔽图放在一起了,不过显示的原理是一样的

这里涉及到图片的一个截取

我们贴图使用的是putimage,这个函数有两套参数(也就是函数重载),之前我们一直使用的参数简单的那个,那么现在我们就需要使用较为复杂的那个了,此时我们不但要给出图片的加载位置坐标,还要将贴的宽度和高度给出,还有从哪个位置开始显示

所以我们还要在配置文件中将炮弹的宽度和高度给出,注意这里宽度并不是图片的宽度,而是一半,而高度就是图片的高度

#define GUNNER_WIDTH   6
#define GUNNER_HEIGHT   20
	//屏蔽图
	::putimage(m_x, m_y,//显示的位置
		GUNNER_WIDTH, GUNNER_HEIGHT, //显示的宽度高度
		&m_img, //显示的源图
		GUNNER_WIDTH, 0, //从原图的哪个位置开始显示
		SRCPAINT); //位或
	//原图
	::putimage(m_x, m_y,
		GUNNER_WIDTH, GUNNER_HEIGHT, 
		&m_img, 
		0, 0, 
		SRCAND); //位与
炮弹移动

直接就是炮弹的纵坐标-=步伐大小

void CGunner::MoveGunner(int step){
	m_y -= step;
}

有个考虑的点,就是炮弹移动有没有临界条件,因为炮弹不像是玩家飞机,始终是在窗口里面的,它出框了就会被删除回收掉,所以有销毁的操作,但是不需要在移动函数中去做

炮弹盒子类开发
创建类

依旧是创建类,粘贴成员,再去源文件定义

这里要注意的就是使用链表头文件要打开标准命名空间

#pragma once
#include<list>
#include"Gunner.h"
using namespace std;
class CGunnerBox {
public:
	list<CGunner*> m_lstGun;
public:
	CGunnerBox();
	~CGunnerBox();
public:
	void ShowAllGunner();
	void MoveAllGunner();
};

要在析构函数中遍历回收炮弹,采用迭代器遍历

#include "GunnerBox.h"

CGunnerBox::CGunnerBox(){}

CGunnerBox::~CGunnerBox(){
	list<CGunner*>::iterator ite = m_lstGun.begin();
	while (ite != m_lstGun.end()) {
		if ((*ite)) {
			delete (*ite);
			(*ite) = nullptr;
		}

		ite++;
	}
	m_lstGun.clear();
}

void CGunnerBox::ShowAllGunner(){}

void CGunnerBox::MoveAllGunner(){}
显示所有炮弹

用增强的范围for来遍历

取链表的每个节点,如果有值那就调用炮弹显示方法

void CGunnerBox::ShowAllGunner(){
	for (CGunner* pGun : m_lstGun) {
		if (pGun) pGun->ShowGunner();
	}
}
移动所有炮弹

移动流程跟显示一样,遍历后调用方法,但是要传递一个步长,步长在配置文件中定义一下,并且在炮弹盒子源文件中包含配置文件的头文件

然后还要在这个函数中实现将出框的炮弹回收,所以还要增加一个判断,判断炮弹是否出界,当炮弹的尾部到窗口上边缘了,就算出界了,然后回收此炮弹

void CGunnerBox::MoveAllGunner(){
	for (CGunner* pGun : m_lstGun) {
		if (pGun) pGun->MoveGunner(GUNNER_MOVE_STEP);
        if (pGun->m_y <= GUNNER_HEIGHT) {
			delete pGun;
			pGun = nullptr;
		}
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CY8oIhTV-1685237139194)(C++.assets/image-20230519193747084.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdATTrFZ-1685237139195)(C++.assets/image-20230519193756392.png)]

但是写到这里我们发现出现了一些问题,我们只能回收掉对象,但是无法删除节点

所以我们要将for循环遍历改为迭代器遍历,然后将出界的炮弹节点回收掉,注意这里不能在最后将整条链表的节点都清空,析构函数清空节点是因为程序已经关闭

所以使用erase函数来删除节点,并且删除完这个节点后还可能会有下一个节点,所以要用迭代器接一下返回值

用迭代器接回收掉的节点的话自带一个迭代器后移效果,那么在判断后就不需要再++了,所以就会出现一个局面,就是如果炮弹出界了,迭代器会接收回收节点后向后移动一下,然后循环内还会再++一次

所以这里我们选择在删除节点后加一个continue,如果删除节点就不再执行循环体后面的代码了

void CGunnerBox::MoveAllGunner(){
	list<CGunner*>::iterator ite = m_lstGun.begin();
	while(ite != m_lstGun.end()){
		if (*ite) (*ite)->MoveGunner(GUNNER_MOVE_STEP);
		if ((*ite)->m_y <= GUNNER_HEIGHT) {  //判断是否出街
			delete (*ite);
			(*ite) = nullptr;

			ite = m_lstGun.erase(ite);   //删除节点
			continue;
		}
		ite++;
	}
}
实现玩家飞机发射炮弹

在开发玩家类的时候有一个发射炮弹的方法没有书写,因为当时还没有创建炮弹类,那么现在就可以去定义实现了

首先在玩家类源文件中包含炮弹的头文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNzeJOts-1685237139195)(C++.assets/image-20230519194343198.png)]

然后再声明定义发射炮弹的方法

	CGunner* SendGunner();
CGunner* CPlayer::SendGunner(){

}

先new出来一个炮弹对象,然后通过对象调用初始化方法 ,这个初始化要传递两个参数,就是炮弹的初始位置,要根据飞机的位置来定

所以定义一个x来当炮弹的横坐标,它等于飞机的横坐标(m_x)+(飞机的宽度-炮弹的宽度)/2

定义一个y当作炮弹的纵坐标,它等于飞机的纵坐标(m_y)- 炮弹的高度

再将这两个参数传入初始化函数,在这个函数里面只需要将炮弹进行具体初始化,移动属于是定时自动移动,所以不需要在这里实现

最后返回这个炮弹指针

CGunner* CPlayer::SendGunner(){
	CGunner* pGun = new CGunner;
	int x = m_x + (PLAYER_WIDTH - GUNNER_WIDTH) / 2;
	int y = m_y - GUNNER_HEIGHT;

	pGun->InitGunner(x, y);
	return pGun;
}
在程序中实现发射炮弹

将炮弹盒子成员属性取消注释,加上炮弹盒子的头文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tnu5x6pD-1685237139195)(C++.assets/image-20230519201837703.png)]

在重绘里面调用显示所有炮弹函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mlIuZUnS-1685237139195)(C++.assets/image-20230519202004780.png)]

所有炮弹移动要在定时器处理函数中实现

所以就要设置一个炮弹移动的定时器,然后在配置文件中加上定时器ID和频率

	::SetTimer(m_hWnd, GUNNER_MOVE_TIMERID, GUNNER_MOVE_INTERVAL, nullptr);
#define GUNNER_MOVE_TIMERID    3
#define GUNNER_MOVE_INTERVAL    50

在定时器处理函数中调用炮弹移动方法

	case GUNNER_MOVE_TIMERID:
	{
		m_gunBox.MoveAllGunner();
	}
	break;

现在炮弹能够移动了,但想要真正实现发射炮弹还要有一个发射炮弹操作

因为我们设计的时候是玩家飞机自动去发射炮弹,所以这里我们依然选择用定时器去处理

还是老流程,创建定时器,配置定时器,在定时器处理函数中做相应操作

	::SetTimer(m_hWnd, GUNNER_SEND_TIMERID, GUNNER_SEND_INTERVAL, nullptr);
#define GUNNER_SEND_TIMERID    4
#define GUNNER_SEND_INTERVAL   500

发射炮弹后,炮弹会进入炮弹盒子中

	case GUNNER_SEND_TIMERID:
	{
		m_gunBox.m_lstGun.push_back(m_player.SendGunner());  //发射的炮弹会放在炮弹盒子里
	}
	break;

那么发射炮弹的流程就是,玩家飞机发射炮弹,然后在发射的同时会初始化炮弹,之后炮弹进入到炮弹盒子中,炮弹盒子会显示移动所有炮弹

发射效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEY8MHGn-1685237139196)(C++.assets/image-20230519203657913.png)]

炮弹发射原理

定时器处理函数会自动调用发射炮弹函数,发射炮弹时会返回一个炮弹类指针,并将它放在炮弹盒子链表中,也就是将炮弹装进炮弹盒子,并且还会自动调用移动所有炮弹函数,发射炮弹就是new一个炮弹对象然后用指针指向,并且确定炮弹的初始坐标位置,调用炮弹初始化函数,然后返回一个炮弹类指针,炮弹盒子中如果有炮弹,那么迭代器链表的节点就不为空,也就是有炮弹指针,那么就会显示炮弹并移动炮弹,如果炮弹出界,那就会回收掉指针,炮弹也就不会显示了

敌人飞机父类开发
创建类

将类图中设计好的成员复制到类中,并写上构造析构

因为其中的方法用到了玩家飞机和炮弹,所以要把他们两个的头文件包含进来

#pragma once
#include<easyx.h>
#include"../GunnerBox/Gunner.h"
#include"../Player/Player.h"

class CFoe {
public:
	IMAGE m_img;
	int m_x;
	int m_y;
	int m_blood;
	int m_showId;
public:
	CFoe();
	~CFoe();
	virtual void InitFoe() = 0;
	virtual void ShowFoe() = 0;
	void MoveFoe(int step);
	virtual bool IsHitPlayer(CPlayer& player) = 0;
	virtual bool IsHitGunner(CGunner* pGun) = 0;
};
源文件定义方法

其中只定义构造析构以及移动的方法即可,剩余为纯虚函数,在子类中去定义、

#include"Foe.h"

CFoe::CFoe(){
	int m_x = 0;
	int m_y = 0;
	int m_blood = 0;
	int m_showId = 0;
}

CFoe::~CFoe(){

}

void CFoe::MoveFoe(int step){

}
敌人飞机移动
void CFoe::MoveFoe(int step){
	m_y += step;
}

这里只是负责移动,至于敌人飞机出界删除以及判断临界的代码不在这里面写,要在敌人飞机盒子里面去写

敌人飞机子类开发
大飞机

头文件

头文件中就是先创建类继承父类,将继承父类的纯虚函数进行声明,然后将父类的头文件包含进来,因为没有自己的成员属性,所以构造析构就不写了

#pragma once
#include"Foe.h"

class CFoeBig :public CFoe{
public:
	virtual void InitFoe();
	virtual void ShowFoe();
	virtual bool IsHitPlayer(CPlayer& player);
	virtual bool IsHitGunner(CGunner* pGun);
};

源文件定义

#include"FoeBig.h"

void CFoeBig::InitFoe() {

}
void CFoeBig::ShowFoe() {

}
bool CFoeBig::IsHitPlayer(CPlayer& player) {

}
bool CFoeBig::IsHitGunner(CGunner* pGun) {

}

初始化

先将图片与成员属性进行绑定,在确定敌人飞机位置时用到了宽度和高度,所以在config中去配置一下

#define FOEBIG_WIDTH   150
#define FOEBIG_HEIGHT  100

初始化高度很容易判断,就是负的敌人飞机高度,而初始化x值应该是一个随机数,在0-(背景宽度-敌人飞机宽度)之间去取

因为这个随机数在三种子类飞机中都要去使用,所以我们索性就在父类飞机中去增加一个成员属性,因为这个随机数种子只要一份即可,所以可以设置成为静态的,创造随机数种子时首先要包含对应头文件并打开标准命名空间

#include<random>
using namespace std;

//类内
static random_device  rd;

然后去源文件中的类外去定义一下

random_device  CFoe::rd;    //静态的成员定义

在初始化血量时,就看我们想让炮弹击中几次后销毁,那就去配置文件中配置一下炮弹伤害和敌人飞机血量

#define GUNNER_HURT    1
#define FOEBIG_BLOOD   5
#define FOEMID_BLOOD   3
#define FOESMA_BLOOD   1

最终实现

void CFoeBig::InitFoe() {
	::loadimage(&m_img, L"./res/foeplanebig.jpg");
	m_x = rd() % (BACK_WIDTH - FOEBIG_WIDTH + 1);
	m_y = -FOEBIG_HEIGHT;
	m_blood = FOEBIG_BLOOD;
	m_showId = 4;
}

显示

因为这里的原图和屏蔽图还是在一张图片上,所以我们还是需要用复杂的参数的putimage

void CFoeBig::ShowFoe() {
	::putimage(m_x, m_y, FOEBIG_WIDTH, FOEBIG_HEIGHT, &m_img, FOEBIG_WIDTH, (4 - m_showId) * FOEBIG_HEIGHT, SRCPAINT);
	::putimage(m_x, m_y, FOEBIG_WIDTH, FOEBIG_HEIGHT, &m_img, 0, (4 - m_showId) * FOEBIG_HEIGHT, SRCAND);
}

剩下两个碰撞相关的函数先不去写,先去把敌人飞机能够显示出来,剩余两种飞机也用相同方法写出来

中飞机

跟打飞机中的基本一样,复制粘贴过来,然后把有关大飞机的都改为中飞机的即可,配置中配置一下中飞机的宽度高度

#define FOEMID_WIDTH   80
#define FOEMID_HEIGHT  60

头文件

#pragma once
#include"Foe.h"

class CFoeMid :public CFoe {
public:
	virtual void InitFoe();
	virtual void ShowFoe();
	virtual bool IsHitPlayer(CPlayer& player);
	virtual bool IsHitGunner(CGunner* pGun);
};

源文件

#include"FoeMid.h"
#include"../config/config.h"

void CFoeMid::InitFoe() {
	::loadimage(&m_img, L"./res/foeplanemid.jpg");
	m_x = rd() % (BACK_WIDTH - FOEMID_WIDTH + 1);
	m_y = -FOEMID_HEIGHT;
	m_blood = FOEMID_BLOOD;
	m_showId = 3;
}
void CFoeMid::ShowFoe() {
	::putimage(m_x, m_y, FOEMID_WIDTH, FOEMID_HEIGHT, &m_img, FOEMID_WIDTH, (3 - m_showId) * FOEMID_HEIGHT, SRCPAINT);
	::putimage(m_x, m_y, FOEMID_WIDTH, FOEMID_HEIGHT, &m_img, 0, (3 - m_showId) * FOEMID_HEIGHT, SRCAND);
}
bool CFoeMid::IsHitPlayer(CPlayer& player) {
	return false;
}
bool CFoeMid::IsHitGunner(CGunner* pGun) {
	return false;
}
小飞机

和中号原理一样

配置

#define FOESMA_WIDTH   60
#define FOESMA_HEIGHT  40

头文件

#pragma once
#include"Foe.h"

class CFoeSma :public CFoe {
public:
	virtual void InitFoe();
	virtual void ShowFoe();
	virtual bool IsHitPlayer(CPlayer& player);
	virtual bool IsHitGunner(CGunner* pGun);
};

源文件

#include"FoeSma.h"
#include"../config/config.h"

void CFoeSma::InitFoe() {
	::loadimage(&m_img, L"./res/foeplanesma.jpg");
	m_x = rd() % (BACK_WIDTH - FOESMA_WIDTH + 1);
	m_y = -FOESMA_HEIGHT;
	m_blood = FOESMA_BLOOD;
	m_showId = 2;
}
void CFoeSma::ShowFoe() {
	::putimage(m_x, m_y, FOESMA_WIDTH, FOESMA_HEIGHT, &m_img, FOESMA_WIDTH, (2 - m_showId) * FOESMA_HEIGHT, SRCPAINT);
	::putimage(m_x, m_y, FOESMA_WIDTH, FOESMA_HEIGHT, &m_img, 0, (2 - m_showId) * FOESMA_HEIGHT, SRCAND);
}
bool CFoeSma::IsHitPlayer(CPlayer& player) {
	return false;
}
bool CFoeSma::IsHitGunner(CGunner* pGun) {
	return false;
}
敌人飞机盒子类开发
头文件
#pragma once
#include <list>
#include"Foe.h"
using namespace std;

class CFoeBox {
public:
	list<CFoe*>m_lstFoe;
	list<CFoe*>m_lstBoomFoe;
public:
	CFoeBox();
	~CFoeBox();
	void ShowAllFoe();
	void MoveAllFoe();
};
源文件
#include"FoeBox.h"

CFoeBox::CFoeBox() {}

CFoeBox::~CFoeBox(){

}

void CFoeBox::ShowAllFoe()
{
}

void CFoeBox::MoveAllFoe()
{
}
析构函数回收两个链表
CFoeBox::~CFoeBox(){
	list<CFoe*>::iterator ite = m_lstFoe.begin();
	while (ite != m_lstFoe.end()) {
		if ((*ite)) {
			delete (*ite);
			(*ite) = nullptr;
		}
		ite++;
	}
	m_lstFoe.clear();

	//----------------------------
	ite = m_lstBoomFoe.begin();
	while (ite != m_lstBoomFoe.end()) {
		if ((*ite)) {
			delete (*ite);
			(*ite) = nullptr;
		}
		ite++;
	}
	m_lstBoomFoe.clear();
}
显示所有敌人飞机
void CFoeBox::ShowAllFoe(){
	for (CFoe* pFoe : m_lstFoe) {
		if (pFoe) pFoe->ShowFoe();
	}

	for (CFoe* pFoe : m_lstBoomFoe) {
		if (pFoe) pFoe->ShowFoe();
	}
}
移动所有敌人飞机

这里我们想让不同飞机的移动步伐不同,那么我们用什么去区分呢,我们选择用showId来区分,因为在移动的时候showId是不变的,且每种飞机不同

我们正常来判断是这么写的

(*ite)->showId ==  4;//大

但是现在介绍一个新的方法RTTI Run-Time Type Id

这里要用到一个关键字:typeid() 类似于sizeof()

typeid(表达式)返回的是包含类型的信息,用于判断

用代码解释就是:

			int a = 0;
			typeid(a) == typeid(int)

这个关键字需要头文件#include 的支持

新增头文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSVSDpIO-1685237139196)(C++.assets/image-20230525192542591.png)]

新增配置

#define FOEBIG_MOVE_STEP       4
#define FOEMID_MOVE_STEP       7
#define FOESMA_MOVE_STEP       10

实现移动并判断是否出界

void CFoeBox::MoveAllFoe(){
	list<CFoe*>::iterator ite = m_lstFoe.begin();
	while (ite != m_lstFoe.end()) {
		if (*ite) {
			if (typeid(**ite) == typeid(CFoeBig)) {  //大
				(*ite)->MoveFoe(FOEBIG_MOVE_STEP);
			}
			else if (typeid(**ite) == typeid(CFoeMid)) {  //中
				(*ite)->MoveFoe(FOEMID_MOVE_STEP);
			}
			else if (typeid(**ite) == typeid(CFoeSma)) {  //小
				(*ite)->MoveFoe(FOESMA_MOVE_STEP);
			}

			//判断是否出界
			if ((*ite)->m_y >= BACK_HEIGHT) {
				delete(*ite);   //删除敌人飞机
				(*ite) = nullptr;
				ite = m_lstFoe.erase(ite);  //删除节点
				continue;
			}
		}
		ite++;
	}

上方用typeid判断子类对象类型中要放**ite,先用 *ite找到父类指针,然后再 *找到子类对象

在程序中实现敌人飞机的显示和移动
头文件

加上敌人飞机盒子的头文件,解开敌人飞机盒子成员属性的注释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vCXBdsvM-1685237139197)(C++.assets/image-20230525194914329.png)]

显示

在重绘中去调用显示所有敌人飞机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BsWOPBE6-1685237139197)(C++.assets/image-20230525194937469.png)]

移动

由于敌人飞机是自动的去移动,也就是定时移动,那么还要添加定时器相关操作

::SetTimer(m_hWnd, FOE_MOVE_TIMERID, FOE_MOVE_INTERVAL, nullptr);

配置一下定时器ID跟频率

#define FOE_MOVE_TIMERID       5 
#define FOE_MOVE_INTERVAL      100

添加ID对应操作

	case FOE_MOVE_TIMERID:
	{
		m_foeBox.MoveAllFoe();
	}
	break;

现在显示和移动掉完了,也就是盒子相关的弄好了,但是还没有创建敌人飞机

创建也是定时的,所以定时器还要再加

	::SetTimer(m_hWnd, FOE_CREATE_TIMERID, FOE_CREATE_INTERVAL, nullptr);

配置

#define FOE_CREATE_TIMERID     6 
#define FOE_CREATE_INTERVAL    1000

那么创建敌人飞机我们用随机数来随机创建飞机的大小,我们还是用之前敌人飞机类里面的随机数种子

创建完飞机,调用一下初始化再把飞机放在盒子里面就可以了

	//根据概率 创建敌人飞机
	case FOE_CREATE_TIMERID:
	{
		int r = CFoe::rd() % 11;
		CFoe* pFoe = nullptr;
		if (r >= 0 && r <= 5) {
			pFoe = new CFoeSma;
		}
		else if (r > 5 && r <= 8) {
			pFoe = new CFoeMid;
		}
		else if (r > 8 && r <= 10) {
			pFoe = new CFoeBig;
		}
        pFoe->InitFoe();
		m_foeBox.m_lstFoe.push_back(pFoe);
	}
	break;
运行效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VbeUrg4x-1685237139197)(C++.assets/image-20230525201033219.png)]

敌人飞机与玩家飞机碰撞
原理

判断是否碰撞就是看敌人飞机与玩家飞机是否有重合,细致一点就是看敌人飞机的边缘与玩家飞机边缘是否有重合,那我们就取玩家飞机上的几个点来作为判断点,当然我们取的点越多那判断的就越严谨,然后判断点是否进入敌人飞机的矩形范围内

实现
bool CFoeBig::IsHitPlayer(CPlayer& player) {
	int x = player.m_x + PLAYER_WIDTH / 2;
	if (m_x <= x && x <= m_x + FOEBIG_WIDTH &&
		m_y <= player.m_y && player.m_y <= m_y + FOEBIG_HEIGHT
		) {
		return true;
	}

	int y = player.m_y + PLAYER_HEIGHT / 2;
	if (m_x <= player.m_x && player.m_x <= m_x + FOEBIG_WIDTH &&
		m_y <= y && y <= m_y + FOEBIG_HEIGHT
		) {
		return true;
	}

	int x3 = player.m_x + PLAYER_WIDTH;
	if (m_x <= x3 && x3 <= m_x + FOEBIG_WIDTH &&
		m_y <= y && y <= m_y + FOEBIG_HEIGHT
		) {
		return true;
	}

	return false;
}
敌人飞机与炮弹碰撞
原理

因为炮弹比较小,所以我们取他头上的一个点就可以了,然后判断是否在敌人飞机矩形范围内

实现
bool CFoeBig::IsHitGunner(CGunner* pGun) {
	int x = pGun->m_x + GUNNER_WIDTH / 2;
	if (m_x <= x && x <= m_x + FOEBIG_WIDTH &&
		m_y <= pGun->m_y && pGun->m_y <= m_y + FOEBIG_HEIGHT
		) {
		return true;
	}
	return false;
}

那现在我们写的是大飞机的,其他飞机也是同理,就不赘述了

写完判断是否碰撞之后我们要去根据返回值的真假来实现碰撞的效果

在APP中实现碰撞的效果
设置定时器

我们还是采取定时器去高频的接收是否碰撞

	::SetTimer(m_hWnd, CHECK_HIT_TIMERID, CHECK_HIT_INTERVAL, nullptr);

这个定时器的频率要设置高一点,起码是要比移动的频率高

#define CHECK_HIT_TIMERID      7
#define CHECK_HIT_INTERVAL     3
实现敌人飞机与玩家飞机相撞

首先我们要创建一个正常敌人飞机飞机链表的迭代器,然后如果判断碰撞玩家飞机为真,也就是刚才写的函数返回值为true,那么游戏结束,游戏结束首先就是所有能动的东西都会停下,那就是把定时器都停了,我们在这里调用StopTimer函数,然后在函数中去停止定时器,最后弹出一个窗口提示游戏结束,这里我们用api的一个函数MessageBox,然后手动投递一个关闭窗口的消息,用来模拟点x,程序才真正退出

	case CHECK_HIT_TIMERID:
	{
		list<CFoe*>::iterator iteFoe = m_foeBox.m_lstFoe.begin();
		while (iteFoe != m_foeBox.m_lstFoe.end()) {
			if (*iteFoe) {
				//判断是否碰撞玩家飞机
				if ((*iteFoe)->IsHitPlayer(m_player)) {
					//碰撞了
					StopTimer();
					::MessageBox(m_hWnd, L"GameOver", L"提示", MB_OK);
					//程序退出
					::PostMessage(m_hWnd, WM_CLOSE, 0, 0);  //手动投递一个关闭窗口的消息,来模拟点x,程序退出
					return;

				}
			}
			iteFoe++;
		}
	}
	break;
StopTimer()函数中去停止所有定时器

停止定时器就只需要设置定时器的前两个参数,也就是窗口句柄和消息定时器ID

然后名字为KillTimer

如果不去停止定时器,那所有物体不会停止,还会不断的弹出提示窗口

void CPlaneApp::StopTimer() {
	::KillTimer(m_hWnd  /*窗口句柄*/, BACK_MOVE_TIMERID/*定时器ID*/);
	::KillTimer(m_hWnd, CHECK_MOVE_TIMERID);
	::KillTimer(m_hWnd, GUNNER_MOVE_TIMERID);
	::KillTimer(m_hWnd, GUNNER_SEND_TIMERID);
	::KillTimer(m_hWnd, FOE_MOVE_TIMERID);
	::KillTimer(m_hWnd, FOE_CREATE_TIMERID);
	::KillTimer(m_hWnd, CHECK_HIT_TIMERID);
}

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Jkc2Uro-1685237139197)(C++.assets/image-20230526195136010.png)]

实现炮弹击中敌人飞机

我们还是在这个定时器处理函数中,在迭代器遍历中,扯出一条判断分支

	case CHECK_HIT_TIMERID:
	{
		list<CFoe*>::iterator iteFoe = m_foeBox.m_lstFoe.begin();
		bool isBoom = false;
		while (iteFoe != m_foeBox.m_lstFoe.end()) {
			if (*iteFoe) {
				//判断是否碰撞玩家飞机
				if ((*iteFoe)->IsHitPlayer(m_player)) {
					//碰撞了
					StopTimer();
					::MessageBox(m_hWnd, L"GameOver", L"提示", MB_OK);
					//程序退出
					::PostMessage(m_hWnd, WM_CLOSE, 0, 0);  //手动投递一个关闭窗口的消息,来模拟点x,程序退出
					return;

				}

				//判断是否撞击炮弹
				list<CGunner*>::iterator iteGun = m_gunBox.m_lstGun.begin();
				while (iteGun != m_gunBox.m_lstGun.end()) {
					if ((*iteFoe)->IsHitGunner(*iteGun)) {  //碰撞了
						delete (*iteGun);   //删除炮弹
						(*iteGun) = nullptr;

						iteGun = m_gunBox.m_lstGun.erase(iteGun);  //删除节点

						(*iteFoe)->m_blood -= GUNNER_HURT;  //敌人飞机掉血
						if ((*iteFoe)->m_blood <= 0) {  //爆炸
							m_foeBox.m_lstBoomFoe.push_back(*iteFoe);

							iteFoe = m_foeBox.m_lstFoe.erase(iteFoe);

							m_score++;   //分数++
							isBoom = true;
							break;
						}
						continue;
					}
					iteGun++;
				}
			}
			if (isBoom) isBoom = false;
			else iteFoe++;
		}
	}
	break;

然后我们如果击毁了敌人飞机,还要增加分数,所以还要去实现显示分数板和增加分数

实现敌人飞机的爆炸效果

显示爆炸效果就是不断地自动切换图片,所以还要加个定时器

::SetTimer(m_hWnd, CHANGE_PIC_TIMERID, CHANGE_PIC_INTERVAL, nullptr);
#define CHANGE_PIC_TIMERID     8
#define CHANGE_PIC_INTERVAL    200
::KillTimer(m_hWnd, CHANGE_PIC_TIMERID);
	case CHANGE_PIC_TIMERID:
	{
		list<CFoe*>::iterator ite = m_foeBox.m_lstBoomFoe.begin();
		while (ite != m_foeBox.m_lstBoomFoe.end()) {
			if (*ite) {
				(*ite)->m_showId--;

				if ((*ite)->m_showId < 0) {  //判断是否回收
					delete (*ite);
					(*ite) = nullptr;

					ite = m_foeBox.m_lstBoomFoe.erase(ite);
					continue;
				}
			}
			ite++;
		}
	}
	break;

显示效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gk6aIGif-1685237139198)(C++.assets/image-20230526204258076.png)]

显示分数板和增加分数

因为分数是APP自己的成员属性,所以要再构造里面初始化

CPlaneApp::CPlaneApp():m_score(0) {}

在初始化中加载图片到指定大小

::loadimage(&m_scoreBoard, L".\\res\\cardboard.png", 100, 40);

在显示分数函数中实现显示分数

void CPlaneApp::ShowScore() {
	//显示分数板
	::putimage(0, 0, &m_scoreBoard);
	//显示分数
	TCHAR buf[5] = { 0 };
	_itow_s(m_score, buf,10);  //将数字转成宽字节下的字符串  效果同 itoa
	RECT rect = { 0,0,100,40 };
	::settextcolor(RGB(52, 6, 9));
	::drawtext(buf, &rect, DT_CENTER | DT_SINGLELINE | DT_VCENTER);  //绘制文字到指定位置(矩形框)设置模式

}

最后在重绘中调用一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3T8TFUDH-1685237139198)(C++.assets/image-20230526205457545.png)]

结束

至此,飞机大战的游戏项目已经实现完毕,下面是最终效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fVJfTLVD-1685237139198)(C++.assets/image-20230526210223488.png)]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/578553.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI周报-一周发生两次Ai事件;DragGAN 问世

&#x1f680; AI 图像编辑技术 DragGAN 问世&#xff0c;用户可以通过拖拽改变汽车大小或人物表情等 近日&#xff0c;马克斯・普朗克计算机科学研究所研究者们推出了一种控制GAN的新方法DragGAN&#xff0c;用户可以通过拖拽改变汽车大小或人物表情等。 DragGAN类似于Photo…

Rk1126 实现 yolov5 6.2 推理

基于 RK1126 实现 yolov5 6.2 推理. 转换 ONNX python export.py --weights ./weights/yolov5s.pt --img 640 --batch 1 --include onnx --simplify 安装 rk 环境 安装部分参考网上, 有很多. 参考: https://github.com/rockchip-linux/rknpu 转换 RK模型 并验证 yolov562_t…

企业想提高商机转化率该如何挑选CRM系统

CRM客户管理系统可以帮助销售人员跟踪和分析潜在客户的需求、行为和偏好&#xff0c;制定合适的销售策略&#xff0c;提高商机转化率。下面我们就来说说&#xff0c;CRM系统如何加速销售商机推进。 1、跟踪客户和动态 Zoho CRM可以帮助您记录和分析客户的需求、行为和偏好&am…

8 年 SQL 人,撑不过前 6 题

抱歉各位&#xff0c;标题党了。。 前两天发布了一款 SQL 题集&#xff1a; 开发了一个SQL数据库题库小程序 <<- 戳它直达 群里小伙伴反馈&#xff0c;太简单&#xff1a; 于是&#xff0c;我又改版了下&#xff1a; 列举几题&#xff0c;大家看看难度&#xff1a; SQL S…

Python类的成员介绍

Python类的成员介绍 在Python中&#xff0c;类&#xff08;class&#xff09;是一种定义对象的模板。对象是由类创建的实例&#xff0c;它们具有属性和方法。属性是对象的变量&#xff0c;而方法是对象的函数。 定义在类中的变量也称为属性&#xff0c;定义在类中的函数也称为方…

DragGAN:interactive point-based manipulation on the generative image manifold

AI绘画可控性研究与应用清华美院课程的文字稿和PPThttps://mp.weixin.qq.com/s?__bizMzIxOTczNjQ2OQ&mid2247484794&idx1&sn3556e5c467512953596237d71326be6e&chksm97d7f580a0a07c968dedb02d0ca46a384643e38b51b871c7a4f89b38a04fb2056e084167be05&scene…

基于html+css的图展示97

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

测量平差实习心得精选三篇(合集)

测量平差实习心得精选三篇 测量平差实习心得一为期两周的实习在不断地学习、尝试、修正的过程中圆满结束了。这次实习让我对许多问题有了深刻的认识。我认识到编程的重要性&#xff0c;认识到自学能力的重要性&#xff0c;认识到从书本到实践还有很长一段路要走。 熟练掌握一门…

探索C++非质变算法:如何更高效地处理数据

前言 &#x1f4d6;欢迎大家来到小K的c专栏&#xff0c;本节将为大家带来C非质变算法的详解 &#x1f389;欢迎各位→点赞&#x1f44f; 收藏&#x1f49e; 留言&#x1f514;​ &#x1f4ac;总结&#xff1a;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指…

随机数发生器设计(二)

一、软件随机数发生器组成概述 密码产品应至少包含一个随机比特生成器。 软件随机数发生器应遵循GM/T 0105-2021《软件随机数发生器设计指南》要求&#xff0c;使用SM3或SM4算法作为生成随机比特算法。 本文以SM3算法为例描述软件随机数发生器的一个设计实例。需要注意的是&a…

如何在华为OD机试中获得满分?Java实现【猜字谜】一文详解!

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Java华为OD机试真题&#xff08;2022&2023) 文章目录 1、题目描述2、输入描述3、输出描述…

python爬虫——pandas的简单使用

pandas作为爬虫中最重要的包之一&#xff0c;我们要想学好爬虫&#xff0c;就必须要深入了解pandas 直接上代码 import pandas as pd import numpy as npdata pd.DataFrame(np.arange(16).reshape((4,4)),index[a,b,c,d],#如果不写列索引默认为0&#xff0c;1&#xff0c;…

基于html+css的图展示96

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【源码解析】Nacos配置热更新的实现原理

使用入门 使用RefreshScopeValue&#xff0c;实现动态刷新 RestController RefreshScope public class TestController {Value("${cls.name}")private String clsName;}使用ConfigurationProperties&#xff0c;通过Autowired注入使用 Data ConfigurationProperti…

警惕AI换脸技术:近期诈骗事件揭示的惊人真相

大家好&#xff0c;我是可夫小子&#xff0c;《小白玩转ChatGPT》专栏作者&#xff0c;关注AIGC、读书和自媒体。 目录 1. deepswap 2. faceswap 3. swapface 总结 &#x1f4e3;通知 近日&#xff0c;包头警方公布了一起用AI进行电信诈骗的案件&#xff0c;其中福州科技公…

医院PACS系统:三维多平面重建操作使用

三维多平面重建&#xff08;MPR\CPR&#xff09;界面工具栏&#xff1a; 按钮1&#xff1a;点击此按钮&#xff0c;用鼠标拖动正交的MPR定位线&#xff0c;可以动态浏览MPR图像。 按钮2&#xff1a;点击此按钮&#xff0c;按下鼠标左键在图像上作任意勾边&#xff0c;弹起鼠标…

python3.8安装rpy2

python3.8安装rpy2 rpy2是一个可以让r和python交互的库&#xff0c;非常强大&#xff0c;但是安装过程有些坎坷。 安装r语言 安装时首先需要安装r语言。 官网下载链接&#xff1a;https://www.r-project.org/ 选择与自己电脑相应的版本就好。 安装rpy2 然后需要安装rpy2库…

Radxa ROCK 5A RK3588S 开箱 vs 树莓派

Rock5 Model A 是一款高性能的单板计算机&#xff0c;采用了 RK3588S (8nm LP 制程&#xff09;处理器&#xff0c;具有 4 个高达 2.4GHz 的 ARM Cortex-A76 CPU 核心、4 个高达 1.8GHz 的 Cortex-A55 内核和 Mali-G610 MP4 GPU&#xff0c;支持 8K 60fps 视频播放&#xff0c…

光力转债上市价格预测

光力转债 基本信息 转债名称&#xff1a;光力转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;4.0亿元。 正股名称&#xff1a;光力科技&#xff0c;今日收盘价&#xff1a;22.53元&#xff0c;转股价格&#xff1a;21.46元。 当前转股价值 转债面值 / 转股…

Redis的常用数据结构之字符串类型

redis中的数据结构是根据value的值来进行区别的&#xff0c;主要分了String、Hash、List、Set&#xff08;无序集合&#xff09;、Zset&#xff08;有序集合&#xff09; 字符串&#xff08;String&#xff09; String类型是redis中最基础的数据结构&#xff0c;也可以理解为…