NetBeans Project

The maze drawing program

The maze drawing program is a simple drawing program that we will use to set up a maze and place objects in the maze. In the next homework assignment you will write a game program that loads the maze created by the maze drawing program.

Version one

In the first version we will implement a drawing program that allows the user to draw the walls in the maze.

The walls in the maze are represented by a Wall class.

public class Wall {

    private Line line;
    
    private double snap(double x) {
        long n = Math.round(x / 40);
        return n * 40.0;
    }
    
    public Wall(double x, double y) {
        line = new Line();
        line.setStartX(snap(x));
        line.setStartY(snap(y));
        line.setEndX(x);
        line.setEndY(y);
    }
    
    public Shape getShape() { return line; }
    
    public void setEnd(double x, double y) {
        line.setEndX(x);
        line.setEndY(y);
    }
    
    public void snapToGrid(double x, double y) {
        double deltaX = Math.abs(x - line.getStartX());
        double deltaY = Math.abs(y - line.getStartY());
        if (deltaX > deltaY) {
            line.setEndX(snap(x));
            line.setEndY(line.getStartY());
        } else {
            line.setEndX(line.getStartX());
            line.setEndY(snap(y));
        }
    }
    
    public void write(PrintWriter out) {
        out.println(Math.round(line.getStartX()) + " " + Math.round(line.getStartY())
                + " " + Math.round(line.getEndX()) + " " + Math.round(line.getEndY()));
    }
}

A wall is essentially a Line, with some additional methods added to support placing the wall with the mouse and writing the wall to a text file. Most of the methods in this class support drawing the walls with the mouse. In the next section when I discuss how the drawing program handles mouse interactions we will see what most of these methods are meant to do.

The MazePane class

The most important class in the application is the MazePane class, which implements a custom component that allows us to draw mazes.

public class MazePane extends Pane {

    private ArrayList<Wall> walls;
    private Wall dragging;

    public MazePane() {
        walls = new ArrayList<Wall>();
        for (int row = 0; row <= 11; row++) {
            for (int col = 0; col <= 16; col++) {
                Circle circle = new Circle();
                circle.setCenterX(col * 40);
                circle.setCenterY(row * 40);
                circle.setFill(Color.RED);
                circle.setRadius(2);
                this.getChildren().add(circle);
            }
        }
        this.setOnMousePressed(e -> startDrag(e));
        this.setOnMouseDragged(e -> dragSegment(e));
        this.setOnMouseReleased(e -> endDrag(e));
    }

    public void startDrag(MouseEvent e) {
        dragging = new Wall(e.getX(), e.getY());
        walls.add(dragging);
        this.getChildren().add(dragging.getShape());
    }

    public void dragSegment(MouseEvent e) {
        dragging.setEnd(e.getX(), e.getY());
    }

    public void endDrag(MouseEvent e) {
        dragging.snapToGrid(e.getX(), e.getY());
        dragging = null;
    }

    public void save() {
        try {
            PrintWriter out = new PrintWriter(new File("maze.txt"));
            out.println(walls.size());
            for (Wall w : walls) {
                w.write(out);
            }
            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

The MazePane is a essentially a container for a list of Wall objects, along with code to draw a grid of guide points and code to handle mouse interactions for placing the walls.

The constructor for the MazePane class sets up grid of guide points by constucting a large number of small Circle objects and adding them to the pane.

Handling mouse interactions

JavaFX applications are GUI applications. The most important thing going on in a GUI application is handling user events. Whan a GUI application's user presses a key on the keyboard, moves the mouse, or clicks a button on the mouse, we say that the user has generated an event. GUI applications have to be set up to respond to these events when they take place. This is especially the case for custom panes: we will have to write all of our own logic to have our custom pane respond to user events when they take place. In this example application I will show how to set up a custom pane to respond to mouse events. In the second version of the maze drawing program I will demonstrate how to add support for keyboard events as well.

To place a wall in the maze drawing program the user will start by clicking the left mouse button somewhere in the maze pane. This initial press of the mouse will start a mouse drag in which the user keeps the mouse button held down while moving the mouse. The mouse drag will end when the user releases the mouse button. Each of these distinct actions, pressing the mouse button, moving the mouse with the button held down, and releasing the mouse, is considered a distinct user event. JavaFX will want us to respond to each of these events separately.

To respond to a user event in JavaFX, you set up an event listener for the event in question. To set up event listeners in our MazePane class we make a series of calls to setOn methods in the MazePane constructor. Each one of these sets up a different listener for a different event type.

this.setOnMousePressed(e -> startDrag(e));
this.setOnMouseDragged(e -> dragSegment(e));
this.setOnMouseReleased(e -> endDrag(e));

The simplest way to set up an event listener in JavaFX is to use a lambda expression. In this example we use lambda expressions that call methods in the MazePane class in response to each different event type.

Each one of these event processing methods takes a single parameter, an object of type javafx.scene.input.MouseEvent. The MouseEvent object carries with it information about what the mouse is doing at the instant the user generated the event. Most importantly, the MouseEvent object can tell us the location of the mouse in the pane when the user generated the event.

The process of drawing a wall segment starts with the initial mouse press. The MazePane responds to the mouse press by calling this method:

public void startDrag(MouseEvent e) {
    dragging = new Wall(e.getX(), e.getY());
    walls.add(dragging);
    this.getChildren().add(dragging.getShape());
}

This method creates a new Wall object that has one of its ends attached to the closest grid point and its other end set to the mouse location. We ask the Wall object to give us the Line object that it contains so we can add the Line to our list of children. Without this step the Line would not be drawn.

As the user drags the mouse we will set the location of the end of the Wall that the user is dragging. In the MazePane class we do

public void dragSegment(MouseEvent e) {
    dragging.setEnd(e.getX(), e.getY());
}

and in the Wall we do

public void setEnd(double x, double y) {
    line.setEndX(x);
    line.setEndY(y);
}

Once the user released the mouse button the drag will end. Releasing the mouse button generates a mouse released event, which gets handled by this method in the MazePane:

public void endDrag(MouseEvent e) {
  dragging.snapToGrid(e.getX(), e.getY());
  dragging = null;
}

The snapToGrid() method in the Wall class handles an important part of the application logic. Since we want to constrain our walls in the maze to start and end at grid points and we also want to constrain our walls to be either strictly horizontal or vertical, we need logic to enforce these constraints.

public void snapToGrid(double x, double y) {
    double deltaX = Math.abs(x - line.getStartX());
    double deltaY = Math.abs(y - line.getStartY());
    if (deltaX > deltaY) {
        line.setEndX(snap(x));
        line.setEndY(line.getStartY());
    } else {
        line.setEndX(line.getStartX());
        line.setEndY(snap(y));
    }
}

The logic here determines whether the line the user is drawing is closer to a horizontal line or a vertical line, and then forces the line to be either horizontal or vertical with its end point snapped to the nearest grid point.

Saving the maze

One last important feature in our maze drawing application is the ability to save drawings to a text file. To support this feature the MazePane class has a save() method that saves the contents of the maze to a text file:

public void save() {
    try {
        PrintWriter out = new PrintWriter(new File("maze.txt"));
        out.println(walls.size());
        for (Wall w : walls) {
            w.write(out);
        }
        out.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

The Wall class also has a write() method that the Wall can use to write itself out to the text file.

public void write(PrintWriter out) {
    out.println(Math.round(line.getStartX()) + " " + Math.round(line.getStartY())
            + " " + Math.round(line.getEndX()) + " " + Math.round(line.getEndY()));
}