Sub-Attaq | Qt State Machine 6.4.3

- Qt 6.4
- Qt State Machine
- Qt State Machine Examples
- Sub-Attaq
- Qt 6.4.3 Reference Documentation

# Sub-Attaq

This example shows Qt's ability to combine the animation framework and the state machine framework to create a game.

The purpose of the game is to destroy all submarines to win the current level. The boat can be controlled using left and right keys. To fire a bomb you can press the up and down keys.

#### The `main()` Function

     int main(int argc, char *argv[])
     {
         QApplication app(argc, argv);
         Q_INIT_RESOURCE(subattaq);

         MainWindow w;
         w.show();

         return app.exec();
     }

The MainWindow instance is created and shown.

#### The `MainWindow` Class

         QMenu *file = menuBar()->addMenu(tr("&File"));

         QAction *newAction = file->addAction(tr("New Game"));
         newAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N));
         QAction *quitAction = file->addAction(tr("Quit"));
         quitAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q));

         if (QApplication::arguments().contains("-fullscreen")) {
             scene = new GraphicsScene(0, 0, 750, 400, GraphicsScene::Small, this);
             setWindowState(Qt::WindowFullScreen);
         } else {
             scene = new GraphicsScene(0, 0, 880, 630, GraphicsScene::Big, this);
             layout()->setSizeConstraint(QLayout::SetFixedSize);
         }

         view = new QGraphicsView(scene, this);
         view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
         scene->setupScene(newAction, quitAction);
         setCentralWidget(view);

MainWindow extends QMainWindow and contains the GraphicsScene instance. It creates and sets up the menu bar as well.

#### The `GraphicsScene` Class

There are several state machines in the application. The `GraphicsScene` state machine handles states related to events outside the actual game scene like the letter animation in the beginning.

##### The `GraphicsScene` Constructor

         PixmapItem *backgroundItem = new PixmapItem(QStringLiteral("background"), mode);
         backgroundItem->setZValue(1);
         backgroundItem->setPos(0,0);
         addItem(backgroundItem);

         PixmapItem *surfaceItem = new PixmapItem(QStringLiteral("surface"), mode);
         surfaceItem->setZValue(3);
         surfaceItem->setPos(0, sealLevel() - surfaceItem->boundingRect().height() / 2);
         addItem(surfaceItem);

         //The item that displays score and level
         progressItem = new ProgressItem(backgroundItem);

         textInformationItem = new TextInformationItem(backgroundItem);

         textInformationItem->setMessage(QString("Select new game from the menu or press Ctrl+N to start!<br/>Press left or right to move the ship and up to drop bombs."), false);
         textInformationItem->setPos(backgroundItem->boundingRect().center().x() - textInformationItem->boundingRect().size().width() / 2,
                                     backgroundItem->boundingRect().height() * 3 / 4);

         //We create the boat
         addItem(boat);
         boat->setPos(this->width()/2, sealLevel() - boat->size().height());
         boat->hide();

The `GraphicsScene` class contains the background images and the score and level information texts.

##### `The` setupScene Method

         QStateMachine *machine = new QStateMachine(this);

         //This state is when the player is playing
         PlayState *gameState = new PlayState(this, machine);

         //Final state
         QFinalState *finalState = new QFinalState(machine);

         //Animation when the player enters the game
         QAnimationState *lettersMovingState = new QAnimationState(machine);
         lettersMovingState->setAnimation(lettersGroupMoving);

         //Animation when the welcome screen disappears
         QAnimationState *lettersFadingState = new QAnimationState(machine);
         lettersFadingState->setAnimation(lettersGroupFading);

         //if it is a new game then we fade out the welcome screen and start playing
         lettersMovingState->addTransition(newAction, &QAction::triggered, lettersFadingState);
         lettersFadingState->addTransition(lettersFadingState, &QAnimationState::animationFinished, gameState);

         //New Game is triggered then player starts playing
         gameState->addTransition(newAction, &QAction::triggered, gameState);

         //Wanna quit, then connect to CTRL+Q
         gameState->addTransition(quitAction, &QAction::triggered, finalState);
         lettersMovingState->addTransition(quitAction, &QAction::triggered, finalState);

         //Welcome screen is the initial state
         machine->setInitialState(lettersMovingState);

         machine->start();

         //We reach the final state, then we quit
         connect(machine, &QStateMachine::finished, qApp, &QApplication::quit);

The four state machine states are created with sequential transitions from one to the next. The `gameState` also has a transition that is triggered by `newAction`, the new game menu item, or its shortcut key at any point in the application. The `gameState` state is an instance of the `PlayState` class.

#### The `PlayState` Class

