前言:最近学习Qt时,当做项目练手,把以前小学期要做的飞机大战又重新做了一遍。以前的时候是用MFC做的,现在改到了用Qt界面来做。不得不说,Qt确实是要好用多了。在此记录,方便自己,希望也能有助于大家。
飞机大战本身并不是很难,这里就介绍我实现过程中的重点了,主要分为以下几个部分:
1、类模型的建立
2、飞机如何移动
3、碰撞检测
4、爆炸动画的实现
5、飞机图片透明效果的实现
6、背景图的移动
7、游戏状态的转换
以下带着源码,分别介绍。
一、类模型的建立
学习完C++之后,飞机大战确实是一个比较好的练手项目,可以检验咱C++这个对象到底“谈”妥了没。 惭愧的说,到现在,我的面向对象思想建立的还是不够完善。在这个项目中,还没有体现到很深层次的面向对象的思路。以下是我简历的UML类图:
其中:
(1)、VObj类:这是我一开始抽象出来的物体类,就像Qt中的Object一样,但后来发现它把Plane类和Bullet类要做的工作都完成了,导致Plane和Bullet没什么可干的。这里完成的工作主要是物体的移动和判断物体是否消失。
(2)、Plane类和Bullet类:几乎是空的。为了与后面的类衔接合理些,也就没去删掉这个中间层。
(3)、MyPlane类:这是继承Plane的我方战机类。其中主要飞机的移动与发射子弹。稍微要注意点是这个类中含有MyBullet类的指针,这是为了发射子弹之用。
(4)、EnemyFIsh类:继承Plane类的敌方杂鱼类。其工作主要由上层的父类完成了,这里也几乎是空的。
(5)、MyBullet类:这是继承Bullet的子类。除了完成一些必要的工作,也几乎是空的。
(6)、Explosion类:爆炸类。这个类是一个独立的类,和其他的类都没有太大关系(和Widge类有点关系)。它主要是完成爆炸的一些工作,如加载爆炸的一系列图片,切换爆炸的图片,设置爆炸的位置等等。
(7)、Widget类:这个类是一个综合的类。从类图中也可以看出来,它和MyPlane、Explosion、EnemyFish都有关系。这个类继承了Qt的Object,通过重绘事件、键盘事件、定时器事件,把以上各类综合起来以达到飞机大战的效果。整个工程80%的工作都这这个类中,后面要介绍的其他几个重点也大部分都是在这个类中实现的。(说句废话,也正是从这个类中,可以看出来我的面向对象的设计理解的还不够深,还不能够设计出耦合性更小,代码利用率更高的程序。)
二、飞机如何移动
飞机时如何移动的呢?这里用了比较低级的做法,即通过检测按键事件,判断飞机的移动方向,然后在定时器事件中移动飞机,最后在重绘事件中不断绘制飞机。这样就达到了飞机移动的效果。同理,子弹的移动和敌方杂鱼机的移动也是这个原理。(这里应该可以用Qt更高级的动画框架来实现,目前我还没学会,嘿嘿)
主要的代码如下:
void Widget::keyPressEvent(QKeyEvent *event)
{
if(state == RUNNING_STATE) //第二个阶段时
{
int fly_dir = 0;
//判断按下哪个按键,并打上标记
if(event->key() == Qt::Key_Up)
fly_dir = FLY_UP;
if(event->key() == Qt::Key_Down)
fly_dir = FLY_DOWN;
if(event->key() == Qt::Key_Left)
fly_dir = FLY_LEFT;
if(event->key() == Qt::Key_Right)
fly_dir = FLY_RIGHT;
myPlane->SetFlyDir(fly_dir,true); //给指定的方向打上标记
if(event->key() == Qt::Key_Space)
{
myPlane->FireBull(); //发射子弹
}
}
else
{
if(event->key() == Qt::Key_Return)
{
state = RUNNING_STATE;
Init();
}
}
}
//定时器事件:主要用于移动飞机;判断战机和敌机、敌机和子弹是否发生碰撞;增加敌机数量;显示爆炸(这里只有定时器的一部分代码)
void Widget::timerEvent(QTimerEvent *event)
{
if(event->timerId() == timer1) //移动飞机和子弹
{
//移动我方战机
for(int i = 1;i<=4;++i)
{
if(myPlane->GetFLyDir(i)) //上下左右:对应1 2 3 4
myPlane->Move(i);
}
//移动子弹
QLinkedList::iterator iter_bul;
for(iter_bul = myPlane->my_bulles.begin();iter_bul!=myPlane->my_bulles.end();++iter_bul)
{
MyBullet *temp_bul = *iter_bul;
temp_bul->Move(-1);
}
//移动敌方杂鱼机
QLinkedList::iterator iter_fish;
for(iter_fish = fish_list.begin();iter_fish != fish_list.end();++iter_fish)
{
EnemyFish * temp_fish = *iter_fish;
temp_fish->Move(0);
}
this->update();
}
}
//重绘事件:
void Widget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
if(state == RUNNING_STATE)
{
ui->label_life->setText(QString::number(myPlane->GetLife()));
ui->label_sco->setText(QString::number(myPlane->GetScore()));
//绘制动态背景
painter.drawPixmap(0,back_spe,back);
painter.drawPixmap(0,back_spe-back.height(),back);
//绘制我方飞机
painter.drawPixmap(myPlane->pos,myPlane->img);
//绘制敌机
foreach(EnemyFish *temp_fish,fish_list)
painter.drawPixmap(temp_fish->pos,temp_fish->img);
//绘制我方子弹
foreach (MyBullet *temp_bul, myPlane->my_bulles)
painter.drawPixmap(temp_bul->pos,temp_bul->img);
//绘制爆炸
foreach(Explosion * temp_exp,exp_list)
{
if(temp_exp->GetCurPicNum()<=EXPLOSION_IMG_NUM)
painter.drawPixmap(temp_exp->GetPos(),*(temp_exp->exp_img_list[temp_exp->GetCurPicNum()-1]));
}
}
else
{
painter.drawPixmap(0,0,back);
if(state == END_STATE)
{
ui->label_life->setText(QString::number(myPlane->GetLife()));
ui->label_sco->setText(QString::number(myPlane->GetScore()));
}
}
painter.end();
}
这里还需要注意一点的是:在按键事件中不能直接移动飞机,而应该做一个标志位,然后在定时器中再进行移动,否则不能同时相应两个按键,比如同时按下左和上,飞机向左上方飞。
三、碰撞检测
何为碰撞检测?即在游戏中判断飞机是否和敌机发生碰撞,飞机的子弹是否和敌机发生碰撞(即是否攻击到敌机)。在这里,基本的原理是判断两个对象(飞机和敌机、敌机和子弹)矩形(即图像所占的空间)是否有交叉,如果有交叉,即发生了碰撞。使用的是QRect::intersects()方法。主要代码如下:
else if(event->timerId() == timer2) //判断是否发生碰撞
{
QLinkedList::iterator iter;
QLinkedList::iterator iter_bul;
bool IsAttacked = false;
bool IsCon = false;
for(iter = fish_list.begin();iter!=fish_list.end();++iter) //遍历敌方所有杂鱼
{
EnemyFish *temp_fish = *iter;
IsAttacked = false; //初始未被攻击
QRect temp(temp_fish->pos,QSize(temp_fish->img.width(),temp_fish->img.height()));
for(iter_bul = myPlane->my_bulles.begin();iter_bul !=myPlane->my_bulles.end();++iter_bul) //遍历我方所有子弹
{
MyBullet *temp_bul = *iter_bul;
QRect temp_rec(temp_bul->pos,QSize(temp_bul->img.width(),temp_bul->img.height()));
IsAttacked = temp.intersects(temp_rec); //判断是否敌机是否受到我方子弹攻击
if(IsAttacked == true ) //子弹攻击到敌机
{
EneFishIsAttacked();
Explosion *temp_exp = new Explosion; //产生爆炸
temp_exp->SetPos(temp_fish->pos);
exp_list.append(temp_exp);
myPlane->my_bulles.removeOne(*iter_bul); //移除子弹
delete temp_bul;
fish_list.removeOne(*iter); //小虾米碰撞到子弹,删除敌机
delete temp_fish;
break;
}
else
{
if(temp_bul->IsDisappear() == true)
{
myPlane->my_bulles.removeOne(*iter_bul); //子弹消失,移除子弹
delete temp_bul;
}
}
}
if(IsAttacked == false)
{
//判断我方战机是否碰撞到敌方战机
QRect myPlane_rect(myPlane->pos,QSize(myPlane->img.width(),myPlane->img.height()));
IsCon = temp.intersects(myPlane_rect);
if(IsCon == true)
{
Explosion *temp_exp = new Explosion; //产生爆炸
temp_exp->SetPos(temp_fish->pos);
exp_list.append(temp_exp);
fish_list.removeOne(*iter); //小虾米碰撞到我方飞机,删除敌机
delete temp_fish;
EneFishIsAttacked();
MyPlaneIsAttacked(); //我方战机损失生命力
break;
}
else
{
if(temp_fish->IsDisappear() == true)
{
fish_list.removeOne(*iter); //敌机移除屏幕,删除敌机
delete temp_fish;
}
}
}
}
back_spe= (back_spe+1)%back.height(); //背景图片移动
}
具体来说,就是遍历子弹列表和敌机列表,在两层循环里判断每个子弹是否和敌机列表中的敌机发生碰撞。判断战机和敌机是否发生碰撞也是这个原理。(总的来说,我是感觉这种方式有点low。Qt的高级图形视图框架里有碰撞检测机制,可以运用到这里,图形视图框架,我弄的还不是很熟,也就没采用了)。
今天,先写到这儿吧。有点多,剩下的下次在写吧。在这儿,先上张图: