0

I'm sketching the grid on the user interface using the width and height that I specified when I created the GridPanel, accordingly. For representational purposes, I am laying the grid starting at 30, 30, and ending at x - 60, y - 60. Depending on the grid dimensions, dynamic shrink and grow are implemented using scaleX and scaleY.

The image below displays the internals of the cell object.

Cell object

GenerateMaze is a recursive backtracking algorithm used to generate perfect mazes. (Surely not complete, but I still should be able to see the result.)

public class GridPanel extends JPanel {
    private class Cell {
        private int y, x;
        private int[] south;
        private int[] north;
        private int[] west;
        private int[] east;

        private Cell(int y, int x) {
            this.y = y;
            this.x = x;

            this.south = new int[4];
            this.north = new int[4];
            this.west = new int[4];
            this.east = new int[4];
        }
    }

    private int w, h;
    private final int scaleX, scaleY;
    private final int Vx, Vy;
    private final Cell[][] grid;
    private boolean[][] discovered;

    public GridPanel(int w, int h) {
        setLayout(new GridLayout(1, 1));

        this.w = w;
        this.h = h;

        scaleX = (int) (w / Math.sqrt(w));
        scaleY = (int) (h / Math.sqrt(h));

        Vx = w / scaleX;
        Vy = w / scaleY;

        discovered = new boolean[Vy][Vx];

        grid = new Cell[Vy][Vx];
        init(grid);
    }

    private void init(Cell[][] grid) {
        for (int j = 0; j < grid.length; j++) {
            for (int i = 0; i < grid[0].length; i++) {
                grid[j][i] = new Cell(j, i);
            }
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        setDoubleBuffered(true);
        g.setColor(Color.CYAN);

        for (int y = 30, j = 0; y < h - 60; y += scaleY, j++) {
            for (int x = 30, i = 0; x < w - 60; x += scaleX, i++) {
                Cell cell = grid[j][i];

                cell.east = new int[]{x, y, x, y + scaleY};
                cell.west = new int[]{x + scaleX, y, x + scaleX, y + scaleY};
                cell.north = new int[]{x, y, x + scaleX, y};
                cell.south = new int[]{x, y + scaleY, x + scaleX, y + scaleY};

                g.drawLine(cell.east[0], cell.east[1], cell.east[2], cell.east[3]);
                g.drawLine(cell.west[0], cell.west[1], cell.west[2], cell.west[3]);
                g.drawLine(cell.south[0], cell.south[1], cell.south[2], cell.south[3]);
                g.drawLine(cell.north[0], cell.north[1], cell.north[2], cell.north[3]);
            }
        }

        generateMaze(g, 0, 0);
    }

    private void generateMaze(Graphics g, int y, int x) {
        discovered[y][x] = true;

        while (true) {
            Cell current = grid[y][x];

            if (validUp(y) && !discovered[y - 1][x]) {
                removeWall(g, current.north);
                generateMaze(g, y - 1, x);
            }
            if (validDown(y) && !discovered[y + 1][x]) {
                removeWall(g, current.south);
                generateMaze(g, y + 1, x);
            }
            if (validRight(x) && !discovered[y][x + 1]) {
                removeWall(g, current.east);
                generateMaze(g, y, x + 1);
            }
            if (validLeft(x) && !discovered[y][x - 1]) {
                removeWall(g, current.west);
                generateMaze(g, y, x - 1);
            }

            // All neighbors have been visited, break the loop
            break;
        }
    }


    private boolean validLeft(int x) {
        if (x - 1 < 0) return false;
        return true;
    }

    private boolean validRight(int x) {
        if (x + 1 < grid[0].length) return true;
        return false;
    }

    private void removeWall(Graphics g, int[] coordinate) {
        SwingUtilities.invokeLater(() -> {
            g.setColor(Color.BLACK);
            g.drawLine(coordinate[0], coordinate[1], coordinate[2], coordinate[3]);
        });
    }

    private boolean validDown(int y) {
        if (y + 1 < grid.length) return true;
        return false;
    }

    private boolean validUp(int y) {
        if (y - 1 < 0) return false;
        return true;
    }
}

° Application Frame:

public class Maze extends JFrame {
    private int w, h;

    public Maze(int w, int h) throws HeadlessException {
        super("Perfect Maze");
        setResizable(false);
        setBackground(Color.BLACK);

        this.w = w;
        this.h = h;

        add(new GridPanel(w, h));

        setSize(w, h);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }
}

° Runner:

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new Maze(800, 800);
        });
    }
}

