Home

Klasy - Adnotacje - Drzewo - Funkcje - Powrót - Struktura

Rozdział 13: Koniec gry

Screenshot of tutorial thirteen

W tym przykładzie zaczniemy tworzyć prawdziwą grę, z punktacją. Damy MyWidget nowe imię (GameBoard) i dodamy kilka slotów.

Definicję umieścimy w gamebrd.h a implementację w gamebrd.cpp.

CannonField zawiera teraz końcowy stan gry.

Problemy z rozmieszczeniem LCDRange zostały naprawione.

Analiza kodu linia po linii

lcdrange.h

    #include <qwidget.h>
    
    class QSlider;
    class QLabel;
    
    class LCDRange : public QWidget
Dziedziczymy raczej po QWidget niż po QVBox: QVBox jest łatwe w użyciu, lecz ma swoje ograniczenia, tak więc przełaczymy się na silniejszy i trochę trudniejszy w użyciu QVBoxLayout.

lcdrange.cpp

    #include <qlayout.h>
Zawieramy qlayout.h now, aby uzyskać API zarządzania rozmieszczeniem.
    LCDRange::LCDRange( QWidget *parent, const char *name )
            : QWidget( parent, name )
Dziedziczymy QWidget w klasyczny sposób.

Drugi konstrukotr uległ tej samej zmianie. init() pozostaje bez zmian, za wyjątkiem dodania kilku linii na dole:

        QVBoxLayout * l = new QVBoxLayout( this );
Tworzymy QVBoxLayout z domyślnymi wartościami, zarządzający dziećmi widgetów.
        l->addWidget( lcd, 1 );
Na górze, dodajemy QLCDNumber, z niezerowym rozciąganiem.
        l->addWidget( slider );
        l->addWidget( label );
Potem dodajemy dwa kolejne, oba z zerowym rozciąganiem.

Rozciąganie jest funkcją oferowaną przez QVBoxLayout (oraz QHBoxLayout i QGridLayout), a bezklasowe QVBox nie. W tym przypadku chcemy by QLCDNumber mógł się rozciągać a inne nie.

cannon.h

CannonField posiada teraz stan końca gry i kilka nowych rzeczy.
        bool  gameOver() const { return gameEnded; }
Funkcja zwraca TRUE jeżeli gra dobiegnie końca, lub FALSE jeżeli ciągle trwa.
        void  setGameOver();
        void  restartGame();
Mamy tu dwa nowe sloty; setGameOver() i restartGame().
        void  canShoot( bool );
Nowy sygnał wskazuje, że CannonField jest w stanie, w którym slot shoot() ma sens. Użyjemy tego poniżej do właczenia/wyłączenia przycisku Shoot.
        bool gameEnded;
Ta prywatna zmienna zawiera stan gry. TRUE oznacza koniec gry, a FALSE znaczy, że gra się ciągle toczy.

cannon.cpp

        gameEnded = FALSE;
Ta linia została dodana do konstruktora. Na początku gra się nie kończy (szcześliwe rozwiązanie dla gracza :-).
    void CannonField::shoot()
    {
        if ( isShooting() )
            return;
        timerCount = 0;
        shoot_ang = ang;
        shoot_f = f;
        autoShootTimer->start( 50 );
        emit canShoot( FALSE );
    }
Dodaliśmy nową funkcję isShooting(), tak ze  shoot() używa jej miast sprawdzać bezporednio. Przy okazji shoot mówi światu zewnętrznemu, że CannonField może strzelać.
    void CannonField::setGameOver()
    {
        if ( gameEnded )
            return;
        if ( isShooting() )
            autoShootTimer->stop();
        gameEnded = TRUE;
        repaint();
    }
