javaswingkeylistenerjcomponent

Unable to implement keylistener in JComponent


I've been working at the following bit of code for quite some time now, and I just can't seem to get the keyListeners to work. I've tried moving the setFocusable(true), requestFocus(), and addKeyListener(this), but it's not making a difference.

And, before anyone mentions it, yes, if I've learned one thing in all my readings up to this point, the internet seems to be in consensus that Key bindings are superior. The problem is, this is for a school assignment, so I've got to go by the books. What should I be doing differently to get the KeyListener to activate?

public class SnakeGUI extends JComponent implements KeyListener {
    private static JTextField timeKeeper;
    private static JTextField scoreKeeper;

    private static int time;
    private static int score;
    private static boolean playing;
    private static SnakeSettings settings;
    private static Snake snake;
    private static SnakePanel snakePanel;

    private static Timer gameTimer;
    private static Timer moveTimer;

    public static void main(String[] args) {

        // @author Every second, the displayed time ticks up
        TimerTask uptick = new TimerTask() {
            public void run() {
                time += 1;
                scoreKeeper.setText(Integer.toString(score));
                timeKeeper.setText(Integer.toString(time));
            }
        };

        // @author Depending on difficulty, the snake moves at different speeds.
        TimerTask move = new TimerTask() {
            public void run() {
                snake.move();
                playing = ! snake.isGameOver(settings.getWidth(), settings.getHeight());
                if (! playing) {
                    // @author Clear timers until next game.
                    gameTimer.cancel();
                    moveTimer.cancel();
                }
                else {
                    snakePanel.setDisplay(snake);
                }
            }
        };

        //@author Use defaults settings first time around.
        settings = new SnakeSettings();

        SnakeSettingsPanel settingsPanel;
        JFrame jf;
        SnakeGUI gui;
        
        // @author Alternate between game/settings elements till they quit.
        while (true) {
            settingsPanel = new SnakeSettingsPanel(settings);
            jf = new JFrame();
            jf.add(settingsPanel);
            jf.pack();
            jf.setTitle("Snake");
            jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            jf.setVisible(true);

            // @author Wait until they press play...
            while (! settingsPanel.getPlay()) {
                try {
                            Thread.sleep(25);
                    } catch (Exception e) {
                            System.out.println(e);
                    }
            }
            
            // @author Then, update settings accordingly.
            settings = settingsPanel.getSettings();
            gui = new SnakeGUI(settings);

            // @author Remove settingspanel, add game gui.
            jf.dispose();
            jf = new JFrame();
            jf.add(gui);
            jf.pack();
            jf.setTitle("Snake");
            jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            jf.setVisible(true);

            // @author Begin a new game.
            playing = true;
            time = 0;
            score = 0;
            snake = new Snake(new Point(settings.getWidth()/2, settings.getHeight()/2));

            gameTimer = new Timer();
            moveTimer = new Timer();
            gameTimer.schedule(uptick, 1000, 1000);

            // @author Set the delay based on the game speed.
            if (Speed.SLOW == settings.getSpeed()) {
                moveTimer.schedule(move, 750, 750);
            }
            else if (Speed.MEDIUM == settings.getSpeed()) {
                moveTimer.schedule(move, 500, 500);
            }
            else {
                moveTimer.schedule(move, 333, 333);
            }
            
            // @author Wait until the game ends.
            while (playing) {
                try {
                            Thread.sleep(25);
                    } catch (Exception e) {
                            System.out.println(e);
                    }
            }
            jf.dispose();
        }

    }

