Why does my custom Swing component repaint faster when I move the mouse? (Java)
Asked Answered
R

3

10

I am trying to make a 2D game with Java and Swing, and the window refreshes too slow. But if I move the mouse or press keys, the window refreshes as fast as it should!

Here is a GIF showing how the window refreshes quickly only when I move the mouse.

enter image description here

Why does the window refresh slowly like that? Why does the mouse and keyboard affect its refresh rate? How, if possible, do I make it refresh quickly all the time?

Background Info

I use a javax.swing.Timer to update the game state every 1/25 seconds, after which it calls repaint() on the game panel to redraw the scene.

I understand that a Timer might not always delay for exactly 1/25 of a second.

I also understand that calling repaint() just requests the window to be repainted ASAP and does not repaint the window immediately.

My graphics card does not support OpenGL 2+ or hardware accelerated 3D graphics, which is why I am not using libgdx or JME for game development.

System Info

  • Operating system: Linux Mint 19 Tara
  • JDK version: OpenJDK 11.0.4
  • Graphics card: Intel Corporation 82945G/GZ

Research

This Stack Overflow user describes the same problem I have, but the author reportedly solved the issue by calling repaint() repeatedly on a separate timer. I tried this, and it does make the window refresh somewhat faster, but even then it is a slower than I want. In this case, wiggling the mouse on the window still improves the refresh rate. Therefore, it seems like that post did not truly solve the issue.

Another Stack Overflow user also encountered the issue, but they use a continuous while-loop instead of a Timer for their game loop. Apparently, this user solved the problem by using Thread.sleep() in their while loop. However, my code accomplishes the delay using a Timer, so I do not know how Thread.sleep() could solve my problem, or even where I would put it.

I've read through Painting with AWT and Swing to figure out whether I just misunderstood the concept of repainting, but nothing in that document elucidates the issue for me. I call repaint() whenever the game updates, and the window only refreshes quickly when mouse or keyboard input is happening.

I have searched the web for several days now trying to find an answer, but nothing seems to help!

Code

import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

class Game {
        public static final int screenWidth = 160;
        public static final int screenHeight = 140;

        /**
         * Create and show the GUI.
         */
        private static void createAndShowGUI() {
                /* Create the GUI. */
                JFrame frame = new JFrame("Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setResizable(false);
                frame.getContentPane().add(new GamePanel());
                frame.pack();

                /* Show the GUI. */
                frame.setVisible(true);
        }

        /**
         * Run the game.
         *
         * @param args  the list of command-line arguments
         */
        public static void main(String[] args) {
                /* Schedule the GUI to be created on the EDT. */
                SwingUtilities.invokeLater(() -> createAndShowGUI());
        }

}

/**
 * A GamePanel widget updates and shows the game scene.
 */
class GamePanel extends JPanel {
        private Square square;

        /**
         * Create a game panel and start its update-and-draw cycle
         */
        public GamePanel() {
                super();

                /* Set the size of the game screen. */
                setPreferredSize(
                        new Dimension(
                                Game.screenWidth,
                                Game.screenHeight));

                /* Create the square in the game world. */
                square = new Square(0, 0, 32, 32, Square.Direction.LEFT);

                /* Update the scene every 40 milliseconds. */
                Timer timer = new Timer(40, (e) -> updateScene());
                timer.start();
        }

        /**
         * Paint the game scene using a graphics context.
         *
         * @param g  the graphics context
         */
        @Override
        protected void paintComponent(Graphics g) {
                super.paintComponent(g);

                /* Clear the screen. */
                g.setColor(Color.WHITE);
                g.fillRect(0, 0, Game.screenWidth, Game.screenHeight);

                /* Draw all objects in the scene. */
                square.draw(g);
        }

        /**
         * Update the game state.
         */
        private void updateScene() {
                /* Update all objects in the scene. */
                square.update();

                /* Request the scene to be repainted. */
                repaint();
        }

}

/**
 * A Square is a game object which looks like a square.
 */
class Square {
        public static enum Direction { LEFT, RIGHT };

