Java game loop (painting) freezes my window

I am changing the "views" using cardLayout (this class has a JFrame variable). When the user clicks the button of a new game, this happens:

 public class Views extends JFrame implements ActionListener { private JFrame frame; private CardLayout cl; private JPanel cards; private Game game; public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if (command.equals("New game")) { cl.show(cards, "Game"); game.init(); this.revalidate(); this.repaint(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { game.loop(); } }); } } } 

Game class method and title:

 public class Game extends JPanel implements KeyListener { public void loop() { while (player.isAlive()) { try { this.update(); this.repaint(); // first class JFrame variable jframee.getFrame().repaint(); // first class JFrame variable jframee.getFrame().revalidate(); Thread.sleep(17); } catch (InterruptedException e) { e.printStackTrace(); } } } public void update() { System.out.println("updated"); } } 

I draw with paintComponent()

 public void paintComponent(Graphics g) { System.out.println("paint"); ... } 

In fact, this does not draw anything. When I do not call the loop() method (therefore, it draws it only once), all images are colored correctly. But when I call the loop() method, nothing happens in the window. (Even the close button on the JFrame does not work.)

How to fix it? (When I created the JFrame inside the game class, everything worked fine, but now I want to have more views, so I need a JFrame in another class.)

Thanks.

+5
source share
3 answers

What does the update do? You should probably not call game.loop() on EDT. You run a loop on the EDT, your repaint will never be called, because repaint queues the event on the EDT, and it seems to be busy. Try moving game.loop() to another thread

 new Thread(new Runnable() { @override public void run() { game.loop(); } }).start(); 

That way, you won’t block EDT until repaint will still run on EDT.

+2
source

Precursor: Event Distribution Stream (EDT) .

Swing single-threaded. What does it mean?

All Swing processing starts with an event. EDT is a thread that processes these events in a loop in the following directions (but more complex):

 class EventDispatchThread extends Thread { Queue<AWTEvent> queue = ...; void postEvent(AWTEvent anEvent) { queue.add(anEvent); } @Override public void run() { while (true) { AWTEvent nextEvent = queue.poll(); if (nextEvent != null) { processEvent(nextEvent); } } } void processEvent(AWTEvent theEvent) { // calls eg // ActionListener.actionPerformed, // JComponent.paintComponent, // Runnable.run, // etc... } } 

The dispatch flow is hidden from us through abstraction: we usually only write listener callbacks.

  • Pressing the button publishes the event ( in native code ): when the event is processed, actionPerformed is called in actionPerformed .
  • The repaint call publishes the event: when the event is processed, paintComponent is called in the EDT.
  • invokeLater event message call: when the event is processed, run is called on EDT .
  • Everything in Swing starts with an event.

Event tasks are processed in sequence, in the order they are placed.

The following event can only be processed when the current event task is returned. That is why we cannot have an endless loop on EDT. actionPerformed (or run , as in your edit) never returns, so calls to repaint post paint events, but they are never handled, and the program seems to freeze.

This is what it means to β€œblock” EDT.


There are two main ways to make animations in a Swing program:

  • Use Thread (or SwingWorker ).

    The advantage of using a stream is that the processing is performed outside of the EDT, so during intensive processing, the graphical user interface can be updated at the same time.

  • Use javax.swing.Timer .

    The advantage of using a timer is that the processing is done in the EDT, so there is no need to worry about synchronization and safely change the state of the GUI components.

Generally speaking, we should only use a thread in a Swing program if there is a reason not to use a timer.

For the user, there is no noticeable difference between them.

Your call to revalidate tells me that you are changing the state of components in a loop (add, remove, change location, etc.). It is not necessarily safe to do from EDT. If you change the state of components, this is a good reason to use a timer rather than a thread. Using a stream without proper synchronization can lead to minor errors that are difficult to diagnose. See Memory Consistency Errors .

In some cases, operations on a component are performed under a tree lock (Swing independently ensures that they are thread-oriented), but in some cases this is not so.


We can rotate the cycle of the following form:

 while ( condition() ) { body(); Thread.sleep( time ); } 

in Timer following form:

 new Timer(( time ), new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { if ( condition() ) { body(); } else { Timer self = (Timer) evt.getSource(); self.stop(); } } }).start(); 

Here is a simple example demonstrating animation with both a stream and a timer. The green bar cyclically moves around the black panel.

Simple animation

 import javax.swing.*; import java.awt.*; import java.awt.event.*; class SwingAnimation implements Runnable{ public static void main(String[] args) { SwingUtilities.invokeLater(new SwingAnimation()); } JToggleButton play; AnimationPanel animation; @Override public void run() { JFrame frame = new JFrame("Simple Animation"); JPanel content = new JPanel(new BorderLayout()); play = new JToggleButton("Play"); content.add(play, BorderLayout.NORTH); animation = new AnimationPanel(500, 50); content.add(animation, BorderLayout.CENTER); // 'true' to use a Thread // 'false' to use a Timer if (false) { play.addActionListener(new ThreadAnimator()); } else { play.addActionListener(new TimerAnimator()); } frame.setContentPane(content); frame.pack(); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } abstract class Animator implements ActionListener { final int period = ( 1000 / 60 ); @Override public void actionPerformed(ActionEvent ae) { if (play.isSelected()) { start(); } else { stop(); } } abstract void start(); abstract void stop(); void animate() { int workingPos = animation.barPosition; ++workingPos; if (workingPos >= animation.getWidth()) { workingPos = 0; } animation.barPosition = workingPos; animation.repaint(); } } class ThreadAnimator extends Animator { volatile boolean isRunning; Runnable loop = new Runnable() { @Override public void run() { try { while (isRunning) { animate(); Thread.sleep(period); } } catch (InterruptedException e) { throw new AssertionError(e); } } }; @Override void start() { isRunning = true; new Thread(loop).start(); } @Override void stop() { isRunning = false; } } class TimerAnimator extends Animator { Timer timer = new Timer(period, new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { animate(); } }); @Override void start() { timer.start(); } @Override void stop() { timer.stop(); } } static class AnimationPanel extends JPanel { final int barWidth = 10; volatile int barPosition; AnimationPanel(int width, int height) { setPreferredSize(new Dimension(width, height)); setBackground(Color.BLACK); barPosition = ( width / 2 ) - ( barWidth / 2 ); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); int width = getWidth(); int height = getHeight(); int currentPos = barPosition; g.setColor(Color.GREEN); g.fillRect(currentPos, 0, barWidth, height); if ( (currentPos + barWidth) >= width ) { g.fillRect(currentPos - width, 0, barWidth, height); } } } } 
+3
source

Move the call to the game.loop () method to something like:

 SwingUtilities.invokeLater(new Runnable() { @Override public void run() { game.loop() } }); 

Thanks.

+1
source

All Articles