Java: удаление компонента Runnable Canvas
Я уже некоторое время работаю над этой игрой, в которую встроены различные режимы игры. Сначала я занимался исполнением, просто exiting the program
после того, как пользователь проиграл или хочет выйти. Поскольку не только раздражает необходимость заново открывать программу, а когда пользователи не понимают, почему она закрылась, у меня есть причина, чтобы после потери или желания выйти, они вернулись в главное меню.
Проблема, которая появляется в это время, состоит в том, что я использую swing
в основной части моего дизайна игры. Это включает в себя не только главное меню, но и другие меню, и даже частично с игрой. Swing
используется для интерактивности кнопок и других основных функций. Так что теперь, когда я перехожу к возвращению в главное меню и все остальное, мне пришлось переписать в основном всю базу rendering
и переключение между окнами.
Поскольку я переписываю render
метод игры, я решил сделать StateRenderer
учебный класс. Исходя из этого, он будет обрабатывать и решать, нужно ли вообще его обрабатывать. Так в пределах run()
Метод, я поставил строку кода, которая проверяет, нужно ли вообще отображать его в состоянии меню.
@Override
public void run() {
long lastTime = System.nanoTime();
long timer = System.currentTimeMillis();
final double ns = BILLION / UPDATE_RATE;
double delta = 0;
int updates = 0, frames = 0;
while (running) {
// right here I am checking the state for it
GameState state = CellDefender.getGameState();
if (state == GameState.MAIN_MENU || state == GameState.STORE_MENU || state == GameState.SETTINGS_MENU) continue;
long now = System.nanoTime();
delta += (now - lastTime) / ns;
lastTime = now;
while (delta >= 1) {
update();
updates++;
delta--;
}
render();
frames++;
if (System.currentTimeMillis() - timer >= 1000) {
while (System.currentTimeMillis() - timer >= 1000) // while idling it builds up much, and makes it less annoying when debugging
timer += 1000;
System.out.println("UPS: " + updates + ", FPS: " + frames);
updates = 0;
frames = 0;
}
}
stop();
}
Теперь, это прекрасно работает, когда я решаю переключиться из главного меню в реальный игровой режим, но если я проиграю в этом режиме или хочу выйти в главное, я получаю эту неприятную ошибку, которую я понятия не имею, как я когда-нибудь исправит это:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: component argument pData
at sun.java2d.windows.GDIWindowSurfaceData.initOps(Native Method)
at sun.java2d.windows.GDIWindowSurfaceData.<init>(Unknown Source)
at sun.java2d.windows.GDIWindowSurfaceData.createData(Unknown Source)
at sun.java2d.d3d.D3DScreenUpdateManager.getGdiSurface(Unknown Source)
at sun.java2d.d3d.D3DScreenUpdateManager.createGraphics(Unknown Source)
at sun.awt.windows.WComponentPeer.getGraphics(Unknown Source)
at java.awt.Component.getGraphics(Unknown Source)
at sun.awt.RepaintArea.paint(Unknown Source)
at sun.awt.windows.WComponentPeer.handleEvent(Unknown Source)
at java.awt.Component.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
at java.awt.EventQueue.access$200(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue.dispatchEvent(Unknown Source)
at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.run(Unknown Source)
Единственное, что я понимаю, это то, что я делаю что-то совершенно не так с AWT, который имеет дело с такими вещами, как Graphics
а также Canvas
, Вероятно, было бы ужасной идеей просто поглотить ошибку с помощью метода try..catch, но опять же я не знаю, где она вызвана.
Чтобы узнать подробнее о том, как я переключаюсь, перейдите от моего главного меню к реальному GameMode:
private void initListeners() {
btnRegular.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setGameState(GameState.REGULAR);
renderer.switchDisplay(RegularMode.class);
}
});
// more code is here, but useless for now
}
мой renderer
мой StateRenderer
который предназначен для обработки всего рендеринга в игре, когда вы на самом деле на экране, который имеет Canvas
элемент, и отслеживается GameState
, Теперь я покажу вам, что бросается в renderer.switchDisplay(Class class)
метод.
public void switchDisplay(Class<? extends GameMode> mode) {
if (mode == RegularMode.class) {
currentMode = new RegularMode(size);
setPreferredSize(size);
screen = new Screen(size.width, size.height);
panel.add(currentMode.getFunctionBar(), BorderLayout.NORTH);
panel.add(currentMode.getScoreBar(), BorderLayout.SOUTH);
// -- extra stuff that is similar --
} else return;
JFrame frame = CellDefender.getFrame();
frame.remove(CellDefender.getMainPanel());
panel.add(this, BorderLayout.CENTER);
frame.add(panel);
frame.validate();
frame.repaint();
frame.pack();
currentMode.initialize();
requestFocus();
}
Это может быть несколько неэффективно, но все это, похоже, работает нормально. Теперь, чтобы погрузиться, когда все переключается обратно в главное меню, которое выбрасывает все ошибки!
Это код, который запускается и напрямую вызывает ошибку:
public static void switchDisplay() {
setGameState(GameState.MAIN_MENU); // Switched the game state, should stop looping.
frame.setContentPane(panel);
frame.validate();
frame.repaint();
frame.pack();
}
Это, конечно, дает мне полное ощущение, что ошибка лежит где-то в моем StateRenderer
класс, а более конкретно все, что относится к run()
метод. Часть, которую я уже обработал цикл рендеринга.
Резюме
Поэтому у меня возникла проблема при переключении с панели с Canvas
компонент, который реализует Runnable
, В моем коде я решил проблему с рендерингом, когда у него нет видимого Canvas
или когда GameState
не тот, который для игры, чтобы сделать. Однако при переключении с холста, который в данный момент отображается и обновляется, на меню, в котором это не выполняется, возникает исключение NullPointerException.
Я хотел бы пойти дальше и поблагодарить всех и каждого за вашу помощь, потому что этот вопрос действительно поставил меня в тупик.
Edit (s)
При дальнейшем тестировании, которое я всегда решаю сделать, когда обращаюсь за помощью, я вижу, что проблема возникает в CellDefender.switchDisplay()
метод на линии frame.validate()
, Я не понимаю, почему это вызывает проблему.
1 ответ
Согласно обсуждению в комментариях, проблема, скорее всего, связана с нарушением Swing "Правило однопоточности":
После реализации компонента Swing весь код, который может влиять или зависеть от состояния этого компонента, должен выполняться в потоке диспетчеризации событий.
Результаты нарушения этого правила могут быть произвольно странными, но NullPointerException
Глубоко внутри инфраструктуры управления Swing являются одними из наиболее распространенных.
Во многих случаях эта проблема может быть решена с помощью прагматического шаблона: предположим, у вас есть метод, который изменяет компоненты Swing:
void modifySwingComponents()
{
someComponent.add(someOtherComponent);
someComponent.remove(somethingElse);
someTextComponent.setText("Text");
...
}
Затем вы можете легко проверить, нарушено ли правило единой нити, вставив что-то вроде
System.out.println(Thread.currentThread());
в этом методе. Всегда должен печатать Thread[AWT-EventQueue-0,6,main]
(или аналогичный, указывающий, что метод выполняется в потоке диспетчеризации событий). Кроме того, вы можете напрямую запросить
System.out.println(SwingUtilities.isEventDispatchThread());
Если метод вызывается из потока, который не является потоком обработки событий (EDT), вы можете "обернуть" этот метод в Runnable, который вы поместили в очередь событий:
void modifySwingComponents()
{
if (SwingUtilities.isEventDispatchThread())
{
// We're on the right thread - direcly call the method
modifySwingComponentsOnEDT();
}
else
{
// Put the task to execute the method in the event queue,
// so that it will be executed on the EDT as soon as possible
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
modifySwingComponentsOnEDT();
}
});
}
}
// Always called on the EDT!
void modifySwingComponentsOnEDT()
{
someComponent.add(someOtherComponent);
someComponent.remove(somethingElse);
someTextComponent.setText("Text");
...
}
Но ОБРАТИТЕ ВНИМАНИЕ, что, хотя это выглядит просто и может показаться, что легко решает определенные проблемы, это НЕ освобождает вас от обязанности тщательно проверять и документировать, какой метод выполняется в каком потоке.