John Conway's Life on Qt

  • Tutorial
Hi {{username}}!



Today I want to show how to implement everyone's favorite game of Life by John Conway on Qt. We will write on widgets. Using the example of this project, I will show how to work with QPainter, several classes from core, layouts and generally with graphics in Qt Widgets. Anyone interested in this game or working with graphics on Qt, please read on. In general, the article is aimed at beginners, but advanced guys will also have something to read :).

To whom laziness - here lies the source of the project. It can be built right away, dependencies on core, gui.


Idea


We want to implement Conway's Game Of Life with all the rules on the Qt GUI. It should be beautiful, scalable. It should be possible to specify the size of the field, the interval between generations, the choice of color for the cells. Still need buttons Start, Stop, Clear. You need to be able to save and load the game with all the configs.

Architecture


We will do so. on QPaintEvent we will recalculate the width of the cell depending on the width of the window, draw a grid and cells. We will store everything beautifully in layouts, at the Design stage - we will figure it out. Regarding the settings, we will put everything together very neatly into a small socket.

Design


In general, I decided not to dwell on this point. I will not teach anyone how to do trivial things in UI Designer. I will only paint architecture. Create a horizontal layout manager. Layout for him centralWidget. Then we insert there (in horizontal) two vertical managers. In the left there will be a window with the game, in the right - settings. In the designer, they look like the same size, but we set the stretch factor (the factor of the width relative to the neighboring layout) in the code. Insert a QWidget in the game’s layout - which by the way we will then promote to our game widget, and in the settings layout - settings)). You can talk for a long time, but it’s better to show:


Start to think


Let's start with the algorithm. I apologize, but I was too lazy to think, and I decided to implement the simplest “Life” simulation algorithm - with full calculation. The essence of the algorithm is extremely simple. For each cell, we consider its fate based on the previous generation and its neighbors. Advantages of the algorithm - it is as simple as a mineral water cap. Cons - quite expensive. But, as it happens with everyone, laziness triumphed. I already came up with a few points. We will store two matrices (universe, next) for the current and next generation. The m_masterColor variable is also useful for us to store the color of the cell, timer for the timer, universeSize is the size of the matrix. We will do it simply:
  1. Called startGame ()
  2. The timer starts to spin at a specified interval
  3. On timeout (), run newGeneration ()
  4. Here we fill in the next matrix based on bool isAlive (row, col)
  5. Redirect next to universe
  6. Redraw the widget via update ()
  7. paintEvent () calls grid and cell rendering methods
  8. And so long and tedious while the timer is running.
  9. And the timer works until the moment when universe == next or stopGame () will not be called


I will not focus on the implementation of this whole algorithm, but only on a small part of it - rendering. A bit of theory. In Qt, graphics are primarily the responsibility of QPainter; it contains methods for working with graphics. You can draw with it on the widget only in paintEvent (). By the way, here's what it looks like:
void GameWidget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    paintGrid(p);
    paintUniverse(p);
}


We will go deeper. Here we created an instance of QPainter and pass its link to the paintGrid () and paintUniverse () methods. They are exclusively involved in rendering the model (universe matrix). Everything is just like a clock. Now consider 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);
}


In the comments, all the points that may not be clear are painted. Now we will see how our "universe" is drawn:
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
            }
        }
    }
}


That's great. We can assume that we learned to draw. Nevertheless, I will not particularly dwell on QPainter - it is even very well described in the documentation, I’ll just say that it is based on three elephants - a pen, a brush and a shape (QRect, QCircle ...). The pen draws the outline of the figure, the brush - its fill. In the last listing, we did not set the handle, because we do not want the outlines of the square, but we set the brush to fill.

But how do we give the user the ability to mark cells? Obviously, we re-implement the keyPressEvent () method and will do something in it. Here is the listing of it:
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 / Open Map


This functionality is implemented by two buttons - Save / Load. Their task is to open and save files with game cards. The file contains:
  • Card size
  • Dump card
  • Color of living cells
  • Intergenerational Interval


Approximate format:
[size]
[dump]
[red] [green] [blue]
[interval]


Map size is implemented by GameWidget :: cellNumber () and GameWidget :: setCellNumber ()
Dump - GameWidget :: dump () and GameWidget :: setDump ().
Color - GameWidget :: masterColor () and GameWidget :: setMasterColor ().
The interval is GameWidget :: interval () and GameWidget :: setInterval ().

On the shoulders of MainWindow it remains only to write and read correctly. I will list the loadGame () function:
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);
}


Color selection


I won’t tell much here - it is implemented through QColorDialog and methods (specified above) of the GameWidget class. By the way, to the left of the button’s text there is a small square filled with the color that was selected. This is done through QIcon, which receives a 16x16 QPixmap - filled masterColor.

What I do not want to pay attention to


I won’t tell how to start the timer (timer-> start ()) or redraw the widget (update ()) - I hope this is clear, in the end Qt has, I am not afraid to say, one of the best documentation in the world.

Please do not write in the comments that the cells are rectangular, not square. This is really my cant - I would have to wrap it all in QAbstractScrollArea - but it turned out that I did not. In the end, forks and pool requests are welcome - not for nothing that I host on GitHub'e)).

Photos and examples



"Machine gun" gliders on a 50x50 field with blue color.


"Machine gun" gliders on the field 100x100


The same machine gun, only with orange.

Thanks for reading


Thank you all for taking the time to read this article.

Once again the source code on GitHub .
Map with a machine gun on github: gist .

I wish you success, and most importantly - a successful maiden,
Ilya.

Also popular now: