Java游戏循环(绘画)冻结了我的窗口

我正在用cardLayout改变“视图”(这个类有一个JFrame变量)。 当用户点击新游戏按钮时,会发生这种情况:

 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(); } }); } } } 

游戏的循环方法和课程标题:

 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"); } } 

我正在使用paintComponent()绘画

 public void paintComponent(Graphics g) { System.out.println("paint"); ... } 

实际上它并没有画任何东西。 当我不调用loop()方法(因此它只绘制一次)所有图像都被正确绘制。 但是当我调用loop()方法时,窗口中什么也没发生。 (甚至JFrame上的关闭按钮也不起作用。)

如何解决? (当我在游戏类中创建JFrame一切正常,但现在我希望有更多的视图,所以我需要在其他类中使用JFrame 。)

谢谢。

更新有什么作用? 你可能不应该在EDT上调用game.loop() 。 你正在EDT上运行一个循环,你的重绘不会被调用,因为重绘在EDT上排队一个事件,它似乎很忙。 尝试将game.loop()移动到另一个线程

 new Thread(new Runnable() { @override public void run() { game.loop(); } }).start(); 

这样你就不会阻止EDT,而重绘仍然在EDT上执行。

前体: 事件调度线程(EDT)

Swing是单线程的。 这是什么意思?

Swing程序中的所有处理都以事件开始。 EDT是一个线程,它沿着以下几行循环处理这些事件(但更复杂):

 class EventDispatchThread extends Thread { Queue 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 eg // ActionListener.actionPerformed, // JComponent.paintComponent, // Runnable.run, // etc... } } 

调度线程通过抽象对我们隐藏:我们通常只编写监听器回调。

  • 单击按钮发布事件( 使用本机代码 ):处理事件时,在EDT上调用actionPerformed
  • 调用repaint发布事件:处理事件时,在EDT上调用paintComponent
  • 调用invokeLater发布一个事件:处理事件时, 在EDT上调用run
  • Swing中的所有内容都以事件开始。

事件任务按照发布顺序依次处理。

只有当前事件任务返回时才能处理下一个事件。 这就是为什么我们不能在EDT上有一个无限循环。 actionPerformed (或run ,如在编辑中)永远不会返回,因此repaint 绘制事件的调用但它们永远不会被处理 ,程序似乎会冻结。

这就是“阻止”EDT的意思。


在Swing程序中基本上有两种方法可以做动画:

  • 使用Thread (或SwingWorker )。

    使用线程的好处是处理是在EDT之外完成的,因此如果进行密集处理,GUI仍然可以同时更新。

  • 使用javax.swing.Timer

    使用计时器的好处是处理在EDT 完成,因此不用担心同步,并且可以安全地更改GUI组件的状态。

一般来说,如果有理由不使用计时器,我们应该只在Swing程序中使用一个线程。

对于用户来说,它们之间没有明显的区别。

您对revalidate调用向我表明您正在修改循环中组件的状态(添加,删除,更改位置等)。 这不一定是安全的EDT。 如果要修改组件的状态,则使用计时器而不是线程是一个令人信服的理由。 使用没有正确同步的线程可能会导致难以诊断的细微错误。 请参阅内存一致性错误

在某些情况下,组件上的操作是在树锁下完成的(Swing确保它们本身是线程安全的),但在某些情况下它们不是。


我们可以转换以下forms的循环:

 while( condition() ) { body(); Thread.sleep( time ); } 

进入以下forms的Timer

 new Timer(( time ), new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { if( condition() ) { body(); } else { Timer self = (Timer) evt.getSource(); self.stop(); } } }).start(); 

这是一个用线程和计时器演示动画的简单示例:

简单的动画

 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); } } } } 

将game.loop()方法调用移动到以下内容:

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

谢谢。