Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java game loop (painting) freezes my window

Tags:

java

swing

I'm changing "views" with cardLayout (this class has a JFrame variable). When a user clicks a new game button 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's loop method and heading of class:

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'm painting using paintComponent()

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

Actually it's not painting anything. When I do not call loop() method (so it paints it just once) all images are painted correctly. But when I call loop() method, just nothing is happening in the window. (Even close button on JFrame doesn't work.)

How to fix that? (When I was creating JFrame inside game class everything worked fine, but now I want to have more views so I need JFrame in other class.)

Thanks.

like image 453
dontHaveName Avatar asked Apr 20 '15 10:04

dontHaveName


3 Answers

Precursor: The Event Dispatch Thread (EDT).

Swing is single-threaded. What does this mean?

All processing in a Swing program begins with an event. The EDT is a thread that processes these events in a loop along the following lines (but more complicated):

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 e.g.
        // ActionListener.actionPerformed,
        // JComponent.paintComponent,
        // Runnable.run,
        // etc...
    }
}

The dispatch thread is hidden from us through abstraction: we generally only write listener callbacks.

  • Clicking a button posts an event (in native code): when the event is processed, actionPerformed is called on the EDT.
  • Calling repaint posts an event: when the event is processed, paintComponent is called on the EDT.
  • Calling invokeLater posts an event: when the event is processed, run is called on the EDT.
  • Everything in Swing begins with an event.

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

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

This is what it means to "block" the EDT.


There are basically two ways to do animation in a Swing program:

  • Use a Thread (or a SwingWorker).

    The benefit to using a thread is that the processing is done off the EDT, so if there is intensive processing, the GUI can still update concurrently.

  • Use a javax.swing.Timer.

    The benefit to using a timer is that the processing is done on the EDT, so there is no worry about synchronization, and it is safe to change the state of the GUI components.

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

To the user, there is no discernible difference between them.

Your call to revalidate indicates to me that you are modifying the state of the components in the loop (adding, removing, changing locations, etc.). This is not necessarily safe to do off the EDT. If you are modifying the state of the components, it is a compelling reason to use a timer, not a thread. Using a thread without proper synchronization can lead to subtle bugs that are difficult to diagnose. See Memory Consistency Errors.

In some cases, operations on a component are done under a tree lock (Swing makes sure they are thread-safe on their own), but in some cases they are not.


We can turn a loop of the following form:

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

in to a Timer of the 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 both with a thread and a timer. The green bar moves cyclically across 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);
            }
        }
    }
}
like image 130
Radiodef Avatar answered Oct 25 '22 05:10

Radiodef


What does update do? You probably shouldnt call game.loop() on the EDT. You are running a loop on EDT, your repaint wont ever be invoked since repaint queues an event on EDT and it seems kind busy. Try moving game.loop() to another thread

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

This way you wont block the EDT while the repaint still gets to be executed on the EDT.

like image 42
Renatols Avatar answered Oct 25 '22 05:10

Renatols


Move game.loop() method invocation to something like:

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

Thanks.

like image 38
m3nation Avatar answered Oct 25 '22 05:10

m3nation