0
\$\begingroup\$

I am making a checkers game, and it is behaving very strangely. As of now, I am just trying to get the basics, such as grid layout, and getting the pieces laid out and moveable. My 8 by 8 grid opens just fine, and all of the pieces are laid out on the panels correctly, using two components:

  • PANELS: JPanels made in a seperate class, 64 total colored black and white, and filled into a centerPanel using GridLayout. Each panel has a specific row and column value(checkerPanel[row][col]).
  • PIECES: they are actually JLabels with the text "•" They are enlarged so that they fill the panel. They are placed on white panels in the first 3 rows for the red team, and placed in the last 3 rows for the gray team.

When I try to drag a piece, a new piece opens from the top left piece. When the piece from the top left is dragged over the old piece, which does not move, the old one is erased and the new one takes its place. How can I make the pieces behave correctly, moving from their old place, not the top left? Would it be easier to look into java.awt.Graphics?


I have looked at this StackOverflow question, and searched the Oracle JavaDocs, but can still not figure out what is wrong. Here is my current program:

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

public class Checkers extends JFrame {
    private static final long serialVersionUID = 1l;
    private static final int GRIDSIZE = 8;
    private CheckerPanel[][] checkerPanel = new CheckerPanel[GRIDSIZE][GRIDSIZE];

    public Checkers() {
        initGUI();
        setTitle("Checkers");
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
        setResizable(true);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void initGUI() {
        JLabel titleLabel = new JLabel("Checkers");
        Font titleFont = new Font(Font.SERIF, Font.BOLD, 32);
        titleLabel.setFont(titleFont);
        titleLabel.setHorizontalAlignment(JLabel.CENTER);
        titleLabel.setBackground(Color.BLACK);
        titleLabel.setForeground(Color.WHITE);
        titleLabel.setOpaque(true);     
        add(titleLabel, BorderLayout.NORTH);

        JPanel centerPanel = new JPanel();
        centerPanel.setLayout(new GridLayout(GRIDSIZE, GRIDSIZE)); // makes 8*8 grid
        add(centerPanel, BorderLayout.CENTER);

        for (int row=0; row<GRIDSIZE; row++) {
            for (int col=0; col<GRIDSIZE; col++) {
                JLabel grayPiece = new JLabel("•");
                JLabel redPiece = new JLabel("•");
                Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 70);
                grayPiece.setFont(font);
                redPiece.setFont(font);
                grayPiece.setForeground(Color.GRAY);
                redPiece.setForeground(Color.RED);
                // used so that the pieces are not offset
                grayPiece.setBorder(BorderFactory.createEmptyBorder(-27, 0, 0, 0)); 
                // used so that the pieces are not offset
                redPiece.setBorder(BorderFactory.createEmptyBorder(-27, 0, 0, 0));
                checkerPanel[row][col] = new CheckerPanel(row, col);

                final MouseAdapter ma = new MouseAdapter() {
                    // Clicked label.
                    private JLabel selectedLabel = null;
                    // Location of label in panel when it was clicked.
                    private Point selectedLabelLocation = null; 
                    // Panel's click point.
                    private Point panelClickPoint = null; 

                    public void mousePressed(final MouseEvent e) {
                        final Component pressedComp 
                            =  centerPanel.findComponentAt(e.getPoint());

                        if (pressedComp != null && pressedComp instanceof JLabel) {
                            selectedLabel = (JLabel) pressedComp;
                            selectedLabelLocation = selectedLabel.getLocation();
                            panelClickPoint = e.getPoint();
                            centerPanel.setComponentZOrder(selectedLabel, 0);
                            selectedLabel.repaint();
                        }
                        else {
                            selectedLabel = null;
                            //selectedLabelLocation = null;
                            panelClickPoint = null;
                        }
                        if (selectedLabel != null && selectedLabelLocation != null 
                            && panelClickPoint != null) {
                            final int newX = selectedLabelLocation.x;
                            final int newY = selectedLabelLocation.y;
                            selectedLabel.setLocation(newX, newY);
                        }
                    }

                    public void mouseDragged(final MouseEvent e) {
                        if (selectedLabel != null && selectedLabelLocation != null 
                            && panelClickPoint != null) {
                            final Point newPanelClickPoint = e.getPoint();
                            // The new location is the press-location plus 
                            // the length of the drag for each axis:
                            final int newX = selectedLabelLocation.x 
                                + (newPanelClickPoint.x - panelClickPoint.x),
                                      newY = selectedLabelLocation.y 
                                + (newPanelClickPoint.y - panelClickPoint.y);

                            selectedLabel.setLocation(newX, newY);
                        }
                    }
                };
                centerPanel.addMouseMotionListener(ma); //For mouseDragged().
                centerPanel.addMouseListener(ma); //For mousePressed().


                if ((row%2 == 0 && col%2 == 0) || ((row+1)%2 == 0 && (col+1)%2 == 0)) {
                    checkerPanel[row][col].setBackground(Color.WHITE);
                    checkerPanel[row][col].isWhite = true;
                } 
                else {
                    checkerPanel[row][col].setBackground(Color.BLACK);
                    checkerPanel[row][col].isBlack = true;
                }
                if (row < 3 && checkerPanel[row][col].isWhite) {
                    checkerPanel[row][col].add(redPiece);
                    checkerPanel[row][col].isPiece = true;
                } 
                else if (row > 4 && checkerPanel[row][col].isWhite) {
                    checkerPanel[row][col].add(grayPiece);
                    checkerPanel[row][col].isPiece = true;
                }
                centerPanel.add(checkerPanel[row][col]);
            }
        }
    }

