Java:移除可运行画布组件



我已经在这款游戏上工作了一段时间了,这款游戏实际上内置了不同的游戏模式。首先,在用户丢失或想要退出后,我一直通过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上做了一些完全错误的事情,它处理诸如GraphicsCanvas之类的事情。用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。

我想继续并感谢每个人的帮助,因为这个问题真的把我难住了。

编辑(s)
当我寻求帮助时,我总是决定做进一步的测试,我看到CellDefender.switchDisplay()方法在frame.validate()行中出现了问题。我不明白为什么会出现这个问题。

根据评论中的讨论,这个问题很可能与违反Swing的"单线程规则"有关:

一旦实现了一个Swing组件,所有可能影响或依赖于该组件状态的代码都应该在事件调度线程中执行。

违反此规则的结果可能是任意奇怪的,但是来自Swing管理基础设施内部的NullPointerException s是更常见的。

在许多情况下,这个问题可以通过实用模式来解决:假设您有一个修改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");
    ...
}

但是注意,尽管这看起来很简单,似乎很容易解决某些问题,但它确实释放您从勤快检查和记录哪个方法在哪个线程上执行的责任。

最新更新