Java game loop (painting) freezes my window
Asked Answered
W

3

6

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.

Willman answered 20/4, 2015 at 10:48 Comment(13)
You either blocking the event dispatching thread or breaking the chain or bothEutrophic
I added System.out.println(0) into game's paintComponent method and it wrotes 0 only once, but it didn't draw anything into jframe anywayWillman
Still sounds like you're block the EDTEutrophic
and what should I do? I'm kinda new in javaWillman
Well, consider providing a runnable example which demonstrates your problem. This is not a code dump, but an example of what you are doing which highlights the problem you are having. This will result in less confusion and better responsesEutrophic
Looks like the problem is in loop method. Can you post it?Sounder
I already did, second code blockWillman
well.. is player really alive? try changing it to while(true) for testSounder
I did, same result it writes 0 just onceWillman
The main question is, what thread(s) is all this stuff running on? Are you launching separate worker threads to do some of this? Where are you calling revalidate() and paint() from? What thread is the main game loop running on? All AWT stuff needs to happen on the EDT, and the rest of your work needs to happen off it. Sounds like an AWT threading issue. Reviewing an AWT/Swing tutorial should help; they all cover the relevant threading stuff.Warfield
well I'm not working with threads in any way, just in that loop I'm sleeping that thread. And revalidate and paint are called from class where I have card layoutWillman
What are you looking for in an answer that is not addressed by the existing accepted answer? And see also docs.oracle.com/javase/tutorial/uiswing/concurrency/…Dangle
@radiodef I can give that bounty in 8 hours, that's why it's still thereWillman
S
2

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.

Smoothen answered 23/4, 2015 at 11:43 Comment(1)
update is just updating coordinates of items, but finally it works, I appreciate your help, thanks.Willman
D
3

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);
            }
        }
    }
}
Dangle answered 24/4, 2015 at 1:26 Comment(0)
S
2

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.

Smoothen answered 23/4, 2015 at 11:43 Comment(1)
update is just updating coordinates of items, but finally it works, I appreciate your help, thanks.Willman
I
1

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

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

Thanks.

Iz answered 23/4, 2015 at 8:30 Comment(5)
I did, now it's showing me images(game screen) and game's method update() is looping(so coordinates of objects are updating) but it's not rendering this update(it's not calling paint() method), so it looks like its freezenWillman
Well my dear, the rest of your logic is a black box to me, so I can't help you unless you provide full relevant source code. Thanks.Iz
do you think I can send you whole code to you to take a look at it? (threw email or something)Willman
You can just post it here just like @Eutrophic instructed you above, so everyone can take a look at it including myself. Thanks.Iz
I edited questions, I change card in cardlayout(so it's called loop method) it writes me twice "paint" and xxxxx times "updated", so loop is working but it's not calling paint()Willman

© 2022 - 2024 — McMap. All rights reserved.