1

So, I'm trying to program a Game of Life simulation (Conway), and I want to show it in a JFrame.

For this purpose, I've created a JPanel, and it works perfectly, until I try to actually show a new generation. With prints, I've figured out, that the list is actually correct inside the newGeneration() method, but when paint(Graphics g) gets called (aka, when I try to repaint the JFrame), the list isn't updating.

I'm sure I've missed something obvious, and I'm not well versed in Java, but it's just getting so annoying. I'd really appreciate your help.

Here's my code;

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;


public class Main {

    public static void main(String[] args) {

        new GameOfLife();

    }
}


class GameOfLife {

    // Initialising all class wide variables; sorted by type

    JFrame frame = new JFrame("Game of Life");
    JPanel panel;

    Scanner gameSize = new Scanner(System.in);

    String dimensions;
    String splitHorizontal;
    String splitVertical;
    String confirmation;

    Boolean accepted = false;

    Integer split;
    Integer horizontal;
    Integer vertical;
    Integer livingNeighbours;

    int[][] cells;
    int[][] newCells;

    public GameOfLife() {

        // Prompt for game Size
        System.out.println("Please enter your game size in the following format; 'Horizontal,Vertical'");

        // Run until viable game Size has been chosen
        while (!accepted) {
            dimensions = gameSize.nextLine();

            // Check for correct format
            if (dimensions.contains(",")) {
                split = dimensions.indexOf(",");
                splitHorizontal = dimensions.substring(0, split);
                splitVertical = dimensions.substring(split + 1);

                // Check for validity of inputs
                if (splitHorizontal.matches("[0-9]+") && splitVertical.matches("[0-9]+")) {
                    horizontal = Integer.parseInt(dimensions.substring(0, split));
                    vertical = Integer.parseInt(dimensions.substring(split + 1));

                    // Check for game Size
                    if (horizontal > 1000 || vertical > 1000) {
                        System.out.println("A game of this Size may take too long to load.");
                    } else {

                        // Confirmation Prompt
                        System.out.println("Your game will contain " + horizontal + " columns, and " + vertical + " rows, please confirm (Y/N)");
                        confirmation = gameSize.nextLine();

                        // Check for confirmation, anything invalid is ignored
                        if (confirmation.matches("Y")) {
                            accepted = true;
                            System.out.println("Thank you for your confirmation. Please select live cells. Once your happy with your game, press Spacebar to start the Simulation.");

                            // Setting parameters depending on Size
                            frame.setSize(horizontal * 25 + 17, vertical * 25 + 40);
                            frame.setVisible(true);
                            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                        }
                    }
                }
            }

            // Prompt asking for new dimensions in case of invalid dimensions or non confirmation
            if (!accepted) {
                System.out.println("Please enter different dimensions.");
            }
        }


        // Creating list of cells
        cells = new int[horizontal][vertical];

        // Showing the empty panel for selection of live cells
        panel = new PaintCells(horizontal, vertical, cells);
        frame.add(panel);

        // Select live cells
        panel.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {

                if (cells[(int) Math.ceil(e.getX() / 25)][(int) Math.ceil(e.getY() / 25)] == 1) {
                    cells[(int) Math.ceil(e.getX() / 25)][(int) Math.ceil(e.getY() / 25)] = 0;
                } else {
                    cells[(int) Math.ceil(e.getX() / 25)][(int) Math.ceil(e.getY() / 25)] = 1;
                }
                frame.repaint();

            }
        });

        // Simulation start
        frame.addKeyListener(new KeyListener() {

            @Override
            public void keyTyped(KeyEvent e) {
                if (e.getKeyChar() == ' ') {
                    newGeneration();
                }

            }

            @Override
            public void keyPressed(KeyEvent e) {
            }

            @Override
            public void keyReleased(KeyEvent e) {
            }
        });
    }


    // Generating new generations
    void newGeneration() {

        newCells = new int[horizontal][vertical];

        // Pause inbetween generations
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /*
         * Way of Life Rules:
         * Living cells with 2 or 3 living neighbours live on to the next generation.
         * Dead cells with exactly 3 living neighbours become living cells in the next generation.
         * Every other living cell dies.
         */

        // iterate through every cell
        for (int l = 0; l < vertical; l++) {
            for (int k = 0; k < horizontal; k++) {

                livingNeighbours = 0;

                // check amount of neighbours
                if (k - 1 > -1) {
                    if (l - 1 > -1) {
                        if (cells[k - 1][l - 1] == 1) {
                            livingNeighbours++;
                        }
                    }
                    if (l + 1 < vertical) {
                        if (cells[k - 1][l + 1] == 1) {
                            livingNeighbours++;
                        }
                    }
                    if (cells[k - 1][l] == 1) {
                        livingNeighbours++;
                    }
                }
                if (k + 1 < horizontal) {
                    if (l - 1 >= 0) {
                        if (cells[k + 1][l - 1] == 1) {
                            livingNeighbours++;
                        }
                    }
                    if (l + 1 < vertical) {
                        if (cells[k + 1][l + 1] == 1) {
                            livingNeighbours++;
                        }
                    }
                    if (cells[k + 1][l] == 1) {
                        livingNeighbours++;
                    }

                }
                if (l - 1 >= 0) {
                    if (cells[k][l - 1] == 1) {
                        livingNeighbours++;
                    }
                }
                if (l + 1 < vertical) {
                    if (cells[k][l + 1] == 1) {
                        livingNeighbours++;
                    }
                }


                // change cell value depending on amount of neighbours
                if (cells[k][l] == 1) {
                    if (livingNeighbours < 2 || livingNeighbours > 3) {
                        newCells[k][l] = 0;
                    } else {
                        newCells[k][l] = 1;
                    }
                } else {
                    if (livingNeighbours == 3) {
                        newCells[k][l] = 1;
                    }
                }
            }
        }

        cells = newCells;
        frame.validate();
        frame.paint(frame.getGraphics());
        newGeneration();
    }
}


