Разработка игры на С++, Qt

Написал ремейк небольшой логической игрушки – “Полный квадрат”. Код показался мне достаточно интересным чтобы описать на блоге.

По сюжету игры, ёжик перемещается по лабиринту из облаков. Ходить ёж умеет только по вертикали и горизонтали, а начав движение идет до тех пор, пока не упрется в пустоту (пройденные облака исчезают), край экрана или грозовую тучу. Необходимо выбрать начальную позицию для ежа и такой маршрут движения, чтобы все облака оказались пройдены.

В любой игре, чуть более интересной чем “Сапер“, используются анимации, проигрывается звук, поэтому следующие компоненты Qt затронуты в статье:

  • класс QMovie для отображения gif-анимации тучек и ежа;
  • класс QPropertyAnimation – ежик перемещается плавно, при этом меняются его координаты (свойства);
  • QMediaPlayer из модуля Qt Multimedia. При перемещении наш ёжик топает;
  • Qt Style Sheets (QSS) используется для украшения элементов управления.
game_screens

снимки игровых экранов

Архитектура игры

game_uml

UML диаграмма классов игры

Игра состоит из нескольких экранов, которыми управляет ScreenController. Экраны я рисовал с использованием Qt Designer [1]. Каждый игровой экран содержит кнопки, позволяющие перейти на другой экран, события от которых передаются контроллеру.

Логика игры вынесена в класс GameWidget, который загружает на графическую сцену блоки и как-то обрабатывает их сигналы. Например, если на сцене уже размещен ёжик и пользователь кликнул облачко, то GameWidget обеспечивает перемещение ежа в новую позицию при условии, что это не будет противоречить правилам игры.

Экран помощи позволяет пользователю прочитать правила и сыграть в игру с подсказками. При этом, пользователь может кликать только по специальным подсвеченным тучкам – сигналы от других блоков игнорируются.

WinScreen выводит информацию о победе и предлагает сыграть в игру еще раз.

Qt Style Sheets (QSS)

При рисовании интерфейса в Qt Designer использованы самые обычные кнопки текстовые поля. Скругленные углы кнопок, цвет и размер рамки, изменение цвета при наведении и нажатии мыши получены за счет использования таблиц стилей Qt.

Установить таблицу стилей виджету (QWidget) или приложению (QApplication) можно методом setStyleSheet(). Для стилизации уже готового приложения можно передать таблицу стилей при запуске вместе с соответствующей опцией (-stylesheet style.qss). Таблица стилей Qt являются надстройкой над каскадными таблицами стилей, используемыми при верстке веб страниц (CSS3) [2].

Ключевым понятием в QSS является селектор, т.е. элемент, к которому мы хотим применить стили, при этом, если некоторому объекту устанавливается таблица стилей, то стили будут распространяться также на все дочерние элементы. В качестве селектора могут выступать:

  • все виджеты;

     "* { background-color: rgba(176, 196, 222, 255); }"
    

  • имя класса виджета. Стиль применяется ко всем элементам, которые могут быть приведены к классу с заданным именем;

    QLineEdit, QPushButton { border-radius: 10px; }
    

  • имя объекта определенного класса. Стиль применяется только к этому объекту;

    A#obj { border-radius: 10px; }
    

  • имя класса и свойства объекта. Стиль применяется ко всем объектам класса, обладающими заданным свойством;

    QPushButton[flat="true"] { border-radius: 10px; }
    

  • вложенные виджеты. Можно задавать применение стилей к объектам, вложенным непосредственно в текущий, либо с произвольным уровнем вложенности.

    A > B { background-color: rgba(255, 255, 255, 255); }
    A C { background-color: rgba(0, 0, 0, 255); }
    

В игре стили применяются к объекту класса ScreenController, и автоматически будут распространены на все вложенные виджеты.

