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