«Жизнь» Джона Конвея на Qt

  • Tutorial
Привет, {{username}}!



Сегодня я хочу показать, как реализовать всеми любимую игру Game Of Life Джона Конвея на Qt. Писать будем на виджетах. На примере этого проекта я покажу как работать с QPainter, несколькими классами из core, лэйаутами и вообще с графикой в Qt Widgets. Всем кому интересна эта игра или работа с графикой на Qt, прошу читать дальше. Вообще, статья ориентирована на новичков, но и продвинутым ребятам тоже будет что прочитать:).

Кому лень — вот тут лежит исходничег проекта. Можно собирать сразу, зависимости на core, gui.


Идея


Хотим реализовать Conway's Game Of Life со всеми правилами на Qt GUI. Должно быть красиво, масштабируемо. Должна быть возможность задавать размер поля, интервал между поколениями, выбор цвета для клеток. Еще нужны кнопочки Start, Stop, Clear. Нужно уметь сохранять и загружать игру со всеми конфигами.

Архитектура


Будем делать так. на QPaintEvent будем делать перерасчет ширины клетки в зависимости ширины окна, рисовать сетку и клетки. Все будем хранить красивенько в лейаутах, на этапе Дизайн — разберемся. По части настроек — все очень аккуратненько соберем в маленькую панельку.

Дизайн


В целом, я решил особо не зацикливаться на этом пункте. Я не буду никого учить как делать тривиальные вещи в UI Designer. Только распишу архитектуру. Создаем горизонтальный менеджер компоновки. Лэйаутим под него centralWidget. Потом вставляем туда (в горизонтальный) два вертикальных менеджера. В левом будет окошко с игрой, в правом — настройки. В дизайнере они выглядят как одинакового размера, но stretch factor (фактор ширины относительно соседнего лэйаута) мы зададим в коде. В лэйаут игры вставим QWidget — который мы кстати потом promote'нем к нашему игровому виджету, а в лэйаут настроек — настройки)). Говорить можно долго, но лучше показать:


Начинаем думать


Начнем, пожалуй, с алгоритма. Прошу извинения, но мне было лень думать и я решил реализовать самый простой алгоритм симуляции «Жизни» — с полным расчетом. Суть алгоритма предельно проста. Для каждой клетки мы считаем ее судьбу на основании предыдущего поколения и ее соседей. Плюсы алгоритма — он простой, как крышка из-под минералки. Минусы — довольно затратный. Но, как это со всеми случается — лень победила. Я уже придумал несколько моментов. Будем хранить две матрицы (universe, next) для текущего и следующего поколения. Еще нам пригодится переменная m_masterColor для хранения цвета клетки, timer для таймера, universeSize — размер матрицы. Делать будем просто:
  1. Вызывается startGame()
  2. Начинает крутится таймер с заданным интервалом
  3. На timeout() запускаем newGeneration()
  4. Тут заполняем матрицу next на основе bool isAlive(row, col)
  5. Перенаправляем next на universe
  6. Перерисовываем виджет через update()
  7. paintEvent() вызывает методы отрисовки сетки и ячеек
  8. И так долго и нудно, пока работает таймер.
  9. А таймер работает до того момента когда universe == next или не будет вызван stopGame()


Я не буду зацикливаться на реализации всего этого алгоритма, а только на малой его части — отрисовке. Немного теории. В Qt за графику, преимущественно отвечает QPainter, он содержит методы для работы с графикой. Рисовать с ним на виджете можно только в paintEvent(). Кстати вот как он выглядит:
void GameWidget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    paintGrid(p);
    paintUniverse(p);
}


Будем идти глубже. Тут мы создали экземпляр QPainter и передаем его ссылку методам paintGrid() и paintUniverse(). Они занимаются исключительно отрисовкой модели (матрицы universe). Все просто как часы. Теперь рассмотрим paintGrid():
void GameWidget::paintGrid(QPainter &p)
{
    QRect borders(0, 0, width()-1, height()-1); // borders of the universe
    QColor gridColor = m_masterColor; // color of the grid
    gridColor.setAlpha(10); // must be lighter than main color
    p.setPen(gridColor);
    double cellWidth = (double)width()/universeSize; // width of the widget / number of cells at one row
    for(double k = cellWidth; k <= width(); k += cellWidth)
        p.drawLine(k, 0, k, height());
    double cellHeight = (double)height()/universeSize; // height of the widget / number of cells at one row
    for(double k = cellHeight; k <= height(); k += cellHeight)
        p.drawLine(0, k, width(), k);
    p.drawRect(borders);
}