setStyleSheet(
  "* { background-color: rgba(176, 196, 222, 255); }"
  "QPushButton { "
  "  background-color: rgba(255, 153, 102, 200); "
  "  border-style: outset;"
  "  border-width: 2px;"
  "  border-radius: 10px;"
  "  border-color: beige;"
  "  font: bold 14px;"
  "  width: 3em;"
  "  padding: 6px;"
  "}"
  "QPushButton:hover {"
    "background-color: rgba(255, 102, 0, 200);"
  "}"
  "QPushButton:pressed {"
    "background-color: rgba(255, 0, 0, 200);"
  "}"
  "QPushButton:disabled {"
    "background-color: rgba(204, 153, 102, 200);"
  "}"
  "QTextEdit {"
    "background-color: rgba(102, 204, 102, 200);"
    "  border-style: outset;"
    "  border-width: 0px;"
    "  border-radius: 10px;"
    "  border-color: black;"
    "  width: 3em;"
    "  padding: 6px;"
  "}"
);

В примере задается цвет фона для всех виджетов, параметры кнопок и текстового поля, а также цвет кнопок, находящихся в определенных псевдосостояниях. Стиль, примененный для QPushButton:disabled будет распространен на все неактивные кнопки.

При использовании QSS необходимо заглядывать в документацию, чтобы проверить возможность стилизации свойств определенного типа виджета [2], кроме того, в документации описано огромное количество примеров [3].

Анимация и звук в Qt

Ежик плавно перемещается, за счет использования QPropertyAnimation, при этом сам ёж при перемещении машет лапками – проигрывается соответствующая .gif-анимация (QMovie).

Любой элемент игры отображает какую-либо анимацию, поэтому в класс Block включены соответствующие поля и метод установки анимации.

class Block: public QWidget {
  Q_OBJECT
public:
  explicit Block(QWidget *parent = 0);
  void animation(QString texturename, bool randomStartFrame = false);
protected:
  QMovie *m_animation;
            //!< анимация, проигрываемая блоком
  QLabel *m_label;
            //!< метка для отображения анимации
};

Block::Block(QWidget *parent) : QWidget(parent),
  m_animation(new QMovie(this)), m_label(new QLabel(this)) {
  // ...
  m_label->setMovie(m_animation);
  m_animation->setCacheMode(QMovie::CacheMode::CacheAll);

  resize(BlockParam::BlockSize, BlockParam::BlockSize);
  m_animation->setScaledSize(size());
}
// ...
void Block::animation(QString texturename, bool randomStartFrame) {
  m_animation->stop();
  m_animation->setFileName(texturename);

  if (randomStartFrame)
    m_animation->jumpToFrame(qrand() % m_animation->frameCount());

  m_animation->start();
}

Класс блок агрегирует экземпляр QLabel, на котором и отображается анимация. QMovie поддерживает кеширование, которое по умолчанию отключено. При кешировании, в память помещается не следующий кадр, а пачка кадров, поэтому сокращаются затраты на их переключение. В приведенном фрагменте используется метод jumpToFrame(), устанавливающий текущий кадр, который не должен использоваться при включенном кешировании, однако в нашем случае он производит лишь установку начального кадра анимации и никогда не переключает кадры во время проигрывания.

Я уже рассказывал про графическую сцену в Qt [4], но в тот раз на сцене размещались экземпляры QGraphicsItem, но они не являются наследниками QWidget и не могут включать в себя виджеты (которым является QLabel, используемый для отображения анимации). В связи с этим, все наши ежи и облачка являются виджетами, а вместо QGraphicsScene::addItem() используется метод QGraphicsScene::addWidget(), создающий экземпляр QGraphicsProxyWidget, который и добавляется на сцену. Независимо от того, в какой позиции находился виджет до добавления, на сцене он окажется в левом верхнем углу. При добавлении на сцену QGraphicsProxyWidget копирует состояние исходного виджета, после этого состояние виджета всегда будет соответствовать состоянию прокси и наоборот – например, если скрыть виджет методом hide() – то скрыт будет и соответствующий прокси. При освобождении памяти из под прокси, будет удален исходный виджет. У класса QWidget имеется метод для получения соответствующего прокси – QWidget::graphicsProxyWidget().