Given that the user interface appears and the application closes successfully, I believe the removeWall function is the cause of the issue.

It did not work when I attempted to add method: repaint() at the end of the method.

5
  • 3
    It looks like you don't understand swing painting/drawing. You're calling generateMaze in your paintComponent. Don't do that. In removeWall you're calling "SwingUtilites.invokeLater" and attempting to use the graphics obect passed in for paintComponent. That is fundamentally broken. Do not change anything in your paintComponent, just paint.
    – matt
    Commented Dec 8, 2023 at 8:21
  • Thanks for the reply. How am I gonna reuse the graphics object? I want to draw from generateMaze method, right? Commented Dec 8, 2023 at 8:53
  • Do not reuse a graphics object. The idea in paintComponent is to just draw the component. If you want to build and modify an image, then use a BufferedImage. You can call getGraphics then modify the image with that graphics. Then you don't even need a paintComponent, just use a JLable with an ImageIcon.
    – matt
    Commented Dec 8, 2023 at 9:06
  • 1
    setDoubleBuffered(true); is pointless, JPanel is already double buffered, you should also be calling super.paintComponent first before doing any custom painting Commented Dec 8, 2023 at 9:24
  • " How am I gonna reuse the graphics object? I want to draw from generateMaze method, right?" - Painting can occur for any number of reasons, most of which you don't control. This also means that you perform you paint pass as quick as possible. Instead of trying to generate the maze each time the paint method is called, generate on the class creation and cache the data, use this to render the maze, or as matt has suggested, render it to a BufferedImage, but you might have issues if you want it to resize dynamically Commented Dec 8, 2023 at 9:26

1 Answer 1

1

Introduction

I redid your drawing code and came up with the following GUI. This is your maze grid with no walls removed.

Starting GUI

If you expand the GUI by clicking the box in the upper right, the grid resizes. It's hard to see because Stack Overflow scales the image.

Expanded GUI

Explanation

Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section. Pay particular attention to the Performing Custom Painting section.

When I create a Swing GUI, I use the model-view-controller (MVC) pattern. This pattern allows me to separate my concerns and focus on one small part of the Java application at a time.

A Swing model is made up of one or more plain Java getter/setter classes.

A Swing view is made up of one JFrame and as many JPanels or JDialogs as you need.

Each Action or Listener is a Swing controller. There's usually not one controller to "rule them all".

Model

I rewrote your Cell class. You don't need the coordinate because the cell position in the grid provides the coordinate. The coordinate is the x, y position of the cell in the grid, starting with 0,0 and going up to 39,39.

public class Cell {
    
    private boolean north, south, east, west;
    
    public Cell() {
        this.north = true;
        this.south = true;
        this.east = true;
        this.west = true;
    }

    public boolean isNorth() {
        return north;
    }

    public void setNorth(boolean north) {
        this.north = north;
    }

    public boolean isSouth() {
        return south;
    }

    public void setSouth(boolean south) {
        this.south = south;
    }

    public boolean isEast() {
        return east;
    }

    public void setEast(boolean east) {
        this.east = east;
    }

    public boolean isWest() {
        return west;
    }

    public void setWest(boolean west) {
        this.west = west;
    }
    
}

Your code to create the maze will remove the walls of a cell by setting the appropriate boolean to false; The Swing view code will paint the status of the grid. Period. Nothing else.

Here's the PerfectMazeModel class.

public class PerfectMazeModel {
    
    public final Cell[][] grid;
    
    public PerfectMazeModel() {
        this.grid = new Cell[40][40];
        for (int y = 0; y < grid.length; y++) {
            for (int x = 0; x < grid[y].length; x++) {
                grid[y][x] = new Cell();
            }
        }
    }

    public Cell[][] getGrid() {
        return grid;
    }
    
}

Here we create the grid. You don't have to make the grid square.

View

I use a JFrame and create a drawing JPanel. The drawing code is a bit complicated.

public class DrawingPanel extends JPanel {

    private static final long serialVersionUID = 1L;
    
    private final PerfectMazeModel model;
    