    public SnakeGUI(SnakeSettings set) {
        addKeyListener(this);
        setFocusable(true);
        requestFocus();

        setLayout(new GridBagLayout());
        
        JLabel tm = new JLabel("Time:");
        JLabel sc = new JLabel("Score:");
        timeKeeper = new JTextField(4);
        scoreKeeper = new JTextField(4);
        timeKeeper.setText("0");
        scoreKeeper.setText("0");
        timeKeeper.setEditable(false);
        scoreKeeper.setEditable(false);

        JPanel jp = new JPanel();
        jp.setLayout(new FlowLayout());

        jp.add(tm);
        jp.add(timeKeeper);
        jp.add(sc);
        jp.add(scoreKeeper);

        snakePanel = new SnakePanel(set);
        
        GridBagConstraints p = new GridBagConstraints();
            p.gridx = 0;
            p.gridy = 0;
        
        add(jp, p);
        p.gridy = 1;
        add(snakePanel, p);
        
        setVisible(true);
    }

    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}
    public void keyPressed(KeyEvent e) {
        System.out.println(1); // For testing purposes
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            System.out.println(1);
                snake.changeDirection(SnakeInterface.Direction.Left);
            }
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            System.out.println(2); // For testing purposes
                snake.changeDirection(SnakeInterface.Direction.Right);
            }
        if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                snake.changeDirection(SnakeInterface.Direction.Down);
            }
        if (e.getKeyCode() == KeyEvent.VK_UP) {
                snake.changeDirection(SnakeInterface.Direction.Up);
            }
    }
}