Ten slot kończy grę. Musi być wywoływany spoza CannonField, ponieważ ten widget nie wie kiedy skończyć grę. Jest to ważny szczegół projektowania w koncepcji programowania komponentowego. Chcemy, by komponent był maksymalnie elastyczny, oraz by działał z różnymi zasadami. Przykładowo wersja tej gry dla wielu graczy, która ma zasadę, że kto pierwszy trafi 10 razy wygrywa, użyłaby CannonField w niezmienionej formie.

Jeżeli gra już się skończyła, wracamy natychmiast. Jeśli gra trwa,zatrzymujemy strzał, ustawiamy flage końca gry i przerysowujemy cały widget.

    void CannonField::restartGame()
    {
        if ( isShooting() )
            autoShootTimer->stop();
        gameEnded = FALSE;
        repaint();
        emit canShoot( TRUE );
    }
Ten slot uruchamia grę na nowo. Jeśli pocisk jest w powietrzu, zatrzymujemy strzelanie. Resetujemu następnie zmienną gameEnded I przerysowujemy cały widget.

moveShot() także emituje nowy sygnał canShoot(TRUE) w tym samy czasie co hit() lub miss().

Modyfikacje w 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" );
        }
Wydarzenie rysowania zostało rozszerzone o wyświetlanie tekstu "Game Over" jeżeli gra się skończyła, tzn. gameEnded jest TRUE.

Aby napisać tekst, ustawiamy czarne pióro. Następnie wybieramy 48 stopniową czcionkę z rodziny Courier. W końcu piszemy tekst na środku widgetu. Niestety na niektórych systemach (szczególnie serwerach X z czcionkami Unicode), może upłynąć pewien czas, zamim taka czionka załaduje się. Lecz Qt keszuje czcionki, więc będzie to zauważalne tylko podczas pierwszego uruchomienia gry.

        if ( updateR.intersects( cannonRect() ) )
            paintCannon( &p );
        if ( isShooting() && updateR.intersects( shotRect() ) )
            paintShot( &p );
        if ( !gameEnded && updateR.intersects( targetRect() ) )
            paintTarget( &p );
    }
Rysujemy pocisk jedynie kiedy strzelamy a cel tylko gdy gramy.

gamebrd.h

To jest nowe. Zawiera definicję klasy GameBoard, która ostatnio występowała jako MyWidget.
    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;
    };
Dodaliśmy teraz cztery nowe sloty. Są chronione i używane wewnętrznie. Dodaliśmy także dwa QLCDNumbers: hits oraz shotsLeft, które wyświetlają stan gry.

gamebrd.cpp

Ten plik jest nowy. zawiera implementację klasy GameBoard ostatnio jako MyWidget.

Dokonaliźmy pewnych zmian w konstruktorze GameBoard.

        cannonField = new CannonField( this, "cannonField" );
cannonField jest teraz zmienną czonkową, Tak więc delikatnie zmieniamy konstruktor pod jej użycie.
        connect( cannonField, SIGNAL(hit()),
                 this, SLOT(hit()) );
        connect( cannonField, SIGNAL(missed()),
                 this, SLOT(missed()) );
Tym razem chcemy coś zrobić gdy pocisk trafia lub mija cel. Tak więc łączymy sygnały hit() i missed() z CannonField do dwóch chronionych slotów o tej samej nazwie lecz znajdujących się w tej klasie.
        connect( shoot, SIGNAL(clicked()), SLOT(fire()) );
Popzrednio łączyliśmy sygnał pocisku clicked() bezpośrednio ze slotem z CannonField - shoot(). Tym razem chcemy wiedzieć ile strzałów zostało oddanych, więc miast tego łączymy go z chronionym slotem w tej klasie.

Zauważ jak łatwo zmienić zachowanie programu kiedy pracuje się z samo-wyjaśniającymi się komponentami.

        connect( cannonField, SIGNAL(canShoot(bool)),
                 shoot, SLOT(setEnabled(bool)) );
