Qt Tutorial - Chapter 13

# Chapter 13: Game Over

In this example we start to approach a real playable game. We give MyWidget a new name; GameBoard and add some slots.

We put the definition in gamebrd.h and the implementation in gamebrd.cpp.

The CannonField now has a game over state.

- lcdrange.h contains the LCDRange class definition
- lcdrange.cpp contains the LCDRange implementation
- cannon.h contains the CannonField class definition
- cannon.cpp contains the CannonField implementation
- gamebrd.h contains the GameBoard class definition
- gamebrd.cpp contains the GameBoard implementation
- main.cpp contains MyWidget and main.
- Makefile contains some rules for generating the meta object information necessary for signal/slot creation.

## Line by Line Walk-Through

### cannon.h

The CannonField now has a game over state and a few new functions.

            bool  gameOver()   const { return gameEnded; }

This function returns TRUE if the game is over, or FALSE if a game is going on.

            void  setGameOver();
            void  restartGame();

Here are two new slots; setGameOver() and restartGame().

            bool gameEnded;

This private variable contains the game state. TRUE means that the game is over, and FALSE means that a game is going on.

### cannon.cpp

        #include <qfont.h>

We include the QFont class definition.

            gameEnded     = FALSE;

This line has been added to the constructor. Initially, the game is not over.

        void CannonField::setGameOver()
        {
            if ( gameEnded )
                return;
            if ( shooting )
                stopShooting();
            gameEnded = TRUE;
            repaint();
        }

This slot ends the game. It must be called from outside CannonField, because this widget does not know when to end the game. This is is an important design principle in component programming. We choose to make the component as flexible as possible to make it usable for different games with different rules.

If the game has already been ended, we return immediately. If a game is going on, we stop the shot, set the game over flag and repaint the entire widget.

        void CannonField::restartGame()
        {
            if ( shooting )
                stopShooting();
            gameEnded = FALSE;
            repaint();
        }

This slot starts a new game. If a shot is in the air, we stop shooting. We then reset the `gameEnded` variable and repaint the widget.

Modifications in CannonField::paintEvent():

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

The paint event has been enhanced to display the text "Game Over" if the game is over, i.e. `gameEnded` is TRUE. We don't bother to check the update rectangle here, because speed is not critical when the game is over.

To draw the text, we first set a black pen. The pen color is used when drawing text. Next, we choose a 48 point bold font from the Courier family. Finally, we draw the text centered in the widget's rectangle. Unfortunately, on some systems (especially some X servers) it can take a while to load such a large font. Since Qt caches fonts, you will only notice this the first time the font is used.

                if ( shooting &&  updateR.intersects( shotRect() ) )
                    paintShot( &p );
                if ( updateR.intersects( targetRect() ) )
                    paintTarget( &p );
            }

We only draw the shot or the target when the game is not over.

### gamebrd.h

This file is new. It contains the definition of the GameBoard class, which was last seen as MyWidget.

        class GameBoard : public QWidget
        {
            Q_OBJECT
        public:
            GameBoard( QWidget *parent=0, const char *name=0 );
        protected:
            void  resizeEvent( QResizeEvent * );
        protected slots:
            void  fire();
            void  hit();
            void  missed();
            void  newGame();
        private:
            QPushButton *quit;
            QPushButton *shoot;
            QPushButton *restart;
            LCDRange    *angle;
            LCDRange    *force;
            QLCDNumber  *hits;
            QLCDNumber  *shotsLeft;
            CannonField *cannonField;
        };

We have now added four slots. These are protected and are used internally. We have also added two QLCDNumbers: `hits` and `shotsLeft,` which display the game status. The `restart` button is also new.

### gamebrd.cpp

This file is new. It contains the definition of the GameBoard class, which was last seen as MyWidget.

We have made some changes in the GameBoard constructor.

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

This time we want to do something when the shot has hit or missed the target. Thus we connect the hit() and missed() signals of the CannonField to two protected slots with the same names in this class.

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

Previously, we connected the shoot button's clicked() signal directly to the CannonField's shoot() slot. This time we want to keep track of the number of shots fired, so we connect it to a protected slot in this class instead.

Notice how easy it is to change the behavior of a program when you are working with self-contained components.

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

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

We create, set up and connect the restart button like we have done with the other buttons. Clicking this button will activate the newGame() slot in this widget.

            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" );

We create four new widgets. Note that we don't bother to keep the pointers to the QLabel widgets in the GameBoard class since they live their own life undisturbed. As mentioned before, Qt will delete them when the GameBoard widget is destroyed.

            quit->setGeometry( 10, 10, 75, 30 );
            angle->setGeometry( 10, quit->y() + quit->height() + 10, 75, 130 );
            force->setGeometry( 10, angle->y() + angle->height() + 10, 75, 130 );
            cannonField->move( angle->x() + angle->width() + 10, angle->y() );
            shoot->setGeometry( 10, 315, 75, 30 );
            restart->setGeometry( 380, 10, 110, 30 );
            hits->setGeometry( 130, 10, 40, 30 );
            hitsL->setGeometry( hits->x() + hits->width() + 5, 10, 60, 30 );
            shotsLeft->setGeometry( 240, 10, 40, 30 );
            shotsLeftL->setGeometry( shotsLeft->x()+shotsLeft->width()+5, 10, 60, 30 );

Since we have a large number of widgets, we want to put the geometry setting in one place. This will make it more convenient for us to later change the geometry.

            newGame();

To start the show, we call the newGame() slot.

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

This function fires a shot. If the game is over or there is a shot in the air, we return immediately. We decrement number of shots left and tell the cannon to shoot.

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

This slot is activated when a shot has hit the target. We increment the number of hits. If there are no shots left, the game is over. Otherwise, we make the CannonField generate a new target.

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

This slot is activated when a shot has missed the target. If there are no shots left, the game is over.

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

This slot is activated when the user clicks the restart button. It is also called from the constructor. First, it sets the number of shots to 15. Note that this is the only place in the program that we set the number of shots. Change it to whatever you like to change the game rules. Next, we reset the number of hits, restart the game and generate a new target.

### main.cpp

This file has just been on a diet. MyWidget is gone and the only thing left is the main() function.

## Behavior

Hits and shots left are displayed and the program keeps track of them. The game can end and there's a button to start a new game.

## Excercises

Add a random wind factor and show it to the user.

Make some splatter effects when the shot hits the target.

Implement multiple targets.

You may now go on to chapter fourteen.

[Previous tutorial] [Next tutorial] [Main tutorial page]