    public static void main(String[] args) {
        try {
            String className = UIManager.getCrossPlatformLookAndFeelClassName();
            UIManager.setLookAndFeel(className);
        }
        catch (Exception e) {}

        EventQueue.invokeLater(new Runnable() {
            public void run() {
                new Checkers();
            }
        });
    }

    class CheckerPanel extends JPanel { //the following class makes Panels
        private static final long serialVersionUID = 1L;
        private static final int SIZE = 50;
        public boolean isBlack = false; //if the panel is white
        public boolean isWhite = false; //if the panel is black
        public boolean isPiece = false; //if the panel has a piece

        public CheckerPanel(int row, int col) {
            Dimension size = new Dimension(SIZE, SIZE);
            setPreferredSize(size);
        }
    }
}

Here's a screenshot of the window. When a piece is dragged, a new one comes out from the same place as the red top left piece.

Board

\$\endgroup\$
  • \$\begingroup\$ Why didn't you ask it on SO? This is a pure Java programming question, no? It is specifically about Java UI. \$\endgroup\$ – Engineer Jun 12 '17 at 7:05
  • \$\begingroup\$ I thought it was about game development and I thought it would fit the requirements here \$\endgroup\$ – CodingNinja Jun 12 '17 at 7:06
  • 1
    \$\begingroup\$ You'll find very few people use Swing here. It is really not a particularly games-centric technology. I doubt the problem has anything to do with your game logic, as such. Hence, SO probably better for quick response. \$\endgroup\$ – Engineer Jun 12 '17 at 7:07
  • 1
    \$\begingroup\$ That said, we do accept these questions. In fact, we already had a swing tag. Note that I have removed some of your content; you do not need to explain yourself to us, and as such, some of your content was not useful to the actual question. You also seem to have used tags that act as descriptions of the context, but not as singular identifiers for the fields in which your question pertains to (primarily, tags should not be used as descriptors, and should work "by themselves"). As such, I have changed your tags to better reflect the expertise your question calls for. \$\endgroup\$ – Gnemlock Jun 12 '17 at 7:42
1
\$\begingroup\$

This effect is caused by a number of issues in your code.

First, note that you're creating your MouseAdapter inside two nested for-loops. In effect, this means you're not just creating one MouseAdapter, you're actually creating GRIDSIZE x GRIDSIZE = 64 instances of MouseAdapter.

But that's not the cause of the problem. Rather, it has to do with how you find the piece that the user clicked on. At the top of mousePressed(...) you're calling centerPanel.findComponentAt(e.getPoint()). However, due to the way this method is defined, it will not actually find the piece you're after. Why is that?

