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

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

Другие вопросы по тегам