第十三章- 游戏结束

在这里插入图片描述
在这里插入图片描述

在这个例子中我们开始研究一个带有记分的真正可玩的游戏。我们给MyWidget一个新的名字GameBoard并添加一些槽。

我们把定义放在gamebrd.h并把实现放在gamebrd.cpp

CannonField现在有了一个游戏结束状态。

LCDRange中的布局问题已经修好了。

  • lcdrange.h包含LCDRange类定义
  • lcdrange.cpp包含LCDRange类实现
  • cannon.h包含CannonField类定义
  • cannon.cpp包含CannonField类实现
  • gamebrd.h包含GameBoard类定义
  • gamebrd.cpp包含GameBoard类实现
  • main.cpp包含MyWidget和main

一行一行地解说

lcdrange.h

#include <QWidget>

class QSlider;
class QLabel;

class LCDRange : public QWidget

我们继承了QWidget,并且使用QVBoxLayout。QVBoxLayout不是一个窗口部件,它管理窗口部件。

lcdrange.cpp

LCDRange::LCDRange(QWidget *parent, Qt::WindowFlags name)
    : QWidget(parent, name)

我们使用一种平常的方式继承QWidget

另外一个构造函数作了同样的改动。init()没有变化

    QVBoxLayout *layout = new QVBoxLayout;

我们使用所有默认值创建一个QVBoxLayout,管理这个窗口部件的子窗口部件。

cannon.h:

CannonField现在有一个游戏结束状态和一些新的函数。

    bool  gameOver() const {return gameEnded;}

如果游戏结束了,这个函数返回TRUE,或者如果游戏还在继续,返回FALSE。

    void  setGameOver();
    void  restartGame();

这里是两个新槽:setGameOver()restartGame()

    void  canShoot(bool);

