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.