The `PlayState` class is a QState derived class that handles the state when the game is in progress.

         machine = new QStateMachine;

         //This state is active when the player is playing
         LevelState *levelState = new LevelState(scene, this, machine);

         //This state is active when the player is actually playing but the game is not paused
         QState *playingState = new QState(levelState);
         levelState->setInitialState(playingState);

         //This state is active when the game is paused
         PauseState *pauseState = new PauseState(scene, levelState);

         //We have one view, it receives the key press events
         QKeyEventTransition *pressPplay = new QKeyEventTransition(scene->views().at(0), QEvent::KeyPress, Qt::Key_P);
         pressPplay->setTargetState(pauseState);
         QKeyEventTransition *pressPpause = new QKeyEventTransition(scene->views().at(0), QEvent::KeyPress, Qt::Key_P);
         pressPpause->setTargetState(playingState);

         //Pause "P" is triggered, when the player pauses the game
         playingState->addTransition(pressPplay);

         //To get back to playing when the game has been paused
         pauseState->addTransition(pressPpause);

         //This state is active when player has lost
         LostState *lostState = new LostState(scene, this, machine);

         //This state is active when player has won
         WinState *winState = new WinState(scene, this, machine);

         //If boat has been destroyed then the game is finished
         levelState->addTransition(scene->boat, &Boat::boatExecutionFinished,lostState);

         //This transition checks if we have won or not
         WinTransition *winTransition = new WinTransition(scene, this, winState);

         //If boat has been destroyed then the game is finished
         levelState->addTransition(winTransition);

         //This state is for an animation when the score changes
         UpdateScoreState *scoreState = new UpdateScoreState(levelState);

         //This transition updates the score when a submarine is destroyed
         UpdateScoreTransition *scoreTransition = new UpdateScoreTransition(scene, this, levelState);
         scoreTransition->setTargetState(scoreState);

         //If the boat has been destroyed then the game is finished
         playingState->addTransition(scoreTransition);

         //We go back to play state
         scoreState->addTransition(playingState);

         //We start playing!!!
         machine->setInitialState(levelState);

         //Final state
         QFinalState *finalState = new QFinalState(machine);

         //This transition is triggered when the player presses space after completing a level
         CustomSpaceTransition *spaceTransition = new CustomSpaceTransition(scene->views().at(0), this, QEvent::KeyPress, Qt::Key_Space);
         spaceTransition->setTargetState(levelState);
         winState->addTransition(spaceTransition);

         //We lost so we should reach the final state
         lostState->addTransition(lostState, &QState::finished, finalState);

         scene->textInformationItem->hide();
         machine->start();

The `PlayState` state machine handles higher level game logic like pausing the game and updating the score.

The `playingState` state is a QState instance that is active while the user is actively playing the game. The `pauseState` is set up with transitions to and from `playingState`, which are triggered by pressing the p key. The `lostState` is created with a transition to it, which is triggered when the boat is destroyed. The `winState` is also created here with a transition to and from the `levelState`.

#### The `LevelState` Class

     void LevelState::initializeLevel()
     {
         //we re-init the boat
         scene->boat->setPos(scene->width()/2, scene->sealLevel() - scene->boat->size().height());
         scene->boat->setCurrentSpeed(0);
         scene->boat->setCurrentDirection(Boat::None);
         scene->boat->setBombsLaunched(0);
         scene->boat->show();
         scene->setFocusItem(scene->boat, Qt::OtherFocusReason);
         scene->boat->run();

         scene->progressItem->setScore(game->score);
         scene->progressItem->setLevel(game->currentLevel + 1);

         const GraphicsScene::LevelDescription currentLevelDescription = scene->levelsData.value(game->currentLevel);
         for (const QPair<int,int> &subContent : currentLevelDescription.submarines) {

             GraphicsScene::SubmarineDescription submarineDesc = scene->submarinesData.at(subContent.first);

             for (int j = 0; j < subContent.second; ++j ) {
                 SubMarine *sub = new SubMarine(submarineDesc.type, submarineDesc.name, submarineDesc.points);
                 scene->addItem(sub);
                 int random = QRandomGenerator::global()->bounded(15) + 1;
                 qreal x = random == 13 || random == 5 ? 0 : scene->width() - sub->size().width();
                 qreal y = scene->height() -(QRandomGenerator::global()->bounded(150) + 1) - sub->size().height();
                 sub->setPos(x,y);
                 sub->setCurrentDirection(x == 0 ? SubMarine::Right : SubMarine::Left);
                 sub->setCurrentSpeed(QRandomGenerator::global()->bounded(3) + 1);
             }
         }
     }

The components of the scene are initialized based on what level the player has reached.

Example project @ code.qt.io