Цикл игры 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
в другом классе.)
Благодарю.
3 ответа
Что делает обновление? Вы, вероятно, не должны звонить game.loop()
на EDT. Вы выполняете цикл в EDT, ваш перерисовка никогда не будет вызываться, так как перерисовка ставит в очередь событие в EDT, и это кажется довольно занятым. Попробуй переехать game.loop()
в другой поток
new Thread(new Runnable() {
@override
public void run() {
game.loop();
}
}).start();
Таким образом, вы не будете блокировать EDT, в то время как перерисовка все еще должна выполняться на EDT.
Предшественник: поток рассылки событий (EDT).
Качели однопоточные. Что это значит?
Вся обработка в программе Swing начинается с события. EDT - это поток, который обрабатывает эти события в цикле по следующим направлениям (но более сложным):
class EventDispatchThread extends Thread {
Queue<AWTEvent> 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 e.g.
// ActionListener.actionPerformed,
// JComponent.paintComponent,
// Runnable.run,
// etc...
}
}
Поток диспетчеризации скрыт от нас через абстракцию: мы обычно пишем только обратные вызовы слушателя.
- Нажатие кнопки публикует событие ( в собственном коде): когда событие обрабатывается,
actionPerformed
называется на EDT. - призвание
repaint
отправляет событие: когда событие обрабатывается,paintComponent
называется на EDT. - призвание
invokeLater
отправляет событие: когда событие обрабатывается,run
называется на EDT. - Все в Swing начинается с события.
Задачи события обрабатываются в последовательности, в порядке их размещения.
Следующее событие может быть обработано только тогда, когда текущая задача события возвращается. Вот почему у нас не может быть бесконечного цикла на EDT. actionPerformed
(или же run
, как в вашем редактировании) никогда не возвращается, поэтому вызовы repaint
публикуйте события рисования, но они никогда не обрабатываются, и программа, кажется, зависает.
Вот что значит "блокировать" EDT.
Есть два основных способа сделать анимацию в программе Swing:
Использовать
Thread
(илиSwingWorker
).Преимущество использования потока заключается в том, что обработка выполняется вне EDT, поэтому при интенсивной обработке графический интерфейс пользователя может обновляться одновременно.
Использовать
javax.swing.Timer
,Преимущество использования таймера заключается в том, что обработка выполняется в EDT, поэтому нет необходимости беспокоиться о синхронизации и безопасно изменять состояние компонентов графического интерфейса.
Вообще говоря, мы должны использовать поток в программе Swing только в том случае, если есть причина не использовать таймер.
Для пользователя между ними нет заметной разницы.
Ваш звонок в revalidate
указывает на то, что вы изменяете состояние компонентов в цикле (добавление, удаление, изменение местоположения и т. д.). Это не обязательно безопасно делать от EDT. Если вы изменяете состояние компонентов, это веская причина использовать таймер, а не поток. Использование потока без надлежащей синхронизации может привести к незначительным ошибкам, которые трудно диагностировать. См. Ошибки согласованности памяти.
В некоторых случаях операции над компонентом выполняются под блокировкой дерева (Swing самостоятельно гарантирует, что они поточно-ориентированы), но в некоторых случаях это не так.
Мы можем повернуть цикл следующей формы:
while( condition() ) {
body();
Thread.sleep( time );
}
в к 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()
}
});
Благодарю.