它与程序类是组合关系。
他需要作为一个子类来继承游戏壳(CGameFrame)。
由于继承了游戏壳,所以要将游戏壳中的虚函数在此处实现
他与程序类是组合关系。
他与炮弹类是依赖关系。
void InitPlayer(); //初始化
void ShowPlayer(); //显示贴图
void MovePlayer(int direct,int step); //移动 由于要捕获键盘输入的信息才能确定移动的方向,所以要有一个int类型的参数。
CGunner* SendGunner(); //发射炮弹 返回值应为炮弹类的指针
创建炮弹盒子类是为了统一管理炮弹,因为在程序中不只有一枚炮弹,它与炮弹是聚合关系。
与程序类是组合关系。
这里不用统一初始化,因为炮弹在被创建的时候就已经初始化过了
三种敌人飞机的父类。
他与炮弹类和玩家类都是依赖的关系
是敌人飞机类的子类
是敌人飞机类的子类
是敌人飞机类的子类
因为敌人飞机也不止一个,所以要创建一个类来管理敌人飞机,他与敌人飞机类是聚合关系,与程序类是组合关系。
正常创建一个名为飞机大战的项目
以上创建头文件与原文件均是先创建一个文本文档,然后将其后缀名改为.h与.cpp即可
我们手动去建这些文件夹是为了更方便管理也提高了可读性,如果用vs去建那么默认就在工程所在文件夹中建出来了
全部整备完毕的文件夹应该如下图所示
创建虚拟目录的方法:
添加现有项的快捷键:Shift+Alt+A
先在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类并将类图中的属性与函数在里面声明出来,还有构造析构函数,要包含头文件
#pragma once
#include
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;
}
}
定时器原理定时器会定时产生WM_TIMER消息,然后增加一个对应的定时器消息映射表用于接收此类消息,然后调用对应的处理函数,处理函数中WPARAM会接收到定时器ID,最终在处理函数中根据定时器ID做相应的操作
增加消息映射表的方法:消息映射表是我们在游戏壳中创建的一个映射表,我们通过这个表的key找到消息类别和处理函数的结构体,然后通过回调函数去接收处理消息,我们为了方便根据具体游戏添加相应的消息映射表,我们留了一个接口。那么在游戏中添加消息映射表的方法为:创建一个处理函数(函数名一定要为On_消息ID的样式),然后在AddMsgMap中添加对应的宏==(INIT_MSGMAP(消息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"飞机大战")
显示窗口:
将类图中的成员属性和成员函数粘贴到头文件中,并且添加构造析构函数
由于炮弹类还没有开发,所以先将发射炮弹的函数注释掉
#pragma once
#include
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),然后以位与方式贴原图,白色部分位与任何 颜色都为任何颜色,所以白边部分得到的还是背景色,飞机部分与下面屏蔽图白色部分得到的是飞机颜色,所以在显示的时候就只显示飞机部分了。
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);
}
}
首先在头文件中将玩家飞机的头文件包含在里面,然后将玩家类型的成员属性取消注释,用于之后操作
因为玩家飞机在程序刚开始就有了,所以玩家飞机的初始化要在程序初始化时就实现
显示玩家飞机:
那么玩家飞机怎么移动呢,因为他不是定时自己去移动,而是在我们键盘按下后再去移动,所以我们还要添加一个键盘按下的消息映射表
先在添加消息映射表中添加一个键盘按下的消息映射表,然后在头文件中声明处理函数,源文件中定义处理函数
因为键盘按下属于键盘类别的消息,所以参数为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
显示效果:
但是我们发现在移动的时候会有顿挫感,那么怎么才能让移动更加灵活更加丝滑呢
分析原因:如果我们一下一下按方向键的话,那么他移动的频率就决定于手速,但是如果我们一直按下方向键,那么他的移动频率就由系统决定了,所以有顿挫感的原因就是系统发射消息的频率太低
那么怎么提高这个频率呢,我们肯定不能更改系统的发射频率,所以我们想到可以用定时器来解决,用定时器以很高的频率来检测玩家是否按下方向键
那么有了思路之后我们来设定定时器
::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
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
#include"Gunner.h"
using namespace std;
class CGunnerBox {
public:
list m_lstGun;
public:
CGunnerBox();
~CGunnerBox();
public:
void ShowAllGunner();
void MoveAllGunner();
};
要在析构函数中遍历回收炮弹,采用迭代器遍历
#include "GunnerBox.h"
CGunnerBox::CGunnerBox(){}
CGunnerBox::~CGunnerBox(){
list::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;
}
}
}
但是写到这里我们发现出现了一些问题,我们只能回收掉对象,但是无法删除节点
所以我们要将for循环遍历改为迭代器遍历,然后将出界的炮弹节点回收掉,注意这里不能在最后将整条链表的节点都清空,析构函数清空节点是因为程序已经关闭
所以使用erase函数来删除节点,并且删除完这个节点后还可能会有下一个节点,所以要用迭代器接一下返回值
用迭代器接回收掉的节点的话自带一个迭代器后移效果,那么在判断后就不需要再++了,所以就会出现一个局面,就是如果炮弹出界了,迭代器会接收回收节点后向后移动一下,然后循环内还会再++一次
所以这里我们选择在删除节点后加一个continue,如果删除节点就不再执行循环体后面的代码了
void CGunnerBox::MoveAllGunner(){
list::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++;
}
}
在开发玩家类的时候有一个发射炮弹的方法没有书写,因为当时还没有创建炮弹类,那么现在就可以去定义实现了
首先在玩家类源文件中包含炮弹的头文件
然后再声明定义发射炮弹的方法
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;
}
将炮弹盒子成员属性取消注释,加上炮弹盒子的头文件
在重绘里面调用显示所有炮弹函数
所有炮弹移动要在定时器处理函数中实现
所以就要设置一个炮弹移动的定时器,然后在配置文件中加上定时器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;
那么发射炮弹的流程就是,玩家飞机发射炮弹,然后在发射的同时会初始化炮弹,之后炮弹进入到炮弹盒子中,炮弹盒子会显示移动所有炮弹
发射效果
炮弹发射原理
定时器处理函数会自动调用发射炮弹函数,发射炮弹时会返回一个炮弹类指针,并将它放在炮弹盒子链表中,也就是将炮弹装进炮弹盒子,并且还会自动调用移动所有炮弹函数,发射炮弹就是new一个炮弹对象然后用指针指向,并且确定炮弹的初始坐标位置,调用炮弹初始化函数,然后返回一个炮弹类指针,炮弹盒子中如果有炮弹,那么迭代器链表的节点就不为空,也就是有炮弹指针,那么就会显示炮弹并移动炮弹,如果炮弹出界,那就会回收掉指针,炮弹也就不会显示了
将类图中设计好的成员复制到类中,并写上构造析构
因为其中的方法用到了玩家飞机和炮弹,所以要把他们两个的头文件包含进来
#pragma once
#include
#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
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
#include"Foe.h"
using namespace std;
class CFoeBox {
public:
listm_lstFoe;
listm_lstBoomFoe;
public:
CFoeBox();
~CFoeBox();
void ShowAllFoe();
void MoveAllFoe();
};
#include"FoeBox.h"
CFoeBox::CFoeBox() {}
CFoeBox::~CFoeBox(){
}
void CFoeBox::ShowAllFoe()
{
}
void CFoeBox::MoveAllFoe()
{
}
CFoeBox::~CFoeBox(){
list::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 的支持
新增头文件
新增配置
#define FOEBIG_MOVE_STEP 4
#define FOEMID_MOVE_STEP 7
#define FOESMA_MOVE_STEP 10
实现移动并判断是否出界
void CFoeBox::MoveAllFoe(){
list::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找到父类指针,然后再 *找到子类对象
加上敌人飞机盒子的头文件,解开敌人飞机盒子成员属性的注释
在重绘中去调用显示所有敌人飞机
由于敌人飞机是自动的去移动,也就是定时移动,那么还要添加定时器相关操作
::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;
判断是否碰撞就是看敌人飞机与玩家飞机是否有重合,细致一点就是看敌人飞机的边缘与玩家飞机边缘是否有重合,那我们就取玩家飞机上的几个点来作为判断点,当然我们取的点越多那判断的就越严谨,然后判断点是否进入敌人飞机的矩形范围内
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;
}
那现在我们写的是大飞机的,其他飞机也是同理,就不赘述了
写完判断是否碰撞之后我们要去根据返回值的真假来实现碰撞的效果
我们还是采取定时器去高频的接收是否碰撞
::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::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;
停止定时器就只需要设置定时器的前两个参数,也就是窗口句柄和消息定时器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);
}
测试:
我们还是在这个定时器处理函数中,在迭代器遍历中,扯出一条判断分支
case CHECK_HIT_TIMERID:
{
list::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::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::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;
显示效果:
因为分数是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); //绘制文字到指定位置(矩形框)设置模式
}
最后在重绘中调用一下
至此,飞机大战的游戏项目已经实现完毕,下面是最终效果