В нашей игре, за добавление виджетов на сцену отвечает виртуальный метод GameWidget::itemAdd(), принимающий тип добавляемого виджета и создающий экземпляр соответствующего класса. В классе GameHelpWidget этот метод перегружается для обработки элементов нового типа. После добавления элемента на сцену, выполняется метод move(), т.к. иначе виджет останется в левом верхнем углу сцены.

void GameHelpWidget::itemAdd(int i, int j, char type) {
  if (type == 'h') {
    m_items[i][j] = new CloudHelp();
    m_scene->addWidget(m_items[i][j])->setZValue(CloudLayer);
    connect(m_items[i][j], SIGNAL(clicked()), SLOT(onBlockClicked()));
    m_items[i][j]->move(j * BlockParam::BlockSize, i * BlockParam::BlockSize);
  }
  else
    GameWidget::itemAdd(i, j, type);
}

Класс QPropertyAnimation позволяет анимировать свойства объекта, при этом указываются начальное и конечное значения свойства и время, за которое изменение должно произойти. QPropertyAnimation рассчитывает новое значение свойства, а по приходу сигнала от таймера (который есть внутри любого QObject) изменяет свойство и запрашивает перерисовку объекта. Под свойствами тут имеется ввиду любое Q_PROPERTY, в том числе, определенное пользователем – так, например, если бы мы разрабатывали виджет, моделирующий работу чашечных весов, рационально бы было определить угол наклона весов в виде свойства, а анимацию поворота рычага выполнять с помощью стандартного QPropertyAnimation. В нашей игре требуется плавно изменять координаты ёжика, а по окончанию анимации выполнять проверки на счет того, что ёж мог оказаться в тупике – проиграл, или обошел все клетки – выиграл.

void GameWidget::blockClickedHandler(Block *block) {
  // ...
  QPropertyAnimation *animation = new QPropertyAnimation(m_actor, "pos");
  animation->setDuration(delay);
  animation->setStartValue(m_actor->pos());
  animation->setEndValue(QPoint(BlockParam::BlockSize * j,
     BlockParam::BlockSize * i) + ActorShift);

  connect(animation, SIGNAL(finished()), this, SLOT(onMoveFinished()));
  animation->start();
  // ...
}

void GameWidget::onMoveFinished() {
  delete sender();

  // ... проверки, изменения состояний игры и ежа и т.п.
}

В конструкторе QPropertyAnimation указывается объект, свойство которого требуется изменять и имя свойства. Кроме того, может быть указан родительский объект, при разрушении которого из под анимации будет освобождена память – это удобно, если время проигрывания анимации совпадает со временем жизни какого-то объекта, но т.к. у нас такого класса нет – память освобождается в слоте-обработчике завершения анимации.

Во время перемещения ежа проигрывается трек с топотом. Для проигрывания звука используем класс QMediaPlayer.

Actor::Actor(QWidget *parent): Block(parent),
  m_player(new QMediaPlayer(this)),
  m_playlist(new QMediaPlaylist(m_player)) {

  m_player->setPlaylist(m_playlist);
  m_playlist->addMedia(QUrl("qrc:/game/audio/hardStep.wav"));
  m_playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);

  state(Idle);
}

