[Предыдущий: Урок 12] [Учебное пособие] [Следующий: Урок 14] Урок 13 - Конец игры
Файлы:
В этом примере мы начнем приближаться к получению реальной, подходящей для использования по назначению, игры с подсчетом очков. Мы дали MyWidget новое имя (GameBoard) и добавили несколько слотов. Мы поместили определение в gameboard.h, а реализацию - в gameboard.cpp. CannonField теперь содержит состояние "конец игры". Исправлены проблемы с размещением в LCDRange. Разберем программу строка за строкойt13/lcdrange.cpplabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); Мы установили политику размера (size policy) QLabel равной (Preferred, Fixed). Вертикальный компонент гарантирует, что метка не будет растягиваться или сжиматься по вертикали; ее оптимальные размеры сохранятся (ее sizeHint()). Это решит проблемы с размещением, наблюдаемые в уроке 12. t13/cannonfield.hCannonField теперь содержит состояние "конец игры" и несколько новых функций. bool gameOver() const { return gameEnded; } Эта функция возвращает true, если игра завершена, и false - если игра продолжается. void setGameOver(); void restartGame(); Два новых слота: setGameOver() и restartGame(). void canShoot(bool can); Этот новый сигнал означает, что CannonField находится в состоянии, где слот shoot() имеет смысл. Мы используем его ниже для включения или отключения кнопки Shoot. bool gameEnded; В этой закрытой переменной содержится состояние игры; true означает конец игры, а false - игра продолжается. t13/cannonfield.cppgameEnded = false; Эта строчка была добавлена в конструктор. Сначала игра не завершена (к счастью для игрока :-). void CannonField::shoot() { if (isShooting()) return; timerCount = 0; shootAngle = currentAngle; shootForce = currentForce; autoShootTimer->start(5); emit canShoot(false); } Мы добавили новую функцию isShooting(), так что shoot() использует ее вместо непосредственной проверки. Также снаряд сообщает всем остальным виджетам, что CannonField не может сейчас стрелять. void CannonField::setGameOver() { if (gameEnded) return; if (isShooting()) autoShootTimer->stop(); gameEnded = true; update(); } Этот слот завершает игру. Он должен вызываться извне CannonField, поскольку виджет не знает когда завершать игру. Это является важным принципом проектирования компонентного программирования. Мы предпочли сделать компонент настолько гибким насколько это возможно для того, чтобы сделать его более удобным при использовании с различными правилами (например, в многопользовательской версии игры, в которой выигрывает игрок, первым попавший в цель десять раз, CannonField можно использовать без изменений). Если игра завершена, мы немедленно возвращаемся. Если игра продолжается, то мы останавливаем снаряд, устанавливаем флаг "конец игры" и перерисовываем весь виджет. void CannonField::restartGame() { if (isShooting()) autoShootTimer->stop(); gameEnded = false; update(); emit canShoot(true); } Этот слот начинает новую игру. Если снаряд находится в полете, то мы прекращаем стрельбу. Затем мы сбрасываем значение переменной gameEnded и перерисовываем виджет. moveShot() также испускает новый сигнал canShoot(true) одновременно с hit() или miss(). Модификации в CannonField::paintEvent(): void CannonField::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
if (gameEnded) {
painter.setPen(Qt::black);
painter.setFont(QFont("Courier", 48, QFont::Bold));
painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"));
}
Событие рисования было расширено с тем, чтобы если игра завершена вывести на экран текст "Game Over", т.е. когда gameEnded равно true. Мы не беспокоимся здесь о проверке прямоугольника обновления, поскольку при завершении игры скорость не важна. Для рисования текста мы сначала установим черное перо; цвет пера используется при рисовании текста. Далее мы выбираем полужирный шрифт Courier размером в 48 пунктов. В заключение мы рисуем текст по центру прямоугольника виджета. К сожалению, на некоторых системах (особенно на X-серверах со шрифтами Unicode) может потребоваться время на загрузку достаточного большого шрифта. Поскольку Qt кэширует шрифты, вы будете уведомлены об этом только во время первого использования шрифта. paintCannon(painter); if (isShooting()) paintShot(painter); if (!gameEnded) paintTarget(painter); } Мы рисуем снаряд только во время стрельбы, а цель - только во время игры (т.е., пока игра не завершена). t13/gameboard.hЭто новый файл. Он содержит определение класса GameBoard, ранее известного как MyWidget. class QLCDNumber; class CannonField; class GameBoard : public QWidget { Q_OBJECT public: GameBoard(QWidget *parent = 0); protected slots: void fire(); void hit(); void missed(); void newGame(); private: QLCDNumber *hits; QLCDNumber *shotsLeft; CannonField *cannonField; }; Мы добавили четыре слота. Они являются защищенными и предназначены для внутреннего использования. Мы также добавили два объекта QLCDNumber (hits и shotsLeft), отображающих состояние игры. t13/gameboard.cppЭто новый файл. Он содержит реализацию класса GameBoard, ранее известного как MyWidget. Мы внесли несколько изменений в конструктор GameBoard. cannonField = new CannonField; Теперь cannonField является переменном-членом, поэтому чтобы ее использовать мы осторожно внесли изменения в конструктор. connect(cannonField, SIGNAL(hit()), this, SLOT(hit())); connect(cannonField, SIGNAL(missed()), this, SLOT(missed())); А сейчас мы хотим сделать что-нибудь, когда снаряд попадет в цель или промахнется мимо нее. Итак, мы соединим сигналы hit() и missed() виджета CannonField с двумя защищенными слотами с такими же именами в данном классе. connect(shoot, SIGNAL(clicked()), this, SLOT(fire())); Ранее мы соединяли сигнал кнопки Shoot - clicked() - непосредственно со слотом CannonField'а - shoot(). Сейчас же мы хотим отслеживать количество выстрелов, поэтому вместо этого мы соединим его с защищенным слотом в этом же классе. Обращаем ваше внимание на то, как легко изменить поведение программы при работе с модульными (self-contained) компонентами. connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool))); Мы также используем сигнал cannonField'а - canShoot() - чтобы разрешить или запретить кнопку Shoot, соответственно. QPushButton *restart = new QPushButton(tr("&New Game")); restart->setFont(QFont("Times", 18, QFont::Bold)); connect(restart, SIGNAL(clicked()), this, SLOT(newGame())); Мы создали, настроили и соединили кнопку New Game аналогично тому, как поступили с другими кнопками. Щелчок по этой кнопке будет активировать слот newGame() данного виджета. hits = new QLCDNumber(2); hits->setSegmentStyle(QLCDNumber::Filled); shotsLeft = new QLCDNumber(2); shotsLeft->setSegmentStyle(QLCDNumber::Filled); QLabel *hitsLabel = new QLabel(tr("HITS")); QLabel *shotsLeftLabel = new QLabel(tr("SHOTS LEFT")); Мы создали четыре новых виджета. Обратите внимание на то, что мы не беспокоимся о сохранности указателей на виджеты QLabel в классе GameBoard, поскольку ничего не планируем с ними делать. Qt удалит их когда будет разрушен виджет GameBoard, а классы компоновки соответственно автоматически изменят из размеры. QHBoxLayout *topLayout = new QHBoxLayout; topLayout->addWidget(shoot); topLayout->addWidget(hits); topLayout->addWidget(hitsLabel); topLayout->addWidget(shotsLeft); topLayout->addWidget(shotsLeftLabel); topLayout->addStretch(1); topLayout->addWidget(restart); Верхняя правая ячейка сетки QGridLayout начинает переполняться. Мы поместили растяжение (stretch) только слева от кнопки New Game чтобы гарантировать, что эта кнопка всегда будет появляться в правой части окна. newGame(); Мы завершили создание GameBoard, теперь мы запускаем все используя newGame(). Хотя newGame() - это слот, он также может быть использован как обычная функция. void GameBoard::fire() { if (cannonField->gameOver() || cannonField->isShooting()) return; shotsLeft->display(shotsLeft->intValue() - 1); cannonField->shoot(); } Эта функция выстреливает снаряд. Если игра завершилась или снаряд еще в воздухе, мы немедленно вернемся. Мы уменьшаем количество оставшихся выстрелов и сообщаем пушке о необходимости совершить выстрел. void GameBoard::hit() { hits->display(hits->intValue() + 1); if (shotsLeft->intValue() == 0) cannonField->setGameOver(); else cannonField->newTarget(); } Этот слот активируется, когда снаряд попал в цель. Тогда мы увеличиваем количество попаданий. Если снарядов не осталось, то игра завершена. В противном случае CannonField генерирует новую цель. void GameBoard::missed() { if (shotsLeft->intValue() == 0) cannonField->setGameOver(); } Этот слот активируется, когда снаряд пролетел мимо цели. Если снарядов не осталось, то игра завершена. void GameBoard::newGame() { shotsLeft->display(15); hits->display(0); cannonField->restartGame(); cannonField->newTarget(); } Этот слот активируется, когда пользователь щелкает по кнопке New Game. Также он вызывается из конструктора. Сначала он устанавливает количество снарядов равным 15. Обратите внимание на то, что это единственное место в программе где мы устанавливаем количество снарядов. Измените его так, как вам необходимо чтобы изменить правила игры. Далее, мы сбрасываем количество попаданий в цель, перезапускаем игру и генерируем новую цель. t13/main.cppЭтот файл уменьшился в размере. MyWidget из файла убрали и теперь осталась только одна функция main(), оставшаяся неизменной за исключением имени. Запуск приложенияПушка может стрелять по цели; после поражения цели автоматически появляется новая цель. Количество попаданий и оставшихся снарядов отображаются на экране и программа следит за ними. Игру можно завершить и тогда станет доступной кнопка начала новой игры. Домашнее заданиеДобавьте случайную поправку на ветер и покажите ее пользователю. Создайте несколько эффектов разрыва когда снаряд попадает в цель. Реализуйте множественные цели. [Предыдущий: Урок 12] [Учебное пособие] [Следующий: Урок 14]
|
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |