9

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;
                        }
                }
        }
}
James Z
  • 12,209
  • 10
  • 24
  • 44
  • 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? – k5_ Sep 15 '19 at 21:44
  • 3
    @k5_ A Swing timer is always executed in EDT by default. I do not think using `invokeLater` will help. – George Z. Sep 15 '19 at 21:46
  • @k5_ Thanks for the reply! I implemented your suggestion but the problem is still there. From what I can tell, it looks like nothing changed. –  Sep 15 '19 at 21:51
  • Would also suggest grabbing a simple JavaFX example and seeing if that has the same issue – MadProgrammer Sep 15 '19 at 22:25
  • This is a case where calling [`paintImmediately`](https://docs.oracle.com/javase/8/docs/api/javax/swing/JComponent.html#paintImmediately-int-int-int-int-) 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)`](https://docs.oracle.com/javase/8/docs/api/java/awt/Component.html#setIgnoreRepaint-boolean-) on the component when starting the animation as you know there will be updates in reasonable time. – Holger Sep 16 '19 at 10:50

2 Answers2

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.)

George Z.
  • 6,643
  • 4
  • 27
  • 47
  • 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! –  Sep 15 '19 at 22:05
  • 1
    @user12071097 You could experiment with this. I updated my post. :) – George Z. Sep 15 '19 at 22:09
  • 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 :-) –  Sep 16 '19 at 03:56
  • @user12071097 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. – George Z. Sep 16 '19 at 08:52
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
    }