    public DrawingPanel(PerfectMazeModel model) {
        this.model = model;
        this.setPreferredSize(new Dimension(800, 800));
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        Cell[][] grid = model.getGrid();
        int margin = 20;
        int width = this.getWidth() - 2 * margin;
        int height = this.getHeight() - 2 * margin;
        int cellWidth = width / grid[0].length;
        int cellHeight = height / grid.length;
        int cellDimension = Math.min(cellWidth, cellHeight);
        
        int dx = margin;
        int dy = margin;
        for (int y = 0; y < grid.length; y++) {
            for (int x = 0; x < grid[y].length; x++) {
                Cell cell = grid[y][x];
                if (cell.isNorth()) {
                    g.drawLine(dx, dy, dx + cellDimension, dy);
                }
                if (cell.isWest()) {
                    g.drawLine(dx, dy, dx, dy + cellDimension);
                }
                if (cell.isSouth()) {
                    int cx = dx + cellDimension;
                    int cy = dy + cellDimension;
                    g.drawLine(dx, cy, cx, cy);
                }
                if (cell.isEast()) {
                    int cx = dx + cellDimension;
                    int cy = dy + cellDimension;
                    g.drawLine(cx, dy, cx, cy);
                }
                dx += cellDimension;
            }
            dx = margin;
            dy += cellDimension;
        }
    }
}

We get the cell dimension based on the current width and height of the drawing JPanel.

x and y are the logical coordinates of the grid cell. dx and dy are the upper left corner of the grid cell. cx and cy are the upper right and lower left points of the grid cell. We draw the line based on the boolean flag.

The cell/grid model isn't perfect because the east line of one cell is the same line as the west line of the adjacent cell. The same holds true for the north and south lines of a cell.

Controller

Since I'm just showing the model and drawing code, there are no controllers. I've left the addition of the code to remove lines for you.

Code

Here's the complete runnable code. I made the additional classes inner classes so I could post this code as one block.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class PerfectMaze implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new PerfectMaze());
    }
    
    private final DrawingPanel drawingPanel;
    
    private final PerfectMazeModel model;
    
    public PerfectMaze() {
        this.model = new PerfectMazeModel();
        this.drawingPanel = new DrawingPanel(model);
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Perfect Maze");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        frame.add(drawingPanel, BorderLayout.CENTER);
        
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }
    
    public void drawingPanelRepaint() {
        this.drawingPanel.repaint();
    }
    
    public class DrawingPanel extends JPanel {

        private static final long serialVersionUID = 1L;
        
        private final PerfectMazeModel model;
        
        public DrawingPanel(PerfectMazeModel model) {
            this.model = model;
            this.setPreferredSize(new Dimension(800, 800));
        }
        
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            
            Cell[][] grid = model.getGrid();
            int margin = 20;
            int width = this.getWidth() - 2 * margin;
            int height = this.getHeight() - 2 * margin;
            int cellWidth = width / grid[0].length;
            int cellHeight = height / grid.length;
            int cellDimension = Math.min(cellWidth, cellHeight);
            
            int dx = margin;
            int dy = margin;
            for (int y = 0; y < grid.length; y++) {
                for (int x = 0; x < grid[y].length; x++) {
                    Cell cell = grid[y][x];
                    if (cell.isNorth()) {
                        g.drawLine(dx, dy, dx + cellDimension, dy);
                    }
                    if (cell.isWest()) {
                        g.drawLine(dx, dy, dx, dy + cellDimension);
                    }
                    if (cell.isSouth()) {
                        int cx = dx + cellDimension;
                        int cy = dy + cellDimension;
                        g.drawLine(dx, cy, cx, cy);
                    }
                    if (cell.isEast()) {
                        int cx = dx + cellDimension;
                        int cy = dy + cellDimension;
                        g.drawLine(cx, dy, cx, cy);
                    }
                    dx += cellDimension;
                }
                dx = margin;
                dy += cellDimension;
            }
        }
    }
    
    public class PerfectMazeModel {
        
        public final Cell[][] grid;
        
        public PerfectMazeModel() {
            this.grid = new Cell[40][40];
            for (int y = 0; y < grid.length; y++) {
                for (int x = 0; x < grid[y].length; x++) {
                    grid[y][x] = new Cell();
                }
            }
        }

        public Cell[][] getGrid() {
            return grid;
        }
        
    }
    
    public class Cell {
        
        private boolean north, south, east, west;
        
        public Cell() {
            this.north = true;
            this.south = true;
            this.east = true;
            this.west = true;
        }

        public boolean isNorth() {
            return north;
        }

        public void setNorth(boolean north) {
            this.north = north;
        }

        public boolean isSouth() {
            return south;
        }

        public void setSouth(boolean south) {
            this.south = south;
        }

        public boolean isEast() {
            return east;
        }

        public void setEast(boolean east) {
            this.east = east;
        }

        public boolean isWest() {
            return west;
        }

        public void setWest(boolean west) {
            this.west = west;
        }
        
    }

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.