Użyjemy także sygnału z CannonField's - canShoot() aby odpowiednio włączyc/wyłączyć przycisk Shoot.
        QPushButton *restart 
            = new QPushButton( "&New Game", this, "newgame" );
        restart->setFont( QFont( "Times", 18, QFont::Bold ) );
    
        connect( restart, SIGNAL(clicked()), this, SLOT(newGame()) );
Tworzymy, ustawiamy i łaczymy przycisk New Game tak jak to robiliśmy z innymi przyciskami. Wciśnięcie go uruchomi slot widgetu 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" );
Tworzymy cztery nowe widgety. Zauważ, że nie przejmujemy się trzymaniem wskaźników do widgetów QLabel widgets w klasie GameBoard ponieważ nie bardzo jest co z nimi tu robić. Qt skasu je gdy widget GameBoard zostanie zniszczony, a klasy zarządzające rozmieszczeniem odpowiednio je rozszerzą.
        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 );
Liczba widgetów w górnej komórce rośnie. Kiedyś była pusta, teraz jest wystarczająco zapełniona by zgrupować ją razem z ustawieiami rozmieszczenia dla lepszego wyglądu.

Zauważ, iż pozwalami widgetom mieć ich preferowane rozmiary, zamiast umieszczać rozszerzanie na lewo od przycisku New Game.

        newGame();
    }
Skończyliśmy konstruowanie GameBoard, więc uruchomimy ją przez newGame(). (newGame() jest slotem, ale jak powiedzieliśmy, sloty moga być także używane jako zwykłe funkcje.)
    void GameBoard::fire()
    {
        if ( cannonField->gameOver() || cannonField->isShooting() )
            return;
        shotsLeft->display( shotsLeft->intValue() - 1 );
        cannonField->shoot();
    }
Ta funkcja wystrzeliwuje pocisk. Jeśli gra się kończy lub nie ma pocisku w powietrzu, wracamy natychmiast. Zmniejszamy liczbę pozostałych pocisków i mówimy działu, że może strzelać.
    void GameBoard::hit()
    {
        hits->display( hits->intValue() + 1 );
        if ( shotsLeft->intValue() == 0 )
            cannonField->setGameOver();
        else
            cannonField->newTarget();
    }
Ten slot jest uruchamiany gdy pocisk trafi w cel. Zwiększamy liczbe trafień. Jeśli nie ma już pocisków, gra się kończy. W przeciwnym przypadku, CannonField stworzy nowy cel.
    void GameBoard::missed()
    {
        if ( shotsLeft->intValue() == 0 )
            cannonField->setGameOver();
    }
Ten slot jest uruchamiany gdy pocisk nie trafi w cel. Jeśli nie ma już pocisków, gra się kończy.
    void GameBoard::newGame()
    {
        shotsLeft->display( 15 );
        hits->display( 0 );
        cannonField->restartGame();
        cannonField->newTarget();
    }
Ten slot jest uruchamiany gdy użytkownik wciśnie przycisk restart. Jest także wywoływany z konstruktora. Najpierw, ustawia liczbę pocisków na 15. Zauważ, że to jedyne miejsce w programie, gdzie ustawiamy liczbe strzałów. Zmień go jeżeli chcesz zmienić reguły gry. Następnie, resetujemy liczbę strzałów, uruchamiamy ponownie grę i tworzymy nowy cel.

main.cpp

Ten plik był tylko atrapą. MyWidget zniknął i została tylko funkcja main() function, niezmieniona oprócz nazwy.

Zachowanie

Strzały i trafienia są pokazywane a program dba o nie. Gra może się skończyć i jest też przycisk Nowej gry.

Ćwiczenia

Dodaj współczynnik wiatru i pokaż go użytkownikowi.

Dodaj jakieś efekty zniszczenia celu.

Dodaj wiele celów.

Możesz teraz przejść do rozdziału rozdziału czternastego.

[Poprzedni tutorial] [Następny tutorial] [Główna strona tutoriala]



 
Copyright (c) 2000 Troll Tech Znaki towarowe
Wersja Qt 2.1.0