findComponentAt internally works like this: on the Container it is invoked on, iterate over all child components. If a child is itself an instance of Container, recursively call findComponentAt on that child; otherwise check if the given point lies inside the child's boundaries. The problem with that method is that (a) when no child passes the test, it returns this (rather than, say, null) and (b) JLabel inherits from Container. These two points combined mean: the first leaf in the component tree under centerPanel that is either a non-Container and contains e.getPoint() OR is simply a Container will be returned. And since your pieces are JLabels and JLabels are containers, findComponentAt will always return the first piece, no matter if it actually contains the given point or not.

But wait, there's more! As a matter of fact, these are still not the only issues in the your current version of your code. Even if you replaced the call of findComponentAt with something that actually finds the right piece, like say, the following:

private JLabel findPiece(Point point) {
    for (int row = 0; row < GRIDSIZE; row++) {
        for (int col = 0; col < GRIDSIZE; col++) {
            CheckerPanel cp = checkerPanel[row][col];
            Point cpPoint = cp.getLocation();

            for (Component comp : cp.getComponents()) {
                if (comp instanceof JLabel &&
                    comp.contains(point.x - cpPoint.x, point.y - cpPoint.y)) {
                    return (JLabel) comp;
                }
            }
        }
    }

    return null;
}

you still have a problem due to the way your component hierarchy is organized. Your centerPanel contains 64 CheckerPanels which in turn may or may not contain a JPanel piece. Unfortunately, though, that means that you cannot draw a proper piece movement from one panel to the next because for drawing, child components (the JLabels) are confined to the boundaries of the parent container (CheckerPanel) which they're a child of. You've tried to circumvent that by calling centerPanel.setComponentZOrder(selectedLabel, 0). However, if you look at the documentation of setComponentZOrder you'll note the following:

If the component is a child of some other container, it is removed from that container before being added to this container.

This applies to your situation here: selectedLabel initially is not a child of centerPanel but of some CheckerPanel instance. Thus by calling setComponentZOrder you're removing it from that CheckerPanel and adding it to the centerPanel which basically sabotages your whole data structure in which the centerPanel contains CheckerPanels which contain JLabels.

In my opinion, you can make your own life a lot easier if you forget about a component hierarchy altogether for your game. Components are really made for traditional GUIs in desktop applications: menus, buttons, drop-down lists etc. Instead, you will get rid of a lot of problems if you implement centerPanel in such a way that you override its paintComponent method in which you draw the complete board with all the pieces yourself, i.e., CenterPanel and the pieces do not inherit from any AWT or Swing classes anymore but are purely for data structuring -- if you need theses classes at all!

If you want to go one step further and have animations such as dragging a piece be especially smooth, you may even forget about paintComponent and instead use a BufferStrategy to actively draw at a chosen frame rate, rather than repaint when some event occurs. However, this is again a bit more involved and for a Checkers game done purely for practice, you could certainly stick to the paintComponent approach of the previous paragraph.

\$\endgroup\$
  • \$\begingroup\$ If a child is itself an instance of Container, recursively call findComponentAt on that child... Wouldn't that find the text? or would it not because text is not a component? \$\endgroup\$ – CodingNinja Jun 14 '17 at 7:04
  • 2
    \$\begingroup\$ Exactly, text is not a component in itself. \$\endgroup\$ – Thomas Jun 14 '17 at 7:21
  • \$\begingroup\$ What would you suggest I change? Can I use Graphics to make a moveable circle as a piece, rather than JLabel? Should I make the pieces images? Thank you again for helping me out \$\endgroup\$ – CodingNinja Jun 14 '17 at 7:23
  • 1
    \$\begingroup\$ It's not super trivial to fix your problem based on the current approach of your code. As I said, setComponentZOrder changes your data structure since the JLabel becomes a child of the centerPanel instead of its CheckerPanel. One hacky way to keep most of what you have would be to (1) when you process the dragging, take into account that child locations are relative to the parent component, so you need to add the location of CheckerPanel parent to the JLabel's coordinates. Then when the mouse button is released, find the correct CheckerPanel and re-add the label there. \$\endgroup\$ – Thomas Jun 14 '17 at 7:26
  • 1
    \$\begingroup\$ Like I suggested in my second to last paragraph, look into paintComponent(Graphics g) as an alternative approach to the component structure. Here, you would draw the whole board and the pieces with Graphics' draw primitives yourself, which makes computation and control of locations so much easier. \$\endgroup\$ – Thomas Jun 14 '17 at 7:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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