В комментариях все моменты которые могут быть не понятны — расписаны. Теперь будем смотреть как рисуется наша «вселенная»:
void GameWidget::paintUniverse(QPainter &p)
{
    double cellWidth = (double)width()/universeSize;
    double cellHeight = (double)height()/universeSize;
    for(int k=1; k <= universeSize; k++) {
        for(int j=1; j <= universeSize; j++) {
            if(universe[k][j] == true) { // if there is any sense to paint it
                qreal left = (qreal)(cellWidth*j-cellWidth); // margin from left
                qreal top  = (qreal)(cellHeight*k-cellHeight); // margin from top
                QRectF r(left, top, (qreal)cellWidth, (qreal)cellHeight);
                p.fillRect(r, QBrush(m_masterColor)); // fill cell with brush of main color
            }
        }
    }
}


Вот и отличненько. Можно считать, что рисовать-то мы научились. Я все-же не буду особо останавливаться на QPainter — он даже очень хорошо описан в документации, только скажу, что он основан на трех слонах — ручке (pen), кисти (brush) и фигуре (QRect, QCircle...). Ручка рисует контур фигуры, кисть — ее заливку. В последнем листинге мы не задавали ручку, так как не хотим контуров квадратику, но задали кисть для заливки.

Но как мы дадим пользователю возможность отмечать клетки? Очевидно-же, ре-реализуем метод keyPressEvent() и будем что-то в нем делать. Вот кстати его листинг:
void GameWidget::mousePressEvent(QMouseEvent *e)
{
    double cellWidth = (double)width()/universeSize;
    double cellHeight = (double)height()/universeSize;
    int k = floor(e->y()/cellHeight)+1;
    int j = floor(e->x()/cellWidth)+1;
    universe[k][j] = !universe[k][j];
    update();
}


Сохранить/Открыть карту


Этот функционал реализуют две кнопочки — Save/Load. Их задача — открывать и сохранять файлы с игровыми картами. В файле хранится:
  • Размер карты
  • Дамп карты
  • Цвет живых клеток
  • Интервал между поколениями


Примерный формат:
[size]
[dump]
[red] [green] [blue]
[interval]


Размер карты реализуют GameWidget::cellNumber() и GameWidget::setCellNumber()
Дамп — GameWidget::dump() и GameWidget::setDump().
Цвет — GameWidget::masterColor() и GameWidget::setMasterColor().
Интервал — GameWidget::interval() и GameWidget::setInterval().

На плечах MainWindow осталось только правильно писать и читать. Я наведу листинг функции loadGame():
void MainWindow::loadGame()
{
    QString filename = QFileDialog::getOpenFileName(this,
                                                    tr("Open saved game"),
                                                    QDir::homePath(),
                                                    tr("Conway's Game Of Life File (*.life)"));
    if(filename.length() < 1)
        return;
    QFile file(filename);
    if(!file.open(QIODevice::ReadOnly))
        return;
    QTextStream in(&file);

    int sv;
    in >> sv;
    ui->cellsControl->setValue(sv);

    game->setCellNumber(sv);
    QString dump="";
    for(int k=0; k != sv; k++) {
        QString t;
        in >> t;
        dump.append(t+"\n");
    }
    game->setDump(dump);

    int r,g,b; // RGB color
    in >> r >> g >> b;
    currentColor = QColor(r,g,b);
    game->setMasterColor(currentColor); // sets color of the dots
    QPixmap icon(16, 16); // icon on the button
    icon.fill(currentColor); // fill with new color
    ui->colorButton->setIcon( QIcon(icon) ); // set icon for button
    in >> r; // r will be interval number
    ui->iterInterval->setValue(r);
    game->setInterval(r);
}


Выбор цвета


Не буду тут много рассказывать — реализовано через QColorDialog и методы (указанные выше) класса GameWidget. Кстати, слева от текста кнопочки есть квадратик заполненный цветом, который был выбран. Делается это через QIcon, который получает QPixmap размера 16х16 — заполненный masterColor.

То, на что я не хочу обращать внимание


Я не буду рассказывать как запускать таймер (timer->start()) или перерисовывать виджет (update()) — надеюсь это и так понятно, в конце-концов Qt обладает, не побоюсь сказать — одной из самых лучших документаций в мире.

Прошу не писать в комментариях что клетки получились прямоугольные, а не квадратные. Это действительно мой косяк — я должен был бы обернуть все это в QAbstractScrollArea — но так вышло, что я это не сделал. В конце-концов, форки и пул-реквесты приветствуются — не зря же я хостерюсь на GitHub'e)).