// Our canvas
class PaintCells extends JPanel {

    private Integer horizontal;
    private Integer vertical;
    private int[][] newOriginalCells;

    // Get our X and Y from the original prompts
    public PaintCells(Integer originalHorizontal, Integer originalVertical, int[][] originalCells) {
        this.horizontal = originalHorizontal;
        this.vertical = originalVertical;
        this.newOriginalCells = originalCells;
    }


    @Override
    public void paint(Graphics g) {
        for (int i = 0; i < vertical; i++) {
            for (int j = 0; j < horizontal; j++) {

                // Check cell value
                if (newOriginalCells[j][i] == 1) {
                    g.setColor(Color.black);
                } else {
                    g.setColor(Color.white);
                }

                // paint according to value
                g.fillRect(j * 25, i * 25, 25, 25);
                if (newOriginalCells[j][i] == 1) {
                    g.setColor(Color.white);
                } else {
                    g.setColor(Color.black);
                }                           // maybe change style?
                g.drawRect(j * 25, i * 25, 25, 25);
            }
        }
    }
}

I'm guessing, the problem is somewhere in newGeneration(), but other than that, I really have no idea anymore.

Lovahrk
  • 23
  • 7
  • 1
    *it works perfectly, until I try to actually show a new generation.* - code invoked from a listener executes on the `Event Dispatch Thread (EDT)`. This thread is responsible for responding to events and painting the GUI. The first thing you do is Thread.sleep() to show the animation of the generations. However since the EDT is sleeping it can't paint the GUI. Don't use Thread.sleep() on the EDT. The solution is to either 1) Use a SwingWorker and "publish" results or 2) use a Swing Timer to schedule the animation. – camickr Nov 09 '21 at 18:48
  • 1
    Read the [Swing tutorial](https://docs.oracle.com/javase/tutorial/uiswing/TOC.html). There are section on "Concurrency in Swing" and "How to Use a Swing Timer". You can also check out: https://stackoverflow.com/questions/64196198/bubble-sort-animation for examples of both approaches which may give you some ideas.. – camickr Nov 09 '21 at 18:48
  • @camickr the thing is, it used to work even with the sleep thing, and I don't think, my changes should have affected that.. but thanks, i'll try to use the swing timer instead – Lovahrk Nov 09 '21 at 19:11

1 Answers1

0

You have a common problem which I had myself a few months ago. Java Swing GUI system works in thread called Event Dispatch Thread (EDT). This thread handle events like mouse clicks, typing etc. and paint the components to the screen. You should use this thread not as your main thread, but as sub-thread which working only once a certain time/when event happens, and not let him run continuously. In your code, since the user choose the cell to live, this thread run non-stop (because you started the program inside a listener, which is part of the EDT), and your GUI stuck, because it's updating only at the end of the thread. You can solve this by using javax.swing.Timer. Timer is an object that allows you do tasks once a while, and it is perfect to this problem. Use code like this:

ActionListener actionListaner = new ActionListener(){
    public void actionPerformed(ActionEvent e){
        //Put here you ne genration repeating code
    }
};
int delay = 1000;//You delay between generations in millis
Timer timer = new timer(delay, actionListener);

The code in the actionPerformed method will repeat every second (or any other time you want it to repeat), and every operation of the timer will recall EDT instead of let it run non-stop.

Programmer
  • 803
  • 1
  • 6
  • 13