Solution

  • Alright, this...

    // @author Alternate between game/settings elements till they quit.
    while (true) {
        settingsPanel = new SnakeSettingsPanel(settings);
        jf = new JFrame();
        jf.add(settingsPanel);
        jf.pack();
        jf.setTitle("Snake");
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.setVisible(true);
    
        // @author Wait until they press play...
        while (! settingsPanel.getPlay()) {
            try {
                        Thread.sleep(25);
                } catch (Exception e) {
                        System.out.println(e);
                }
        }
        
        // @author Then, update settings accordingly.
        settings = settingsPanel.getSettings();
        gui = new SnakeGUI(settings);
    
        // @author Remove settingspanel, add game gui.
        jf.dispose();
        jf = new JFrame();
        jf.add(gui);
        jf.pack();
        jf.setTitle("Snake");
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.setVisible(true);
    
        // @author Begin a new game.
        playing = true;
        time = 0;
        score = 0;
        snake = new Snake(new Point(settings.getWidth()/2, settings.getHeight()/2));
    
        gameTimer = new Timer();
        moveTimer = new Timer();
        gameTimer.schedule(uptick, 1000, 1000);
    
        // @author Set the delay based on the game speed.
        if (Speed.SLOW == settings.getSpeed()) {
            moveTimer.schedule(move, 750, 750);
        }
        else if (Speed.MEDIUM == settings.getSpeed()) {
            moveTimer.schedule(move, 500, 500);
        }
        else {
            moveTimer.schedule(move, 333, 333);
        }
        
        // @author Wait until the game ends.
        while (playing) {
            try {
                        Thread.sleep(25);
                } catch (Exception e) {
                        System.out.println(e);
                }
        }
        jf.dispose();
    }
    

    is just crazy, add on top the use of static and you're quickly running into a lot of areas of trouble, any of which could easily explode in your face.

    Swing is not thread safe, so a part from risking dirty states between threads (dirty read/writes), you risk the UI trying to do a paint pass while the data is been updated - try and debug that.

    Swing is an event driven environment, that is, something happens and you respond to it, unlike a procedural driven workflow, like console apps, where each statement follows on from the last.

    The following is an overly simplified version, which has a "menu" and "game" pane which you can switch between simply, using a CardLayout and a series of "observers"

    One "trick" ... actually, it's not a "trick", it's down an out-right dirty hack, is when the "move" timer ticks, we request focus again. Again, this is hack and if you have other controls on the UI you expecting the user to interact with, then this is going to cause you no end of issues, as I've already stated, KeyListener is not an appropriate solution to the problem.

    Instead you should be using:

    And for a slightly less bad way of doing things...

    import java.awt.CardLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.FontMetrics;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.GridBagLayout;
    import java.awt.GridLayout;
    import java.awt.Rectangle;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.time.Duration;
    import java.time.Instant;
    import java.util.Set;
    import java.util.TreeSet;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new MainPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class MainPane extends JPanel {
    
            private CardLayout cardLayout;
            private MenuPane menuPane;
            private GamePane gamePane;
    
            public MainPane() {
                cardLayout = new CardLayout();
                setLayout(cardLayout);
    
                menuPane = new MenuPane(new MenuPane.Observer() {
                    @Override
                    public void didStartGame(MenuPane source) {
                        gamePane.start();
                        cardLayout.show(MainPane.this, "game");
                    }
                });
                gamePane = new GamePane(new GamePane.Observer() {
                    @Override
                    public void gameDidEnd(GamePane source, int score) {
                        source.stop();
                        cardLayout.show(MainPane.this, "menu");
                    }
                });
    
                add(menuPane, "menu");
                add(gamePane, "game");
            }
    
        }
    
        public class MenuPane extends JPanel {
    
            public interface Observer {
    
                public void didStartGame(MenuPane source);
            }
    
            public MenuPane(Observer observer) {
                setLayout(new GridBagLayout());
                JPanel contentPane = new JPanel(new GridLayout(-1, 1));
                JButton startButton = new JButton("Start");
                startButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        observer.didStartGame(MenuPane.this);
                    }
                });
    
                contentPane.add(startButton);
                add(contentPane);
            }
    
        }
    
        public class GamePane extends JPanel {
    
            public interface Observer {
    
                public void gameDidEnd(GamePane source, int score);
            }
    
            public enum Direction {
                UP, DOWN, LEFT, RIGHT;
            }
    
            private Set<Direction> keyManager = new TreeSet<>();
    
            private Timer gameTimer;
    
            private int score;
            private Instant startedAt;
    
            private Rectangle player = new Rectangle(195, 195, 10, 10);
    
            public GamePane(Observer observer) {
                addKeyListener(new KeyAdapter() {
                    @Override
                    public void keyPressed(KeyEvent e) {
                        if (e.getKeyCode() == KeyEvent.VK_UP) {
                            keyManager.add(Direction.UP);
                        } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                            keyManager.add(Direction.DOWN);
                        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                            keyManager.add(Direction.LEFT);
                        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                            keyManager.add(Direction.RIGHT);
                        }
                    }
    
                    @Override
                    public void keyReleased(KeyEvent e) {
                        if (e.getKeyCode() == KeyEvent.VK_UP) {
                            keyManager.remove(Direction.UP);
                        } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                            keyManager.remove(Direction.DOWN);
                        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                            keyManager.remove(Direction.LEFT);
                        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                            keyManager.remove(Direction.RIGHT);
                        }
                    }
                });
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(400, 400);
            }
    
            public void start() {
                stop();
    
                keyManager.clear();
    
                startedAt = Instant.now();
                score = 0;
    
                gameTimer = new Timer(5, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        //scoreKeeper.setText(Integer.toString(score));
                        //timeKeeper.setText(Integer.toString(time));
                        move();
                    }
                });
                gameTimer.start();
            }
    
            public void stop() {
                if (gameTimer != null) {
                    gameTimer.stop();
                }
            }
    
            protected void move() {
                requestFocusInWindow();
                int delta = 1;
                if (keyManager.contains(Direction.UP)) {
                    player.y -= delta;
                }
                if (keyManager.contains(Direction.DOWN)) {
                    player.y += delta;
                }
                if (keyManager.contains(Direction.LEFT)) {
                    player.x -= delta;
                }
                if (keyManager.contains(Direction.RIGHT)) {
                    player.x += delta;
                }
    
                if (player.y < 0) {
                    player.y = 0;
                } else if (player.y + player.height >= getHeight()) {
                    player.y = getHeight() - player.height;
                }
                if (player.x < 0) {
                    player.x = 0;
                } else if (player.x + player.width >= getWidth()) {
                    player.x = getWidth() - player.width;
                }
                repaint();
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g.create();
    
                g2d.setColor(Color.RED);
                g2d.fill(player);
    
                g2d.setColor(Color.BLACK);
    
                FontMetrics fm = g2d.getFontMetrics();
                String text = Integer.toString(score);
                g2d.drawString(text, 16, fm.getAscent());
    
                text = "---";
                if (startedAt != null) {
                    text = Long.toString(Duration.between(startedAt, Instant.now()).toSeconds());
                }
                g2d.drawString(text, getWidth() - 16 - fm.stringWidth(text), fm.getAscent());
    
                g2d.dispose();
            }
    
        }
    }
    

    Oh, and on the topic of static, don't use it this way, too easy for things to go very wrong. Instead, make use of dependency injection (ie Passing Information to a Method or a Constructor)