Фотки и примеры



«Пулемет» планеров на поле 50х50 с синим цветом.


«Пулемет» планеров на поле 100х100


Тот же пулемет, только с оранжевым цветом.

Спасибо за прочтение


Спасибо вам всем за то, что уделили время на прочтение этой статьи.

Еще раз исходники на GitHub.
Карта с Пулеметом на github:gist.

Успехов вам, и главное — удачного дева,
Илья.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 25

    +6
    А почему не на QGraphicsScene? Она же лучше подходит для всяких custom вещей.
      0
      К сожалению я ее не использовал еще. Может быть посмотрю в ее сторону, но не сейчас.
      0
      Немножко оффтопом: вот достаточно интересная задачка от Интела software.intel.com/en-us/contests/threading-challenge-students-2011/codecontest.php для многопоточности, так же это поинтересней простой игры жизни)
        0
        Спасибо — обязательно посмотрю.
        0
        А можно ли обновлять только часть виджета в paintEvent? Т.е. перерисовывать не всё поле, а только те клетки, которые изменились? Понятно, что вы специально так не сделали… Вопрос о возможности как таковой.
          +2
          Нет. paintEvent по определению вызывается тогда, когда нужно перерисовать весь виджет или его часть (нужная область передается через параметр QPaintEvent * event). Причем под частью здесь подразумевается не область с изменившимися клетками, а область, вылезшая, например, из-за пределов данного виджета при прокрутке и т.п, т.е. определяемая внешними по отношению к данному виджету факторами.
            +3
            Хотя, в update(), вызываемый из таймер эвента, конечно, можно передавать обновляемую область в качестве параметра.
              0
              Спасибо за наводку! ;)
              0
              Перерисовка тех клеточек, что изменились, по сути не связана с работой самого paintEvent, как уже говорил SiLiKhon. Это уже зависит от логики рендеринга.

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

              Для данного примера, изменений больше чем «не изменений», и скорость рендеринга работает предельно быстро. Собственно, для данного случае это делать не нужно).
              0
              Эх, жизнь была моим hello-world в Qt =)
              Пойду откапывать исходники, посмеюсь над кривокодом.
                0
                Покажете потом, ок?
                  0
                  Не думаю, что мой старый код окажется интересным, в нём всё было реализовано совсем уж «в лоб», не было так же ни сохранения, ни загрузки. =)
                0
                Здорово получилось, конечно можно было бы все сделать совсем по другому, через QGraphicsScene например. Но все равно.
                  0
                  Как я сказал выше — я с QGraphicsScene не работал. Мне роднее рисовать на виджете.
                  +5
                  Начинаем думать
                  Начнем, пожалуй, с алгоритма. Прошу извинения, но мне было лень думать
                    +4
                    Из комбинации лени и логики получаются программисты ©
                      0
                      Я писал проект за 2-3 часа — 4fun. Мне было банально лень использовать алгоритм со списками — за что прошу прощения.
                      0
                      >Еще нужны кнопочки Start, Stop, Clear
                      Ещё нужны Save и Load
                        0
                        Исправил.
                        0
                        Ух ты… Там в GIT еще и ваша GALACTICA лежит… осталось CONTINENT, KLIGON и ADVENTURES сделать. :)
                          0
                          Вы зря ржете. Вы похоже даже не читали что это за проект. Я его не афишировал еще — но через полгода это будет очень сильный проект. В вкратце это lightweight web developement ide с эвристическим анализом проекта.

                          А чем вам название не угодило?
                            0
                            Илья, вежливость наше все. Я не увидел насмешники в словах автора вышеуказанного коммента).
                              +1
                              Прошу извинение у автора комментария если в этом комментарии не было насмешки.

                              Просто сколько раз мне уже про галактические компиляторы рассказывали которые, и только они должны мою IDE компилить — это уже рефлекс.
                              0
                              Я не ржу… просто так называлась одна из игр времен ЕС-ЭВМ… если полазить на антресоли, то могу найти вам девятидорожечную бобину для ленточного ES-накопителя с этой игрой… :) Там же есть и CONTINENT, KLIGON и ADVENTURES… Кстати, CONTINENT, предтеча «Цивилизации», чудесно-бы смотрелся рядом с LIVE. Тогда цветных дислеев не было и это вызывало сложности считывания рельефа материков, а сейчас хоть 8 цветов на экране ей бы добавило очарования при полной аутентичной дикости первоисточника…
                          • НЛО прилетело и опубликовало эту надпись здесь

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое