我正在尝试用Java和Swing制作2D游戏,窗口刷新太慢。但如果我移动鼠标或按键,窗口就会以应有的速度刷新!
这是一个 GIF,展示了仅当我移动鼠标时窗口如何快速刷新。
为什么窗口刷新那么慢?为什么鼠标和键盘会影响其刷新率?如果可能的话,如何让它一直快速刷新?
我使用 javax.swing.Timer 每 1/25 秒更新一次游戏状态,之后它在游戏面板上调用 repaint() 来重绘场景。
我知道计时器可能并不总是准确延迟 1/25 秒。
我也明白调用 repaint() 只是请求尽快重新绘制窗口,并不会立即重新绘制窗口。
我的显卡不支持 OpenGL 2+ 或硬件加速 3D 图形,这就是为什么我不使用 libgdx 或 JME 进行游戏开发。
这个 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;
}
}
}
}
我猜你可能无法通过启用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 将冻结,因为事件无法发生。)
我遇到了同样的问题。解决方案非常简单。拨打
revalidate()
后立即拨打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
}
我看了一下这个,奇怪的是,如果我使用带有 ImageIcon 的 JLabel,它似乎可以解决问题。
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;
}
}
}
}
我所做的就是创建一个用于 JLabel 图像图标的 BufferedImage,并且它更新顺利。