这个新的信号表明CannonField使shoot()槽生效的状态。我们将在下面使用它用来使`Shoot按钮生效或失效。

    bool gameEnded;

这个私有变量包含游戏的状态。true说明游戏结束,false说明游戏还将继续。

cannon.cpp:

    gameEnded = false;

这一行已经被加入到构造函数中。最开始的时候,游戏没有结束。

void CannonField::shoot()
{
    if (isShooting())
        return;
    timerCount = 0;
    shoot_ang = ang;
    shoot_f = f;
    autoShootTimer->start(50);
    emit canShoot(false);
}

我们添加一个新的isShooting()函数,所以shoot()使用它替代直接的测试。同样,shoot告诉CannonField现在不可以射击。

void CannonField::setGameOver()
{
    if (gameEnded)
        return;
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = true;
    repaint();
}

这个槽终止游戏。它必须被CannonField外面的调用,因为这个窗口部件不知道什么时候终止游戏。这是组件编程中一条重要设计原则。我们选择使组件可以尽可能灵活以适应不同的规则(比如,在一个首先射中十次的人胜利的多人游戏版本可能使用不变的CannonField)。

如果游戏已经被终止,我们立即返回。如果游戏会继续到我们的射击完成,设置游戏结束标志,并且重新绘制整个窗口部件。

void CannonField::restartGame()
{
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = false;
    repaint();
    emit canShoot(true);
}

这个槽开始一个新游戏。如果炮弹还在空中,我们停止射击。然后我们重置gameEnded变量并重新绘制窗口部件。

就像hit()miss()一样,moveShot()同时也发射新的canShoot(true)信号。

CannonField::paintEvent()的修改:

void CannonField::paintEvent(QPaintEvent *e)
{
    QRect updateR = e->rect();
    QPainter p(this);

    if (gameEnded)
    {
        p.setPen(Qt::black);
        p.setFont(QFont("Courier", 48, QFont::Bold ));
        p.drawText(rect(),Qt::AlignHCenter, "Game Over");
    }

绘画事件已经通过如果游戏结束,比如gameEndedtrue,就显示文本Game Over而被增强了。我们在这里不怕麻烦来检查更新矩形,是因为在游戏结束的时候速度不是关键性的。

为了画文本,我们先设置了黑色的画笔,当画文本的时候,画笔颜色会被用到。接下来我们选择Courier字体中的48号加粗字体。最后我们在窗口部件的矩形中央绘制文本。不幸的是,在一些系统中(特别是使用Unicode的X服务器)它会用一小段时间来载入如此大的字体。因为Qt缓存字体,我们只有第一次使用这个字体的时候才会注意到这一点。

    if (updateR.intersects(cannonRect()))
        paintCannon(&p);
    if (autoShootTimer->isActive() && updateR.intersects(shotRect()))
        paintShot(&p);
    if (updateR.intersects(targetRect()))
        paintTarget(&p);
}

我们只有在射击的时候画炮弹,在玩游戏的时候画目标。

gamebrd.h

这个文件是新的。它包含最后被用来作为MyWidgetGameBoard类的定义。

#ifndef GAMEBOARD_H
#define GAMEBOARD_H

#include <QWidget>
#include "cannon.h"

class LCDRange;
class QLCDNumber;
class CannonField;
class QPushButton;

class GameBoard : public QWidget
{
    Q_OBJECT
public:
    explicit GameBoard(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());

protected slots:
    void  fire();
    void  hit();
    void  missed();
    void  newGame();

private:
    QLCDNumber  *hits;
    QLCDNumber  *shotsLeft;
    CannonField *cannonField;
};

#endif // GAMEBOARD_H

我们现在已经添加了四个槽。这些槽都是被保护的,只在内部使用。我们也已经加入了两个QLCDNumbers(hits和shotsLeft)用来显示游戏的状态。

gamebrd.cpp:

这个文件是新的。它包含最后被用来作为MyWidgetGameBoard类的实现,

我们已经在GameBoard的构造函数中做了一些修改。

    cannonField = new CannonField;

cannonField现在是一个成员变量,所以我们在使用它的时候要小心地改变它的构造函数。

    connect(cannonField, SIGNAL(hit()), this, SLOT(hit()));
    connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));

这次当炮弹射中或者射失目标的时候,我们想做些事情。所以我们把CannonFieldhit()missed()信号连接到这个类的两个被保护的同名槽。

    connect(shoot, SIGNAL(clicked()), SLOT(fire()));

以前我们直接把Shoot按钮的clicked()信号连接到CannonField的shoot()槽。这次我们想跟踪射击的次数,所以我们把它改为连接到这个类里面一个被保护的槽。

注意当你用独立的组件工作的时候,改变程序的行为是多么的容易。

    connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));

我们也使用cannonFieldcanShoot()信号来适当地使Shoot按钮生效和失效。

    QPushButton *restart = new QPushButton("&New Game");
    restart->setFont(QFont("Times", 18, QFont::Bold));
    connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));

我们创建、设置并且连接这个New Game按钮就像我们对其它按钮所做的一样。点击这个按钮就会激活这个窗口部件的newGame()槽。

    hits = new QLCDNumber(2);
    shotsLeft = new QLCDNumber(2);
    QLabel *hitsL = new QLabel("HITS");
    QLabel *shotsLeftL = new QLabel("SHOTS LEFT");

我们创建了四个新的窗口部件。注意我们不怕麻烦的把QLabel窗口部件的指针保留到GameBoard类中是因为我们不想再对它们做什么了。当GameBoard窗口部件被销毁的时候,Qt将会删除它们,并且布局类会适当地重新定义它们的大小。

    QHBoxLayout *topBox = new QHBoxLayout;
    grid->addLayout(topBox, 0, 1);
    topBox->addWidget(shoot);
    topBox->addWidget(hits);
    topBox->addWidget(hitsL);
    topBox->addWidget(shotsLeft);
    topBox->addWidget(shotsLeftL);
    topBox->addStretch(1);
    topBox->addWidget(restart);

右上单元格的窗口部件的数量正在变大。从前它是空的,现在它是完全充足的,我们把它们放到布局中来更好的看到它们。

注意我们让所有的窗口部件获得它们更喜欢的大小,改为在New Game按钮的左边加入了一个可以自由伸展的东西。

    newGame();
}

我们已经做完了所有关于GameBoard的构造,所以我们使用newGame()来开始。(newGame()是一个槽,但是就像我们所说的,槽也可以像普通的函数一样使用。)

void GameBoard::fire()
{
    if (cannonField->gameOver() || cannonField->isShooting())
        return;
    shotsLeft->display(shotsLeft->intValue() - 1);
    cannonField->shoot();
}

这个函数进行射击。如果游戏结束了或者还有炮弹在空中,立即返回。我们减少炮弹的数量并告诉加农炮进行射击。

void GameBoard::hit()
{
    hits->display( hits->intValue() + 1 );
    if ( shotsLeft->intValue() == 0 )
        cannonField->setGameOver();
    else
        cannonField->newTarget();
}

当炮弹击中目标的时候这个槽被激活。我们增加射中的数量。如果没有炮弹了,游戏就结束了。否则,我们会让CannonField生成新的目标。

void GameBoard::missed()
{
    if (shotsLeft->intValue() == 0)
        cannonField->setGameOver();
}

当炮弹射失目标的时候这个槽被激活,如果没有炮弹了,游戏就结束了。

void GameBoard::newGame()
{
    shotsLeft->display( 15 );
    hits->display( 0 );
    cannonField->restartGame();
    cannonField->newTarget();
}

当用户点击Restart按钮的时候这个槽被激活。它也会被构造函数调用。首先它把炮弹的数量设置为15。注意这里是我们在程序中唯一设置炮弹数量的地方。把它改变为你所想要的游戏规则。接下来我们重置射中的数量,重新开始游戏,并且生成一个新的目标。

main.cpp:
这个文件仅仅被删掉了一部分。MyWidget没了,并且唯一剩下的是main()函数,除了名称的改变其它都没有改变。

行为

射中的和剩余炮弹的数量被显示并且程序继续跟踪它们。游戏可以结束了,并且还有一个按钮可以开始一个新游戏。

练习

添加一个随机的风的因素并把它显示给用户看。

当炮弹击中目标的时候做一些飞溅的效果。

实现多个目标。

现在你可以进行第十四章了。


lcdrange.h

#ifndef LCDRANGE_H
#define LCDRANGE_H

#include <QWidget>

class QSlider;
class QLabel;

class LCDRange : public QWidget
{
    Q_OBJECT
public:
    LCDRange(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
    LCDRange(const QString &s, QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());

    int value() const;
    const QString text() const;

public slots:
    void setValue(int);
    void setRange(int minVal, int maxVal);
    void setText(const QString &s);

signals:
    void valueChanged(int);

private:
    void init();

    QSlider *slider;
    QLabel  *label;
};

#endif // LCDRANGE_H

lcdrange.cpp

#include "lcdrange.h"
#include <QVBoxLayout>
#include <QLCDNumber>
#include <QSlider>
#include <QLabel>

LCDRange::LCDRange(QWidget *parent, Qt::WindowFlags name)
    : QWidget(parent, name)
{
    init();
}

LCDRange::LCDRange(const QString &s, QWidget *parent, Qt::WindowFlags name)
    : QWidget(parent, name)
{
    init();
    setText(s);
}

void LCDRange::init()
{
    QLCDNumber *lcd  = new QLCDNumber(2);
    slider = new QSlider(Qt::Horizontal);
    slider->setRange(0, 99);
    slider->setValue(0);

    label = new QLabel;
    label->setAlignment(Qt::AlignHCenter);

    connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
    connect(slider, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)));

    setFocusProxy(slider); //设置这个窗口部件的焦点为slider

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(lcd);
    layout->addWidget(slider);
    layout->addWidget(label);
    setLayout(layout);
}

int LCDRange::value() const
{
    return slider->value();
}

const QString LCDRange::text() const
{
     return label->text();
}

void LCDRange::setValue(int value)
{
    slider->setValue(value);
}

void LCDRange::setRange(int minVal, int maxVal)
{
    if (minVal < 0 || maxVal > 99 || minVal > maxVal)
    {
        qWarning( "LCDRange::setRange(%d,%d)\n"
                 "\tRange must be 0~99\n"
                 "\tand minVal must not be greater than maxVal",
                 minVal, maxVal);
        return;
    }
    slider->setRange(minVal, maxVal);
}

void LCDRange::setText(const QString &s)
{
    label->setText(s);
}

cannon.h

#ifndef CANNON_H
#define CANNON_H

class QTimer;

#include <QWidget>

class CannonField : public QWidget
{
    Q_OBJECT
public:
    CannonField(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());
    int angle() const {return ang;}
    int force() const {return f;}
    bool gameOver() const {return gameEnded;}
    bool isShooting() const;

public slots:
    void setAngle(int degrees);
    void setForce(int newton);
    void shoot();
    void newTarget();
    void setGameOver();
    void restartGame();

private slots:
    void moveShot(); // 更新炮弹的位置

signals:
    void hit();
    void missed();
    void angleChanged(int);
    void forceChanged(int);
    void canShoot(bool);

protected:
    void paintEvent(QPaintEvent *); // 窗口刷新/重绘

private:
    void paintShot(QPainter *);
    void paintTarget(QPainter *);
    void paintCannon(QPainter *);
    QRect cannonRect() const;
    QRect shotRect() const;
    QRect targetRect() const;

    int ang;
    int f;

    int timerCount;
    QTimer *autoShootTimer;
    float shoot_ang;
    float shoot_f;

    QPoint target;
    bool gameEnded;
};

#endif // CANNON_H

cannon.cpp

#include "cannon.h"
#include <QPaintEvent>
#include <QDateTime>
#include <QPainter>
#include <QPixmap>
#include <QTimer>

#include <stdlib.h>
#include <math.h>

CannonField::CannonField(QWidget *parent, Qt::WindowFlags name)
    : QWidget(parent, name)
{
    ang = 45;
    f = 0;
    timerCount = 0;
    autoShootTimer = new QTimer();
    connect(autoShootTimer, SIGNAL(timeout()), this, SLOT(moveShot()));
    shoot_ang = 0;
    shoot_f = 0;
    target = QPoint(0, 0);
    gameEnded = false;
    setAutoFillBackground(true);
    setPalette(QPalette(QColor(250, 250, 200)));
    newTarget();
}

void CannonField::setAngle(int degrees)
{
    if (degrees < 5)
        degrees = 5;
    if (degrees > 70)
        degrees = 70;
    if (ang == degrees)
        return;
    ang = degrees;
    repaint(cannonRect());
    emit angleChanged(ang);
}

void CannonField::setForce(int newton)
{
    if (newton < 0)
        newton = 0;
    if (f == newton)
        return;
    f = newton;
    emit forceChanged(f);
}

void CannonField::shoot()
{
    if (isShooting())
        return;
    timerCount = 0;
    shoot_ang = ang;
    shoot_f = f;
    autoShootTimer->start(50);
    emit canShoot(false);
}

void  CannonField::newTarget()
{
    static bool first_time = true;
    if (first_time)
    {
        first_time = false;
        QTime midnight(0, 0, 0);
        srand(midnight.secsTo(QTime::currentTime()));
    }
    QRegion r(targetRect());
    target = QPoint(200 + rand() % 190, 10 + rand() % 255);
    repaint(r.united(targetRect()));
}

void CannonField::setGameOver()
{
    if (gameEnded)
        return;
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = true;
    repaint();
}

void CannonField::restartGame()
{
    if (isShooting())
        autoShootTimer->stop();
    gameEnded = false;
    repaint();
    emit canShoot(true);
}

void CannonField::moveShot()
{
    QRegion r(shotRect());
    timerCount++;

    QRect shotR = shotRect();

    if (shotR.intersects(targetRect()))
    {
        autoShootTimer->stop();
        emit hit();
    }
    else if (shotR.x() > width() || shotR.y() > height())
    {
        autoShootTimer->stop();
        emit missed();
        emit canShoot(true);
    }
    else
    {
        r = r.united(QRegion(shotR));
    }

    repaint(r);
}

void CannonField::paintEvent(QPaintEvent *e)
{
    QRect updateR = e->rect();
    QPainter p(this);

    if (gameEnded)
    {
        p.setPen(Qt::black);
        p.setFont(QFont("Courier", 48, QFont::Bold ));
        p.drawText(rect(),Qt::AlignHCenter, "Game Over");
    }
    if (updateR.intersects(cannonRect()))
        paintCannon(&p);
    if (autoShootTimer->isActive() && updateR.intersects(shotRect()))
        paintShot(&p);
    if (updateR.intersects(targetRect()))
        paintTarget(&p);
}

void CannonField::paintShot(QPainter *p)
{
    p->setBrush(Qt::black);
    p->setPen(Qt::NoPen);
    p->drawEllipse(shotRect()); // 绘制填充圆
}

void CannonField::paintTarget(QPainter *p)
{
    p->setBrush(Qt::red);
    p->setPen(Qt::NoPen);
    p->drawRect(targetRect());
}

const QRect barrelRect(33, -4, 15, 8);

void CannonField::paintCannon(QPainter *p)
{
    QRect cr = cannonRect();
    QPixmap pix(cr.size());
    pix.fill(Qt::transparent); //使用透明像素图

    QPainter tmp(&pix);
    tmp.setBrush(Qt::blue);
    tmp.setPen(Qt::NoPen);

    tmp.translate(0, pix.height() - 1);
    tmp.drawPie(QRect(-35,-35, 70, 70), 0, 90*16);
    tmp.rotate(-ang);
    tmp.drawRect(barrelRect);
    tmp.end();

    p->drawPixmap(cr.topLeft(), pix);
}

QRect CannonField::cannonRect() const
{
    QRect r(0, 0, 50, 50);
    r.moveBottomLeft(rect().bottomLeft());
    return r;
}

QRect CannonField::shotRect() const
{
    const double gravity = 4;

    double time      = timerCount / 4.0;
    double velocity  = shoot_f;
    double radians   = shoot_ang * 3.14159265 / 180;

    double velx      = velocity * cos(radians);
    double vely      = velocity * sin(radians);
    double x0        = (barrelRect.right() + 5) * cos(radians);
    double y0        = (barrelRect.right() + 5) * sin(radians);
    double x         = x0 + velx * time;
    double y         = y0 + vely * time - 0.5 * gravity * time * time;

    QRect r = QRect(0, 0, 6, 6);
    r.moveCenter(QPoint(qRound(x), height() - 1 - qRound(y)));

    return r;
}

QRect CannonField::targetRect() const
{
    QRect r(0, 0, 20, 10);
    r.moveCenter(QPoint(target.x(),height() - 1 - target.y()));
    return r;
}

bool CannonField::isShooting() const
{
    return autoShootTimer->isActive();
}

gamebrd.h

#ifndef GAMEBOARD_H
#define GAMEBOARD_H

#include <QWidget>
#include "cannon.h"

class LCDRange;
class QLCDNumber;
class CannonField;
class QPushButton;

class GameBoard : public QWidget
{
    Q_OBJECT
public:
    explicit GameBoard(QWidget *parent = 0, Qt::WindowFlags name = Qt::WindowFlags());

protected slots:
    void  fire();
    void  hit();
    void  missed();
    void  newGame();

private:
    QLCDNumber  *hits;
    QLCDNumber  *shotsLeft;
    CannonField *cannonField;
};

#endif // GAMEBOARD_H

gamebrd.cpp

#include "gameboard.h"

#include <QApplication>
#include <Qpushbutton>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QLCDNumber>
#include <QLabel>
#include <Qfont>

#include "lcdrange.h"
#include "cannon.h"

GameBoard::GameBoard(QWidget *parent, Qt::WindowFlags name)
    : QWidget(parent, name)
{
    QPushButton *quit = new QPushButton("&Quit");
    quit->setFont(QFont("Times", 18, QFont::Bold));

    connect(quit, SIGNAL(clicked()), qApp, SLOT(quit()));

    LCDRange *angle  = new LCDRange("ANGLE");
    angle->setRange(5, 70);

    LCDRange *force  = new LCDRange("FORCE");
    force->setRange(10, 50);

    cannonField = new CannonField;

    connect(angle, SIGNAL(valueChanged(int)), cannonField, SLOT(setAngle(int)));
    connect(cannonField, SIGNAL(angleChanged(int)), angle, SLOT(setValue(int)));

    connect(force, SIGNAL(valueChanged(int)), cannonField, SLOT(setForce(int)));
    connect(cannonField, SIGNAL(forceChanged(int)), force, SLOT(setValue(int)));

    connect(cannonField, SIGNAL(hit()), this, SLOT(hit()));
    connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));

    QPushButton *shoot = new QPushButton("&Shoot");
    shoot->setFont(QFont("Times", 18, QFont::Bold));

    connect(shoot, SIGNAL(clicked()), SLOT(fire()));
    connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));

    QPushButton *restart = new QPushButton("&New Game");
    restart->setFont(QFont("Times", 18, QFont::Bold));

    connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));

    hits = new QLCDNumber(2);
    shotsLeft = new QLCDNumber(2);
    QLabel *hitsL = new QLabel("HITS");
    QLabel *shotsLeftL = new QLabel("SHOTS LEFT");

    QGridLayout *grid = new QGridLayout;
    grid->addWidget(quit, 0, 0);
    grid->addWidget(cannonField, 1, 1);
    grid->setColumnStretch(1, 10);

    QVBoxLayout *leftBox = new QVBoxLayout;
    grid->addLayout(leftBox, 1, 0);
    leftBox->addWidget(angle);
    leftBox->addWidget(force);

    QHBoxLayout *topBox = new QHBoxLayout;
    grid->addLayout(topBox, 0, 1);
    topBox->addWidget(shoot);
    topBox->addWidget(hits);
    topBox->addWidget(hitsL);
    topBox->addWidget(shotsLeft);
    topBox->addWidget(shotsLeftL);
    topBox->addStretch(1);
    topBox->addWidget(restart);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addLayout(grid);
    setLayout(layout);

    angle->setValue(60);
    force->setValue(25);
    angle->setFocus();

    newGame();
}

void GameBoard::fire()
{
    if (cannonField->gameOver() || cannonField->isShooting())
        return;
    shotsLeft->display(shotsLeft->intValue() - 1);
    cannonField->shoot();
}

void GameBoard::hit()
{
    hits->display(hits->intValue() + 1);
    if (shotsLeft->intValue() == 0)
        cannonField->setGameOver();
    else
        cannonField->newTarget();
}

void GameBoard::missed()
{
    if (shotsLeft->intValue() == 0)
        cannonField->setGameOver();
}

void GameBoard::newGame()
{
    shotsLeft->display(10);
    hits->display(0);
    cannonField->restartGame();
    cannonField->newTarget();
}


main.cpp

#include <QApplication>
#include "gameboard.h"

int main(int argc, char **argv)
{
    QApplication a(argc, argv);

    GameBoard gb;
    gb.setGeometry(100, 100, 500, 355);
    gb.show();
    return a.exec();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值