为什么当我移动鼠标时,我的自定义Swing组件重绘得更快?(Java)

10
我正在尝试使用Java和Swing制作2D游戏,但窗口刷新太慢了。但是,如果我移动鼠标或按键,窗口将以应有的速度刷新!这里有一个GIF,展示了当我移动鼠标时窗口如何快速刷新。

enter image description here

为什么窗口刷新速度这么慢?为什么鼠标和键盘会影响它的刷新率?如果可能的话,我该如何使其始终快速刷新?

背景信息

我使用 javax.swing.Timer 每 1/25 秒更新游戏状态,然后调用 repaint() 在游戏面板上重新绘制场景。

我知道计时器可能不总是延迟正好 1/25 秒。

我也知道调用 repaint() 只是请求立即重绘窗口,并不能立即重绘窗口。

我的显卡不支持 OpenGL 2+ 或硬件加速的 3D 图形,这就是为什么我不使用 libgdx 或 JME 进行游戏开发的原因。

系统信息

  • 操作系统:Linux Mint 19 Tara
  • JDK版本:OpenJDK 11.0.4
  • 显卡:Intel Corporation 82945G/GZ

研究

这位Stack Overflow用户描述了我遇到的同样问题,但据说作者通过在单独的计时器上重复调用repaint()方法解决了该问题。我尝试过这种方法,确实可以使窗口刷新得更快,但即便如此,速度仍然比我想要的慢。在这种情况下,晃动窗口上的鼠标仍然可以提高刷新率。因此,似乎那篇文章并没有真正解决问题。

另一位Stack Overflow用户也遇到了这个问题,但他们在游戏循环中使用了一个连续的while循环,而不是计时器。显然,这位用户通过在while循环中使用Thread.sleep()方法解决了问题。然而,我的代码使用计时器来完成延迟,所以我不知道Thread.sleep()如何解决我的问题,或者我应该把它放在哪里。

我已经阅读了使用AWT和Swing绘画,试图弄清楚是否只是我误解了重绘的概念,但是那篇文章中没有任何内容能够为我阐明这个问题。每当游戏更新时,我调用repaint(),但是窗口只有在鼠标或键盘输入发生时才会快速刷新。
我已经搜索了几天网络,试图找到答案,但是似乎没有什么帮助!
代码
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;
                        }
                }
        }
}

你的计时器似乎没有在EDT上执行,这可能是问题所在。你可以尝试将其更改为 Timer timer = new Timer(40, (e) -> { SwingUtilities.invokeLater(() -> updateScene()); } ); 并查看问题是否仍然存在? - k5_
3
默认情况下,Swing计时器始终在EDT中执行。我认为使用“invokeLater”并不能帮助解决问题。 - George Z.
@k5_ 感谢您的回复!我已经实施了您的建议,但问题仍然存在。据我所知,似乎没有任何改变。 - user12071097
建议您尝试获取一个简单的JavaFX示例,并查看是否存在相同的问题。 - MadProgrammer
这是一种情况,可以调用paintImmediately。您可以通过在计时器上调用setCoalesce(true)来避免资源的过度消耗;正如您已经知道的那样,在update中应该考虑真正经过的时间。在这样的设置中,甚至可以在启动动画时在组件上调用setIgnoreRepaint(true),因为您知道会在合理的时间内进行更新。 - Holger
2个回答

1

我猜你可能无法通过启用OpenGL来解决问题,因为你的GPU不支持它,一个可能有点傻的解决方法是在每个计时器迭代中手动触发某种事件。

/* 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();

(在Swing应用程序中,唯一可以使用 Thread.sleep() 的地方是 SwingWorker doInBackground 方法中。如果在EDT中调用它,整个GUI将会冻结,因为事件无法发生。)

你的解决方案只在我将游戏置于焦点时有效,但当我切换到另一个窗口时,它也会在那里模拟按键(一堆0填充我选择的任何文本框)。我需要一个更干净的解决方案。不过还是谢谢你的建议! - user12071097
1
@user12071097 你可以试试这个。我更新了我的帖子。 :) - George Z.
嘿!我看了你提供的sun.java2d.opengl答案,它起作用了!也许默认的渲染后端刷新频率不如OpenGL。我在我的Java调用中添加了-Dsun.java2d.opengl=true,现在一切都很顺畅。如果你把细节加入到你的答案中,我会接受它 :-) - user12071097
@user12071097 我认为关于 OpenGL 没有什么需要补充的细节了。我的意思是,它本来就是 OpenGL … 而且在链接中已经解释得很清楚了。 :) 不过很高兴你解决了这个问题。 - George Z.

0
我遇到了同样的问题。解决方案非常简单。在调用repaint()之后立即调用revalidate()
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
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接