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

в 12:12, , рубрики: github, Harmattan, life, linux, MeeGo, nokia, QML, qt, Qt Software, qwidget, qws, the life, Ubuntu, Анимация и 3D графика, жизнь, игра жизнь, Программирование, метки: , , , , , , , , , , , , ,

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

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

Сегодня я хочу показать, как реализовать всеми любимую игру 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'нем к нашему игровому виджету, а в лэйаут настроек — настройки)). Говорить можно долго, но лучше показать:
«Жизнь» Джеймса Конвея на Qt

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

Начнем, пожалуй, с алгоритма. Прошу извинения, но мне было лень думать и я решил реализовать самый простой алгоритм симуляции «Жизни» — с полным расчетом. Суть алгоритма предельно проста. Для каждой клетки мы считаем ее судьбу на основании предыдущего поколения и ее соседей. Плюсы алгоритма — он простой, как крышка из-под минералки. Минусы — довольно затратный. Но, как это со всеми случается — лень победила. Я уже придумал несколько моментов. Будем хранить две матрицы (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 <= universeSizкуe; 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)).

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

«Жизнь» Джеймса Конвея на Qt
«Пулемет» планеров на поле 50х50 с синим цветом.

«Жизнь» Джеймса Конвея на Qt
«Пулемет» планеров на поле 100х100

«Жизнь» Джеймса Конвея на Qt
Тот же пулемет, только с оранжевым цветом.

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

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

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

Успехов вам, и главное — удачного дева,
Илья.

Автор: namespace

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js