        private int x;
        private int y;
        private int width;
        private int height;
        private Direction direction;

        /**
         * Create a square game object.
         *
         * @param x          the square's x position
         * @param y          the square's y position
         * @param width      the square's width (in pixels)
         * @param height     the square's height (in pixels)
         * @param direction  the square's direction of movement
         */
        public Square(int x,
                      int y,
                      int width,
                      int height,
                      Direction direction) {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
                this.direction = direction;
        }

        /**
         * Draw the square using a graphics context.
         *
         * @param g  the graphics context
         */
        public void draw(Graphics g) {
                g.setColor(Color.RED);
                g.fillRect(x, y, width, height);
                g.setColor(Color.BLACK);
                g.drawRect(x, y, width, height);
        }

        /**
         * Update the square's state.
         *
         * The square slides horizontally
         * until it reaches the edge of the screen,
         * at which point it begins sliding in the
         * opposite direction.
         *
         * This should be called once per frame.
         */
        public void update() {
                if (direction == Direction.LEFT) {
                        x--;

                        if (x <= 0) {
                                direction = Direction.RIGHT;
                        }
                } else if (direction == Direction.RIGHT) {
                        x++;

                        if (x + width >= Game.screenWidth) {
                                direction = Direction.LEFT;
                        }
                }
        }
}
Revelationist answered 15/9, 2019 at 21:27 Comment(5)
Your Timer does not execute on the edt, that might be the problem. Can you change it to Timer timer = new Timer(40, (e) -> { SwingUtilities.invokeLater(() -> updateScene()); } ); and see if the problem persists?Chillon
@Chillon A Swing timer is always executed in EDT by default. I do not think using invokeLater will help.Sade
@Chillon Thanks for the reply! I implemented your suggestion but the problem is still there. From what I can tell, it looks like nothing changed.Revelationist
Would also suggest grabbing a simple JavaFX example and seeing if that has the same issueMotorboat
This is a case where calling paintImmediately may be justified. You can avoid runaway resource consumption by calling setCoalesce(true) on the timer; as you already know, you should consider the truly elapsed time in update anyway. In such a setup, you may even invoke setIgnoreRepaint(true) on the component when starting the animation as you know there will be updates in reasonable time.Staciastacie
S
1

I guess you probably cannot solve your issue by enabling OpenGL, since your gpu does not support it, a possible silly workaround could be to fire a kind of event manually in each timer's iteration.

/* Update the scene every 40 milliseconds. */
final Robot robot = new Robot();
Timer timer = new Timer(40, (e) -> {
    robot.mouseRelease(0); //some event
    updateScene();
});
timer.start();

(And the only place you can Thread.sleep() in a Swing Application is inside a SwingWorker's doInBackground method. If you call it in EDT the whole GUI will freeze since events cannot take place.)

Sade answered 15/9, 2019 at 21:50 Comment(4)
Your solution works as long as I have the game in focus, but when I switch to another window it simulates the keypresses there too (a bunch of 0s fill up any textbox I select). I need a cleaner solution. Thanks for the suggestion though!Revelationist
@Revelationist You could experiment with this. I updated my post. :)Sade
Oh hey! I looked at that sun.java2d.opengl answer you linked to, and it worked! Maybe the default rendering backend just doesn't refresh as often as OpenGL. I added the -Dsun.java2d.opengl=true to my java invocation and everything runs smooth now. If you add the details to your answer, I will accept it :-)Revelationist
@Revelationist I do not think there are any details to add related to OpenGL. I mean, it is OpenGL... plus it is explained in the link already. :) Glad that you solved it though.Sade
R
0

I ran into the same issue. The solution is quite simple. Call revalidate() immediately after you call repaint():

