Java game loop (painting) freezes my window

后端 未结 3 1607
名媛妹妹
名媛妹妹 2020-11-28 16:48

I\'m changing \"views\" with cardLayout (this class has a JFrame variable). When a user clicks a new game button this happens:

public class View         


        
相关标签:
3条回答
  • 2020-11-28 17:04

    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);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 17:18

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

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

    Thanks.

    0 讨论(0)
  • 2020-11-28 17:20

    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.

    0 讨论(0)
提交回复
热议问题