Qt教程一 —— 第十三章:游戏结束

Qt 3.0.5

主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数

Qt教程一 —— 第十三章:游戏结束

Screenshot of tutorial thirteen

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

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

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

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

一行一行地解说

t13/lcdrange.h

    #include <qwidget.h>

    class QSlider;
    class QLabel;

    class LCDRange : public QWidget

我们继承了QWidget而不是QVBox。QVBox是非常容易使用的,但是它也显示了它的局域性,所以我们选择使用更加强大和稍微有一些难的QVBoxLayout。(和你记忆中的一样,QVBoxLayout不是一个窗口部件,它管理窗口部件。)

t13/lcdrange.cpp

    #include <qlayout.h>

我们现在需要包含qlayout.h来获得其它布局管理API。

    LCDRange::LCDRange( QWidget *parent, const char *name )
            : QWidget( parent, name )

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

另外一个构造函数作了同样的改动。init()没有变化,除了我们在最后加了几行:

        QVBoxLayout * l = new QVBoxLayout( this );

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

        l->addWidget( lcd, 1 );

At the top we add the QLCDNumber with a non-zero stretch.

        l->addWidget( slider );
        l->addWidget( label );

然后我们添加另外两个,它们都使用默认的零伸展因数。

这个伸展控制是QVBoxLayout(和QHBoxLayout,和QGridLayout)所提供的,而像QVBox这样的类却不提供。在这种情况下我们让QLCDNumber可以伸展,而其它的不可以。

t13/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说明游戏还将继续。

t13/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( black );
            p.setFont( QFont( "Courier", 48, QFont::Bold ) );
            p.drawText( rect(), AlignCenter, "Game Over" );
        }

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

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

        if ( updateR.intersects( cannonRect() ) )
            paintCannon( &p );
        if ( isShooting() && updateR.intersects( shotRect() ) )
            paintShot( &p );
        if ( !gameEnded && updateR.intersects( targetRect() ) )
            paintTarget( &p );
    }

我们只有在设计的时候画炮弹,在玩游戏的时候画目标(这也就是说,当游戏没有结束的时候)。

t13/gamebrd.h

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

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

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

    class GameBoard : public QWidget
    {
        Q_OBJECT
    public:
        GameBoard( QWidget *parent=0, const char *name=0 );

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

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

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

t13/gamebrd.cpp

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

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

        cannonField = new CannonField( this, "cannonField" );

cannonField现在是一个成员变量,所以我们在使用它的时候要小心地改变它的构造函数。(Trolltech的程序员从来不会忘记这点,但是我就忘了。告诫程序员-如果“programmor”是拉丁语,至少。无论如何,返回代码。)

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

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

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

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

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

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

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

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

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

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

        hits = new QLCDNumber( 2, this, "hits" );
        shotsLeft = new QLCDNumber( 2, this, "shotsleft" );
        QLabel *hitsL = new QLabel( "HITS", this, "hitsLabel" );
        QLabel *shotsLeftL
            = new QLabel( "SHOTS LEFT", this, "shotsleftLabel" );

我们创建了四个新的窗口部件。注意我们不怕麻烦的把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。注意这里是我们在程序中唯一设置炮弹数量的地方。把它改变为你所想要的游戏规则。接下来我们重置射中的数量,重新开始游戏,并且生成一个新的目标。

t13/main.cpp

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

行为

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

(请看编译来学习如何创建一个makefile和连编应用程序。)

练习

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

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

实现多个目标。

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

[上一章] [下一章] [教程一主页]


Copyright © 2002 Trolltech Trademarks 译者:Cavendish
Qt 3.0.5版