private void updateScene() {
            /* Update all objects in the scene. */
            square.update();

            /* Request the scene to be repainted. */
            repaint();

            revalidate(); // <-- this will now repaint as fast as you wanted it to
    }
Resort answered 18/1, 2022 at 2:27 Comment(0)
C
0

I took a look at this, and oddly enough if I use a JLabel with an ImageIcon, it seems to solve the problem.

import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JLabel;

class Game {
        public static final int screenWidth = 160;
        public static final int screenHeight = 140;
        /**
         * Create and show the GUI.
         */
        private static void createAndShowGUI() {
                /* Create the GUI. */
                JFrame frame = new JFrame("Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setResizable(false);
                GamePanel panel = new GamePanel();
                
                frame.getContentPane().add(panel.getComponent());
                frame.pack();
                /* Show the GUI. */
                frame.setVisible(true);
        }

        /**
         * Run the game.
         *
         * @param args  the list of command-line arguments
         */
        public static void main(String[] args) {
                /* Schedule the GUI to be created on the EDT. */
                SwingUtilities.invokeLater(() -> createAndShowGUI());
        }

}

/**
 * A GamePanel widget updates and shows the game scene.
 */
class GamePanel{
        private Square square;
        final BufferedImage img;
        JLabel label;
        /**
         * Create a game panel and start its update-and-draw cycle
         */
        public GamePanel() {
                img = new BufferedImage(Game.screenWidth, Game.screenHeight, BufferedImage.TYPE_INT_ARGB);
                square = new Square(0, 0, 32, 32, Square.Direction.LEFT);
                paintComponent();
                label = new JLabel(new ImageIcon(img));
                
                /* Update the scene every 40 milliseconds. */
                Timer timer = new Timer(40, (e) -> updateScene());
                timer.start();
        }

        /**
         * Paint the game scene using a graphics context.
         *
         * @param g  the graphics context
         */
        protected void paintComponent() {
            Graphics g = img.getGraphics();

            /* Clear the screen. */
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, Game.screenWidth, Game.screenHeight);

            /* Draw all objects in the scene. */
            square.draw(g);
        }

        /**
         * Update the game state.
         */
        private void updateScene() {
                /* Update all objects in the scene. */
                square.update();
                paintComponent();
                //label.setIcon(new ImageIcon(img));
                label.repaint();
        }
        public JLabel getComponent(){
            return label;
        }

}

/**
 * A Square is a game object which looks like a square.
 */
class Square {
        public static enum Direction { LEFT, RIGHT };

        private int x;
        private int y;
        private int width;
        private int height;
        private Direction direction;

        /**
         * Create a square game object.
         *
         * @param x          the square's x position
         * @param y          the square's y position
         * @param width      the square's width (in pixels)
         * @param height     the square's height (in pixels)
         * @param direction  the square's direction of movement
         */
        public Square(int x,
                      int y,
                      int width,
                      int height,
                      Direction direction) {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
                this.direction = direction;
        }

        /**
         * Draw the square using a graphics context.
         *
         * @param g  the graphics context
         */
        public void draw(Graphics g) {
                g.setColor(Color.RED);
                g.fillRect(x, y, width, height);
                g.setColor(Color.BLACK);
                g.drawRect(x, y, width, height);
        }

        /**
         * Update the square's state.
         *
         * The square slides horizontally
         * until it reaches the edge of the screen,
         * at which point it begins sliding in the
         * opposite direction.
         *
         * This should be called once per frame.
         */
        public void update() {
                if (direction == Direction.LEFT) {
                        x--;

                        if (x <= 0) {
                                direction = Direction.RIGHT;
                        }
                } else if (direction == Direction.RIGHT) {
                        x++;

                        if (x + width >= Game.screenWidth) {
                                direction = Direction.LEFT;
                        }
                }
        }
}

All I did is create a BufferedImage that is used for the JLabel image icon and it updates smoothly.

Composed answered 6/12, 2023 at 17:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.