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.
#include <qwidget.h> class QSlider; class QLabel; class LCDRange : public QWidgetDziedziczymy 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.
#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.
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.
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.
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.
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.
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
|