void Actor::state(Direction direction) {
  switch (direction) {
    case Direction::Idle:
      animation(":/game/pics/actor_stay.gif");
      m_player->stop();
      break;
    case Direction::Up:
      animation(":/game/pics/actor_up.gif");
      m_player->play();
      break;
    case Direction::Down:
      animation(":/game/pics/actor_down.gif");
      m_player->play();
      break;
// ...

QMediaPlaylist позволяет создать список проигрывания и управлять ими. В приведенном фрагменте трек всего один, а QMediaPlaylist используется для зацикливания проигрывания музыки.

Исходный код игры про ежа.

Литература по теме

  1. Собственные виджеты в Qt Designer [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/958. Дата обращения: 24.07.2014.
  2. Qt Style Sheets Reference [Электронный ресурс] – режим доступа: http://qt-project.org/doc/qt-5/stylesheet-reference.html. Дата обращения: 24.07.2014.
  3. Qt Style Sheets Examples [Электронный ресурс] – режим доступа: http://qt-project.org/doc/qt-5/stylesheet-examples.html. Дата обращения: 24.07.2014.
  4. Работа с графической сценой Qt [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/1117. Дата обращения: 24.07.2014.

4 thoughts on “Разработка игры на С++, Qt

  1. Данияр

    Добрый день!
    Подскажите пожалуйста, как собрать эту игру для opensuse и windows 7. Буду благодарен за краткое руководство.

    Reply
    1. admin Post author

      Нужно установить компилятор c++ библиотеку Qt.
      Если все установлено – то собрать можно 2 способами:
      – в консоли перейти в каталог с исходным кодом (туда, где лежит файл проекта) и вызвать qmake. Естественно, команду qmake операционная система должна смочь найти, поэтому в переменной среды PATH должен быть прописан путь до нее.
      – используя Qt Creator. Пути до qmake и компилятора в PATH можно не прописывать, т.к. Qt Creator подхватит их сам (в Linux), а если не подхватит – добавить их можно в настройках IDE.

      Инструменты->Параметры->Компилятора
      и 
      Инструменты->Параметры->Комплекты
      

      После этого можно жать F5 и играть :).

      Для OpenSuse опишу чуть более подробно, т.к. сам ей пользуюсь. Идете в менеджер управления пакетами, устанавливаете пакеты make, gcc-c++, Mesa-libGL-devel. Скачиваете с официального сайта архив с библиотекой, меняете у указанного файла права доступа – делаете его исполняемым, после чего запускаете и можете пользоваться (просто открываете файл проекта в Qt Creator и жмете F5).

      Для windows установить надо mingw (make там будет внутри, в установщике вроде бы надо галочки проставить). Вот тут описан понятно и в картинках (писалось для школьников вроде бы) процесс установки.

      Пробуйте, если не получится – пишите что именно не работает с описаниями ошибок.

      Reply
  2. аноним

    Здравствуйте.
    Не понимаю как установить игру.

    • закачиваю;
    • открываю папку;
    • нажимаю на непонятный разброс программ в вашей папке;
    • открываю через Microsoft Visual Studio 2013;
    • присоединяю через к адмуньчеру.

    Игра даже не открывается. Подскажите что я не установил.

    Reply
    1. admin Post author

      Что такое адмунчьер?
      Для сборки программы из исходников нужно установить библиотеку Qt. Ее можно поставить в виде плагина для Visual Studio, хотя я так делал последний раз года 3 назад (на работе, писали игрушки и начальник постановил использовать Visual Studio, т.к. была куплена лицензия).
      Если никто не заставляет использовать поделки microsoft, я бы советовал установить Qt, собранный для mingw, т.к. это более вменяемый компилятор.

      Установить игру можно с google market – смотрите в соседней статье описан процесс сборки и есть ссылка на нее…
      У этой игры есть ряд недостатков:

      • притормаживает;
      • очень плохая графика на большом экране;
      • при сворачивании игры (не закрытии) продолжает потреблять много ресурсов.

      Третья проблема была связана с недоработкой в библиотеке Qt, сейчас это уже исправили, т.е. если вы установите последнюю версию Qt и соберете игру, то она будет переходить в спящий режим без проблем.
      Проблема с графикой связана с тем, что я использовал gif-анимацию. Я так сделал потому, что ее умеет проигрывать стандартный класс QMovie, я не думал что игру буду запускать на большом экране, но юзеры начали писать жалобы.
      Притормаживает игра, в общем, тоже из за анимации. Дело в том, что на графической сцене (QGraphicsScene) отображаются объекты QGraphicsItem, но мы тут решили использовать QMovie, а он является виджетом (QWidget). Виджеты никак не связаны с графической сценой и они умеют гораздо больше чем элементы сцены. Добавляя виджет на сцену мы неявно создаем QGraphicsProxyWidget, т.е. объект который выступает посредником между виджетом и элементом сцены и отражает любые изменения одного объекта на другой. Это приводит к бешеным накладным расходам, да и сам виджет рисуется в разы медленнее чем просто картинка.

      Скоро будет новая статья, где я опишу как все эти проблемы.

      Reply

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Вы не робот? * Time limit is exhausted. Please reload CAPTCHA.