Java: удаление компонента Runnable Canvas

1

Я уже давно работаю над этой игрой, которая на самом деле имеет разные игровые режимы, встроенные в нее. Сначала я обрабатывал выполнение, просто 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 and 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(). Я не понимаю, почему это вызывает проблему.

Теги:
runnable
swing
awt
java-canvas

1 ответ

3
Лучший ответ

Согласно обсуждению в комментариях, проблема, скорее всего, связана с нарушением "правила одиночной нити" Swing:

Как только компонент Swing был реализован, весь код, который может повлиять или зависит от состояния этого компонента, должен выполняться в потоке диспетчеризации событий.

Результаты нарушения этого правила могут быть произвольно нечетными, но NullPointerException от глубоко внутри инфраструктуры управления Swing являются одними из наиболее распространенных.

Во многих случаях эту проблему можно решить с помощью прагматичного шаблона: предположим, что у вас есть метод, который изменяет компоненты Swing:

void modifySwingComponents()
{
    someComponent.add(someOtherComponent);
    someComponent.remove(somethingElse);
    someTextComponent.setText("Text");
    ...
}

Затем вы можете легко проверить, не нарушено ли правило Single Thread, вставив что-то вроде

System.out.println(Thread.currentThread());

в этом методе. Он должен всегда печатать Thread[AWT-EventQueue-0,6,main] (или аналогичный, указывая, что метод выполняется в Thread Dispatch Thread). Кроме того, вы можете напрямую запрашивать

System.out.println(SwingUtilities.isEventDispatchThread());

Если метод вызывается из потока, который не является событием Dispatch Thread (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");
    ...
}

Но помните, что, хотя это выглядит просто и может показаться легко разрешить некоторые проблемы, оно НЕ освобождает вас от обязанности тщательно проверять и документировать, какой метод выполняется на каком потоке.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню