1

I have written a program, based off of some examples, to simulate Conway's game of life. Each cell in the game is either alive or dead and they are stored in an array of integers int[][] this array is always at least 1000*1000, meaning that there are millions of cells.

Iterating through the array to find apply all of the different rules is fine, and is not particularly CPU intensive. However, the method used to draw these rectangles on a JFrame can lead to 90+% CPU usage on my Xeon E3 1246 V3 (I7 4770). On array sizes of 2000^2, it can lead to Windows 10 hard locking completely.

@Override
    protected void paintComponent(Graphics g) {
        liveCells = 0;
        super.paintComponent(g);
        Color gColor = g.getColor();
        if(!backgroundWork || isPaused) {
            for (int row = 0; row < grid.length; row++) {
                for (int column = 0; column < grid[row].length; column++) {
                    //Checking if the cell is alive.
                    if (grid[row][column] == 1 ) {
                        liveCells++;
                        g.setColor(Color.red);
                        g.fillRect(column * UIScale, row * UIScale, UIScale, UIScale);

                    }
                }
            }
        }

        g.setColor(gColor);
        if (isPaused) { 
            g.drawString("The game is paused", 0, 30);
            g.drawString("Generation: " + generationCounter, 0, 10);
            g.drawString("Living cells: " + liveCells, 150, 10);
        } else { //If the game is running
            g.drawString("Generation: " + generationCounter++, 0, 10);
            g.drawString("Living cells: " + liveCells, 100, 10);
        }
        g.setColor(gColor);
        try {
            /* Sleep for some microseconds. */
            TimeUnit.MICROSECONDS.sleep(sleepTimer);
        } catch (InterruptedException ex) {
            System.err.println("An InterruptedException was caught: " + ex.getMessage());
        }
    }

I can see quite clearly that the drawing of the cells is the problem, in order to allow the program to run faster, I had already added a way to change the variable backgroundWork, which disables updating the grid of rectangles. Turning this on or off results in differences of up to 80% in task manager CPU utilisation.

With the current method of drawing the grid, I don't see a way to make it much faster, as all the cells are independent of each other, and generally there won't be more than 3 which are red next to each other anyway, so there is no reason to implement a way to draw multiple at the same time.

Can anyone suggest a way to speed up the current process, or a different way of drawing the squares. Thanks for any help.

Theo Pearson-Bray
  • 775
  • 1
  • 13
  • 32
  • 1
    Have you considered directly flipping bits in a BufferedImage's Raster, doing this off of the EDT, and then drawing the image in your paintComponent method? – Hovercraft Full Of Eels Jul 11 '15 at 16:05
  • So something similar to this http://stackoverflow.com/questions/14416107/int-array-to-bufferedimage – Theo Pearson-Bray Jul 11 '15 at 16:20
  • I would have to adapt it to allow for cells of different sizes. Currently g.fillRectangle draws a square with sides of UIScale. Based on what that is set to it can be a 1 to 1 representation between values in the array and pixels on the screen, or 4*4 pixels for one item in the array. – Theo Pearson-Bray Jul 11 '15 at 16:25
  • 1
    I'm not sure if it would make more sense to create a buffered image of the same size as the grid, and then blow it up onscreen, or to allow for multiple pixels in the image to represent one cell. – Theo Pearson-Bray Jul 11 '15 at 16:26
  • You could save a lot of memory by changing your grid of `int` to a grid of `boolean`. – Eric Leibenguth Jul 11 '15 at 16:30
  • Unfortunately not. While currently the grid only holds 0s and 1s, which would be better suited to boolean values, the plan is to have different properties for the cell based on different integer values. – Theo Pearson-Bray Jul 11 '15 at 16:33
  • Hovercraft full of eels. Your suggestion worked. The program is now sat at around 22% CPU usage, which is FAR better. Post an answer and I will accept it. One question however, when using the colour type BufferedImage.TYPE_INT_RGB the background for unset cells is black. It was suggested elsewhere that ARGB would be transparent, but that didn't show up any of the coloured cells. Without an else statement to set other cells to white, is there a way to set the default colour of a BufferedImage? – Theo Pearson-Bray Jul 11 '15 at 16:50

1 Answers1

0

This method, suggested by Hovercraft Full Of Eels (Nice name by the way) is much faster than what I was using previously-

@Override
protected void paintComponent(Graphics g) {
    liveCells = 0;

    BufferedImage BI = new BufferedImage(UIDimensions, UIDimensions, BufferedImage.TYPE_INT_RGB);

    super.paintComponent(g);
    Color gColor = g.getColor();
    if(!backgroundWork || isPaused) {
        for (int row = 0; row < grid.length; row++) {
            for (int column = 0; column < grid[row].length; column++) {
                //Checking if the cell is alive.
                if (grid[row][column] == 1 ) {
                    liveCells++;
                    if(UIScale != 1) {
                        //TODO- Draw squares larger than one pixel
                    } else {
                        BI.setRGB(column, row, 16711680);
                    } 

                    //The old code is commented out below

                    //g.setColor(Color.red);
                    //Drawing the colour in a 4x4 pixel square. With a window of 1000x1000, there are 250x250 organisms, hence the /4 everywhere
                    //g.fillRect(column * UIScale, row * UIScale, UIScale, UIScale);
                    //The way that this works is that it draws rectangles at the coordinates of the grid.
                    //The graphics on the screen aren't a grid themselves, they are just lots of squares
                } else {
                    BI.setRGB(column, row, 16777215);
                }
            }
        }
        g.drawImage(BI, 0, 0, null);
    }

    g.setColor(gColor);

    if (isPaused) { //If the game is paused (isPaused is true)
        g.drawString("The game is paused", 0, 30);
        g.drawString("Generation: " + generationCounter, 0, 10);
        g.drawString("Living cells: " + liveCells, 150, 10);
    } else { //If the game is running
        g.drawString("Generation: " + generationCounter++, 0, 10);
        g.drawString("Living cells: " + liveCells, 100, 10);
    }
    g.setColor(gColor);
    try {
        /* Sleep for some seconds. */
        TimeUnit.MICROSECONDS.sleep(sleepTimer);
    } catch (InterruptedException ex) {
        System.err.println("An InterruptedException was caught: " + ex.getMessage());
    }
}
Theo Pearson-Bray
  • 775
  • 1
  • 13
  • 32
  • 2
    I would not recommend that you ever call sleep within the Swing event thread and this goes doubly for within the paintComponent method. I'm surprised that doing this doesn't put your entire app to sleep. – Hovercraft Full Of Eels Jul 11 '15 at 17:55
  • 1
    Further remarks: 1. **Really** don't do this `sleep` there! 2. Don't create a new BufferedImage each time. You should probably only create it once (namely, when you create the `grid`). 3. In order to "draw squared larger than once pixel", you can simply do `((Graphics2D)g).scale(uiScale,uiScale);` before the `drawImage` call. 4. Don't use these decimal constants. Instead, use hex: `0xFF000000` is opaque black, `0xFFFF0000` is opaque red, `0xFF00FF00`=green, `0xFF0000FF`=blue etc. – Marco13 Jul 11 '15 at 18:42