Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
576 views
in Technique[技术] by (71.8m points)

swing - Java game loop (painting) freezes my window

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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);
            }
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...