主界面的布局采用固定大小的窗口,使用图片背景;有一个由图片填充的按钮,点击可开始游戏。 主界面窗口的参数需在配置头文件 config.h 中提前设定:
#define GAME_WIDTH 480
#define GAME_HEIGHT 854
#define GAME_TITLE "飞机大战"
开始按钮需设定为图片填充:
bt_start = new QPushButton;
bt_start->setGeometry(GAME_WIDTH/2, GAME_HEIGHT/2, 161, 43);
bt_start->clearMask();
bt_start->setBackgroundRole(QPalette::Base);
QPixmap startpix;
startpix.load(":/images/start.png");
bt_start->setFixedSize(startpix.width(), startpix.height());
bt_start->setMask(startpix.createHeuristicMask());
bt_start->setIcon(startpix);
bt_start->setIconSize(QSize(startpix.width(), startpix.height()));
此时的按钮布局并不在窗口的中间,这里采用了透明的lable和网格布局,将按钮居中显示:
fill1 = new QLabel;
fill2 = new QLabel;
fill3 = new QLabel;
QGridLayout *gbox = new QGridLayout;
gbox->addWidget(fill1, 1, 0, 3, 1);
gbox->addWidget(bt_start, 1, 1, 1, 1);
gbox->addWidget(fill2, 1, 2, 3, 1);
gbox->addWidget(fill3, 0, 0, 1, 3);
setLayout(gbox);
最后再连接按钮的槽函数:
connect(bt_start, SIGNAL(clicked(bool)), this, SLOT(showgame()));
在showgame()槽函数中显示出游戏主界面
地图滚动的实现原理:使用两张相同的地图资源图片,在窗口中从上到下循环播放,当第二张照片滚动出屏幕时,回到初始位置重新开始滚动,具体实现函数如下:
pos1Y += SCROLL_SPEED;
if(pos1Y >= 0)
pos1Y = -GAME_HEIGHT;
pos2Y += SCROLL_SPEED;
if(pos2Y >= GAME_HEIGHT)
pos2Y = 0;
其中,SCOLL_SPEED等参数在config.h中提前定义。
在实现了地图滚动之后,就要将自己的飞机添加到游戏界面中了,在myplane类中设定好飞机的初始坐标和边框,再使用drawPixmap绘制在窗口中即可,部分代码如下:
myplane::myplane(QWidget *parent) : QWidget(parent)
{
my_plane.load(MY_PLANE);
my_x = GAME_WIDTH * 0.5 - my_plane.width()*0.5;
my_y = GAME_HEIGHT - my_plane.height();
my_rect.setWidth(my_plane.width());
my_rect.setHeight(my_plane.height());
my_rect.moveTo(my_x, my_y);
}
void myplane::setPosition(int x, int y) //设置飞机位置
{
my_x = x;
my_y = y;
my_rect.moveTo(my_x, my_y);
}
为了使玩家只能通过死亡,暂停后退出来退出当前游戏界面,将该界面设置为无边框可拖动的形式,代码如下:
this->setWindowFlags(Qt::FramelessWindowHint);
void gamewindow::mousePressEvent(QMouseEvent *event)
{
this->windowPos = this->pos(); // 获得部件当前位置
this->mousePos = event->globalPos(); // 获得鼠标位置
this->dPos = mousePos - windowPos; // 移动后部件所在的位置
}
void gamewindow::mouseMoveEvent(QMouseEvent *event)
{
this->move(event->globalPos() - this->dPos);
}
至此,游戏界面已经初步成型了,接下来就需要控制飞机的移动和发射炮弹了
该游戏采用键盘方向键来控制飞机的移动,获取键盘喊下事件,将按下的方向键记录到本机移动方向变量中,再根据所记录的移动方向修改飞机响应的坐标即可,最后再将飞机更改后的位置绘制在游戏界面中,部分代码如下:
void gamewindow::keyPressEvent(QKeyEvent *event)
{
if(event->key() == Qt::Key_Up)
me.fly_dir = FLY_UP;
if(event->key() == Qt::Key_Down)
me.fly_dir = FLY_DONW;
if(event->key() == Qt::Key_Left)
me.fly_dir = FLY_LEFT;
if(event->key() == Qt::Key_Right)
me.fly_dir = FLY_RIGHT;
}
void gamewindow::me_pos()
{
if(me.fly_dir == FLY_UP){
me.my_y = me.my_y - 30;
if(me.my_y < 0)
me.my_y = 0;
me.setPosition(me.my_x, me.my_y);
me.fly_dir = 0;
}
if(me.fly_dir == FLY_DONW){
me.my_y = me.my_y + 30;
if(me.my_y > GAME_HEIGHT - me.my_plane.height())
me.my_y = GAME_HEIGHT- me.my_plane.height();
me.setPosition(me.my_x, me.my_y);
me.fly_dir = 0;
}
if(me.fly_dir == FLY_LEFT){
me.my_x = me.my_x - 40;
if(me.my_x < 0)
me.my_x = 0;
me.setPosition(me.my_x, me.my_y);
me.fly_dir = 0;
}
if(me.fly_dir == FLY_RIGHT){
me.my_x = me.my_x + 40;
if(me.my_x > GAME_WIDTH - me.my_plane.width())
me.my_x = GAME_WIDTH- me.my_plane.width();
me.setPosition(me.my_x, me.my_y);
me.fly_dir = 0;
}
}
void gamewindow::me_position_change()
{
p.start(); //启动位置更新定时器
connect(&p, &QTimer::timeout, [=](){
me_pos();
update();
});
}
实现了本机的移动之后,就应该在本机的当前位置发射炮弹了。采用一个Vector容器存储一定量的炮弹,每发炮弹以一定的时间间隔发射,并记录容器中的炮弹是否被射出,在射出后为其置上一个标志,方便炮弹的”重复利用“;当炮弹飞出屏幕后(这里暂不判断射中敌机)将该炮弹置为未发射的状态即可。当炮弹被发射后,只需根据在config.h中设定的炮弹飞行速度来更改炮弹的Y轴坐标并重绘图片即可实现炮弹的飞行,部分代码如下:
bullet::bullet(QWidget *parent) : QWidget(parent)
{
b_pix.load(MY_BULLET);
b_x = GAME_WIDTH * 0.5 - b_pix.width() * 0.5;
b_y = GAME_HEIGHT;
b_is_shoot = false; //子弹未发射
b_speed = BULLET_SPEED;
b_rect.setWidth(b_pix.width());
b_rect.setHeight(b_pix.height());
b_rect.moveTo(b_x, b_y);
}
void bullet::bullet_pos()
{
if(b_is_shoot) //子弹已发射
{
b_y -= b_speed;
b_rect.moveTo(b_x, b_y);
if(b_y <= -b_rect.height())
b_is_shoot = false;
}
else
return;
}
void gamewindow::shoot() //发射子弹
{
shoot_record++;
if(shoot_record < BULLET_INTERVAL)
return;
shoot_record = 0;
for(int i = 0; i < BULLET_NUM; i++){
if(!my_bullet[i].b_is_shoot)
{
my_bullet[i].b_is_shoot = true;
my_bullet[i].b_x = me.my_x + me.my_rect.width()*0.5 - 10;
my_bullet[i].b_y = me.my_y - 25;
break;
}
}
}
至此,本机的主要功能也已基本实现。
敌机的生成与本机的生成类似,区别仅在于敌机产生位置的X轴坐标为随机数,故需要设定合理的随机数在不同的位置来生成敌机,此外,敌机也和炮弹类似,需要使用一个Vector容器和是否被发射的标志来重复利用。敌机的移动也与炮弹类似,这里不再赘述,部分代码如下:
enemyplane::enemyplane()
{
enemy_pix.load(ENEMY_PLANE);
enemy_x = 0;
enemy_y = 0;
is_out = false;
enemy_speed = ENEMY_SPEED;
enemy_rect.setWidth(enemy_pix.width());
enemy_rect.setHeight(enemy_pix.height());
enemy_rect.moveTo(enemy_x, enemy_y);
}
void enemyplane::enemy_pos()
{
if(!is_out)
return;
enemy_y += enemy_speed;
enemy_rect.moveTo(enemy_x, enemy_y);
if(enemy_y >= GAME_HEIGHT + enemy_rect.height())
{
is_out = false;
}
}
void gamewindow::out_enemy_plane()
{
out_record++;
if(out_record < ENEMY_INTERVAL)
return;
out_record = 0;
for(int i = 0; i < ENEMY_NUM; i++){
if(!enemy[i].is_out){
enemy[i].is_out = true;
enemy[i].enemy_x = rand()%(GAME_WIDTH - enemy[i].enemy_rect.width());
enemy[i].enemy_y = -enemy[i].enemy_rect.height();
break;
}
}
}
在实现了本机的移动、本机炮弹发射和敌机生成后,就可以实现炮弹和敌机的碰撞检测了,本机发射的炮弹和敌机都有各自的边框,当检测到边框重合时,即发生了碰撞,便可以产生敌机爆炸,音效播放,敌机消失等事件,碰撞检测的部分代码如下:
for(int i = 0; i < ENEMY_NUM; i++){
if(!enemy[i].is_out){
continue;
}
for(int j = 0; j < BULLET_NUM; j++){
if(!my_bullet[j].b_is_shoot)
continue;
if(enemy[i].enemy_rect.intersects(my_bullet[j].b_rect)){
enemy[i].is_out = false;
my_bullet[j].b_is_shoot = false;
//score->setText(QString("得分:%1").arg(++s));
}
}
}
产生碰撞后,便可以开始播放爆炸图片,爆炸效果由七张图片组成,通过快速播放七张图片,模拟爆炸效果,爆炸和音效播放的部分代码如下:
explotion::explotion(QWidget *parent) : QWidget(parent)
{
for(int i = 1; i <= EXPLOTION_MAX; i++){
QString str = QString(EXPLOTION_PATH).arg(i);
exp_pix.push_back(QPixmap(str));
}
exp_x = 0;
exp_y = 0;
exp_boom = false;
pix_index = 0;
exp_record = 0;
}
void explotion::updatepix()
{
if(!exp_boom)
return;
exp_record++;
if(exp_record < EXPLOTION_INTERVAL)
return;
exp_record = 0;
pix_index++;
if(pix_index > EXPLOTION_MAX - 1){
pix_index = 0;
exp_boom = false;
}
}
//以下代码在碰撞检测代码中
for(int k = 0; k < EXPLOTION_NUM; k++){
if(!exp[k].exp_boom){
QSoundEffect *boom=new QSoundEffect;
boom->setSource(QUrl::fromLocalFile(SOUND_EXPLOTION));
boom->setVolume(0.07f); //音量
boom->play();
exp[k].exp_boom = true;
exp[k].exp_x = enemy[i].enemy_x;
exp[k].exp_y = enemy[i].enemy_y;
break;
}
}
至此已经可以实现正常的发射炮弹,摧毁敌机的功能,接下来就需要实现敌机炮弹的发射了
敌机的炮弹发射与本机的炮弹发射原理一致,这里不再赘述,只展示部分代码:
enemybullet::enemybullet()
{
e_b_pix.load(ENEMY_BULLET);
e_b_x = GAME_WIDTH * 0.5 - e_b_pix.width() * 0.5;
e_b_y = e_b_pix.height();
e_b_is_shoot = false; //敌机子弹未发射
e_b_speed = ENEMY_BULLET_SPEED;
e_b_rect.setWidth(e_b_pix.width());
e_b_rect.setHeight(e_b_pix.height());
e_b_rect.moveTo(e_b_x, e_b_y);
}
void enemybullet::enemy_bullet_pos()
{
if(e_b_is_shoot){
e_b_y += e_b_speed;
e_b_rect.moveTo(e_b_x, e_b_y);
if(e_b_y >= GAME_HEIGHT)
e_b_is_shoot = false;
}
else
return;
}
在实现敌机可发射炮弹后,同样需要检测本机与敌机子弹的碰撞,并增加本机血条与得分机制,增强可玩性。
生命值初始为10点,当检测到本机与敌机子弹发生碰撞后会减少生命值,并触发爆炸特效,当生命值减为0时,会弹窗提示重新开始游戏,并返回主界面,部分代码如下:
void gamewindow::lifecount()
{
for(int j = 0; j < ENEMY_BULLET_NUM; j++){
if(!en_bullet[j].e_b_is_shoot)
continue;
if(me.my_rect.intersects(en_bullet[j].e_b_rect)){
life->setText(QString("生命:%1").arg(--l));
en_bullet[j].e_b_is_shoot = false;
if(l == 0){
mbox = new QMessageBox;
mbox->setText("很遗憾,请再次尝试");
mbox->setStandardButtons(QMessageBox::Ok);
if(mbox->exec() == QMessageBox::Ok){
t.stop();
p.stop();
bells->stop();
gamewindow::close();
}
}
for(int k = 0; k < EXPLOTION_NUM; k++){
if(!exp[k].exp_boom){
QSoundEffect *boom=new QSoundEffect;
boom->setSource(QUrl::fromLocalFile(SOUND_EXPLOTION));
boom->setVolume(0.07f); //音量
boom->play();
exp[k].exp_boom = true;
exp[k].exp_x = en_bullet[j].e_b_x;
exp[k].exp_y = en_bullet[j].e_b_y;
break;
}
}
}
}
}
当击落一架敌机后,会将总分数加1,并将分数显示在左上角,部分代码如下:
if(enemy[i].enemy_rect.intersects(my_bullet[j].b_rect)){
enemy[i].is_out = false;
my_bullet[j].b_is_shoot = false;
score->setText(QString("得分:%1").arg(++s));}
在游戏界面的右上角有暂停按钮,该按钮的设计同主界面的开始游戏按钮类似,点击暂停后可以选择继续游戏或停止游戏返回主菜单,部分代码如下:
void gamewindow::pause_window()
{
t.stop();
p.stop();
bells->stop();
retwindow = new QMessageBox;
retwindow->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
retwindow->button(QMessageBox::Yes)->setText("继续游戏");
retwindow->button(QMessageBox::No)->setText("返回主菜单");
if(retwindow->exec() == QMessageBox::Yes){
t.start();
p.start();
bells->play();
retwindow->close();
}
else{
t.stop();
p.stop();
bells->stop();
gamewindow::close();
}
}