
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
|