fpiechowski
fpiechowski

Reputation: 517

KeyListener in a new thread for game input capture

I am making an old school Snake game in Java with Swing. I've read that in order to capture input in real time I need to run my game loop in a new thread so that It's wait() method won't interfere with the input capture. I've made InputCapture class implementing KeyListener and I've implemented keyPressed() method like that:

public class InputCapture implements KeyListener {

    private Direction capturedDirection;

    //Methods
    @Override
    public void keyPressed(KeyEvent e) {
        boolean inputConsoleDebug = true;
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            capturedDirection = Direction.left;
            if (inputConsoleDebug) System.out.println("LEFT");
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            capturedDirection = Direction.right;
            if (inputConsoleDebug) System.out.println("RIGHT");
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            capturedDirection = Direction.up;
            if (inputConsoleDebug) System.out.println("UP");
        } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            capturedDirection = Direction.down;
            if (inputConsoleDebug) System.out.println("DOWN");
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    public Direction getCapturedDirection() {
        return capturedDirection;
    }
}

Then I've made Game class extending Thread and I've put game loop code into run() method:

public class Game extends Thread {

    private Board board;
    private Snake snake;
    private JFrame frame;
    private long waitTime;
    private int difficultyStep;
    private Direction inputDirection;
    private InputCapture inputManager;

    //Constructors
    Game(Dimension boardSize) {
        //Set difficulty
        int applesToWin = boardSize.width * boardSize.height - 1;
        final int easiestWaitTime = 1000;
        final int hardestWaitTime = 100;
        difficultyStep = (easiestWaitTime - hardestWaitTime) / applesToWin;
        waitTime = easiestWaitTime;
        //Set starting point
        final int startingPointX = boardSize.width / 2;
        final int startingPointy = boardSize.height / 2;
        //Set board and snake
        board = new Board(boardSize);
        snake = new Snake(board, startingPointX, startingPointy);
        //Set window Frame
        frame = new JFrame(SnakeApplication.getApplicationName());
        frame.setContentPane(board);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.pack();
        frame.setResizable(false);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                super.windowClosing(e);
                interrupt();
            }
        });
        //Set input manager
        inputManager = new InputCapture();
        frame.addKeyListener(inputManager);
        inputDirection = null;
    }

    //Methods
    public void run() {
        board.spawnApple();
        while (!isWon()) {
            try {
                sleep(waitTime);
            } catch (InterruptedException e) {
                return;
            }
            try {
                inputDirection = inputManager.getCapturedDirection();
                snake.move(inputDirection);
            } catch (LosingMove e) {
                showGameOverDialog();
                return;
            }
            board.repaint();
        }
        showWinDialog();
    }

    JFrame getFrame() {
        return frame;
    }

    private boolean isWon() {
        for (int row = 0; row < board.getFields().length; row++) {
            for (int col = 0; col < board.getFields()[0].length; col++) {
                if (!(board.getFields()[row][col].getContent() instanceof Snake.SnakeNode)) return false;
            }
        }
        return true;
    }

    private void showGameOverDialog() {
        JFrame gameOverFrame = new JFrame();
        JOptionPane.showMessageDialog(gameOverFrame, "Game Over!");
    }

    private void showWinDialog() {
        JFrame gameOverFrame = new JFrame();
        JOptionPane.showMessageDialog(gameOverFrame, "You Win!");
    }
}

In my MainMenu class I've made startNewGame() method that is called when New Game button is clicked. This method creates Game object and starts a new thread by calling start() method.

public class MainMenu {

    //Form components references
    private JButton exitButton;
    private JFrame frame;
    private JPanel mainPanel;
    private JButton newGameButton;
    private JLabel titleLabel;

    //Constructors
    MainMenu() {
        //Set window Frame
        frame = new JFrame(SnakeApplication.getApplicationName());
        frame.setContentPane(mainPanel);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setResizable(false);
        frame.pack();
        newGameButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                startNewGame();
            }
        });
        exitButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                exitGame();
            }
        });
    }

    JFrame getFrame() {
        return frame;
    }

    private Dimension showBoardSizeDialog() {
        Frame boardSizeFrame = new Frame();
        int width = Integer.parseInt(JOptionPane.showInputDialog(boardSizeFrame, "Set board's width:"));
        int height = Integer.parseInt(JOptionPane.showInputDialog(boardSizeFrame, "Set board's height:"));
        return new Dimension(width, height);
    }

    private void startNewGame() {
        Dimension boardSize = showBoardSizeDialog();
        frame.setVisible(false);
        Game game = new Game(boardSize);
        game.getFrame().setVisible(true);
        //Starting game loop in a new thread
        game.start();
        try {
            game.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        frame.setVisible(true);
    }
}

But when testing the app it gets stuck in the game loop and doesn't capture input at all. Why? I was trying to debug It, but every time the new thread is started it gets stuck in game loop. The Board itself is painted only when main thread ends its execution. Why? Shouldn't It be repainted many times during game loop if execution is stucked there?

Also, I've made thread interrupt when frame's close button is clicked (red X button) so execution could get back to MainMenu and reappear it, but clicking red close button has no effect.

Upvotes: 0

Views: 627

Answers (2)

Radiodef
Radiodef

Reputation: 37875

The program freezes because of the call to game.join() in startNewGame. join keeps the thread it was called from from continuing execution until the thread it was called on dies. In your situation, join defeats the purpose of using another thread, so you should just remove that.

There are other issues, though. You probably shouldn't use a thread. You should probably use a Swing Timer. Swing isn't thread-safe, and I can already see a few places where your code isn't thread-safe either. (For example, you need to declare capturedDirection as volatile.) Writing correct multi-threaded code with Swing is a bit complicated and it would be much simpler to just use a timer.

Otherwise, if you don't use a timer, you need to use e.g. synchronization between the game thread (which writes to shared game state) and the Swing thread which does painting (and presumably reads from shared game state). If you don't, you may run in to problems that are hard to diagnose.

Also see The Use of Multiple JFrames: Good or Bad Practice?

Upvotes: 2

Mario
Mario

Reputation: 1801

You should make your Game class extending Runnable instead of Thread.

Then to have the game in a different thread:

Game theGame = ... // initialization code here
new Thread(theGame).start();

Upvotes: 0

Related Questions