1

我正在为 JBox2D 模拟创建一个图形前端。模拟以增量方式运行,并且在更新之间,应该绘制模拟的内容。类似于游戏,除了没有输入。

我只需要几何图元来绘制 JBox2D 模拟。这个 API 似乎是最简单的选择,但它的设计有点令人困惑。

目前,我有一个名为Window扩展的类JFrame,其中包含另一个名为的类作为成员Renderer。该类Window仅初始化自身并提供一个updateDisplay()方法(由主循环调用),该updateDisplay(objects)方法调用Renderer. 我自己制作了这两种方法,它们的唯一目的是调用repaint().Renderer

JPanel应该这样使用吗?还是我应该使用一些更复杂的动画方法(例如在某些后端线程中涉及事件和/或时间间隔)?

4

3 回答 3

3

如果您想按设定的时间间隔安排更新,请javax.swing.Timer为其提供 Swing 集成服务。Timer定期在 EDT 上运行其任务,没有显式循环。(显式循环会阻止 EDT 处理事件,这会冻结 UI。我在这里更深入地解释了这一点。)

最终在 Swing 中进行任何类型的绘画,你仍然会做两件事:

  1. 压倒paintComponent一切来做你的画。
  2. 根据需要调用repaint以请求使您的绘图可见。(Swing 通常只在需要时重新绘制,例如当其他程序的窗口越过 Swing 组件的顶部时。)

如果你正在做这两件事,你可能做对了。Swing 并没有真正的高级动画 API。它的设计主要考虑了绘图 GUI 组件。它当然可以做一些好事情,但是您将不得不像您正在做的那样大部分从头开始编写组件。

AWT 和 Swing 中的绘画涵盖了一些“幕后”的东西,如果您没有将其标记为书签的话。

您可能会查看 JavaFX。我个人对它了解不多,但它应该更适合动画。

作为某种优化,可以做的一件事是在单独的图像上绘制,然后将图像绘制到paintComponent. 如果绘画时间很长,这尤其有用:系统可以安排重新绘画,这样当它发生时,它就会受到更多控制。

如果您不绘制图像,那么您需要使用对象构建模型,并且每次都在内部绘制所有对象paintComponent


这是绘制到图像的示例:

import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;

/**
 * Holding left-click draws, and
 * right-clicking cycles the color.
 */
class PaintAnyTime {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new PaintAnyTime();
            }
        });
    }

    Color[]    colors = {Color.red, Color.blue, Color.black};
    int  currentColor = 0;
    BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
    Graphics2D  imgG2 = img.createGraphics();

    JFrame frame = new JFrame("Paint Any Time");
    JPanel panel = new JPanel() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Creating a copy of the Graphics
            // so any reconfiguration we do on
            // it doesn't interfere with what
            // Swing is doing.
            Graphics2D g2 = (Graphics2D) g.create();
            // Drawing the image.
            int w = img.getWidth();
            int h = img.getHeight();
            g2.drawImage(img, 0, 0, w, h, null);
            // Drawing a swatch.
            Color color = colors[currentColor];
            g2.setColor(color);
            g2.fillRect(0, 0, 16, 16);
            g2.setColor(Color.black);
            g2.drawRect(-1, -1, 17, 17);
            // At the end, we dispose the
            // Graphics copy we've created
            g2.dispose();
        }
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(img.getWidth(), img.getHeight());
        }
    };

    MouseAdapter drawer = new MouseAdapter() {
        boolean rButtonDown;
        Point prev;

        @Override
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                prev = e.getPoint();
            }
            if (SwingUtilities.isRightMouseButton(e) && !rButtonDown) {
                // (This just behaves a little better
                // than using the mouseClicked event.)
                rButtonDown  = true;
                currentColor = (currentColor + 1) % colors.length;
                panel.repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (prev != null) {
                Point  next = e.getPoint();
                Color color = colors[currentColor];
                // We can safely paint to the
                // image any time we want to.
                imgG2.setColor(color);
                imgG2.drawLine(prev.x, prev.y, next.x, next.y);
                // We just need to repaint the
                // panel to make sure the
                // changes are visible
                // immediately.
                panel.repaint();
                prev = next;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                prev = null;
            }
            if (SwingUtilities.isRightMouseButton(e)) {
                rButtonDown = false;
            }
        }
    };

    PaintAnyTime() {
        // RenderingHints let you specify
        // options such as antialiasing.
        imgG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        imgG2.setStroke(new BasicStroke(3));
        //
        panel.setBackground(Color.white);
        panel.addMouseListener(drawer);
        panel.addMouseMotionListener(drawer);
        Cursor cursor =
            Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
        panel.setCursor(cursor);
        frame.setContentPane(panel);
        frame.pack();
        frame.setResizable(false);
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

PaintAnyTime 截图


如果例程运行时间很长并且可能同时发生重绘,则也可以使用双缓冲。对与显示的图像分开的图像进行绘图。然后,当绘制例程完成时,图像参考被交换,因此更新是无缝的。

例如,您通常应该对游戏使用双缓冲。双缓冲可防止图像以部分状态显示。例如,如果您为游戏循环使用后台线程(而不是 a Timer)并且游戏正在绘制时发生重绘,则可能会发生这种情况。如果没有双缓冲,这种情况会导致闪烁或撕裂。

Swing 组件默认是双缓冲的,所以如果你的所有绘图都发生在 EDT 上,你不需要自己编写双缓冲逻辑。Swing 已经做到了。

这是一个更复杂的示例,它显示了一个长时间运行的任务和一个缓冲区交换:

import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.event.*;
import java.util.*;

/**
 * Left-click to spawn a new background
 * painting task.
 */
class DoubleBuffer {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new DoubleBuffer();
            }
        });
    }

    final int  width = 640;
    final int height = 480;

    BufferedImage createCompatibleImage() {
        GraphicsConfiguration gc =
            GraphicsEnvironment
                .getLocalGraphicsEnvironment()
                .getDefaultScreenDevice()
                .getDefaultConfiguration();
        // createCompatibleImage creates an image that is
        // optimized for the display device.
        // See http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsConfiguration.html#createCompatibleImage-int-int-int-
        return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
    }

    // The front image is the one which is
    // displayed in the panel.
    BufferedImage front = createCompatibleImage();
    // The back image is the one that gets
    // painted to.
    BufferedImage  back = createCompatibleImage();
    boolean  isPainting = false;

    final JFrame frame = new JFrame("Double Buffer");
    final JPanel panel = new JPanel() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Scaling the image to fit the panel.
            Dimension actualSize = getSize();
            int w = actualSize.width;
            int h = actualSize.height;
            g.drawImage(front, 0, 0, w, h, null);
        }
    };

    final MouseAdapter onClick = new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
            if (!isPainting) {
                isPainting = true;
                new PaintTask(e.getPoint()).execute();
            }
        }
    };

    DoubleBuffer() {
        panel.setPreferredSize(new Dimension(width, height));
        panel.setBackground(Color.WHITE);
        panel.addMouseListener(onClick);
        frame.setContentPane(panel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    void swap() {
        BufferedImage temp = front;
        front = back;
        back = temp;
    }

    class PaintTask extends SwingWorker<Void, Void> {
        final Point pt;

        PaintTask(Point pt) {
            this.pt = pt;
        }

        @Override
        public Void doInBackground() {
            Random rand = new Random();

            synchronized(DoubleBuffer.this) {
                Graphics2D g2 = back.createGraphics();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                    RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                                    RenderingHints.VALUE_STROKE_PURE);
                g2.setBackground(new Color(0, true));
                g2.clearRect(0, 0, width, height);
                // (This computes pow(2, rand.nextInt(3) + 7).)
                int  depth = 1 << ( rand.nextInt(3) + 7 );
                float  hue = rand.nextInt(depth);
                int radius = 1;
                int c;
                // This loop just draws concentric circles,
                // starting from the inside and extending
                // outwards until it hits the outside of
                // the image.
                do {
                    int rgb = Color.HSBtoRGB(hue / depth, 1, 1);
                    g2.setColor(new Color(rgb));

                    int x = pt.x - radius;
                    int y = pt.y - radius;
                    int d = radius * 2;

                    g2.drawOval(x, y, d, d);

                    ++radius;
                    ++hue;
                    c = (int) (radius * Math.cos(Math.PI / 4));
                } while (
                       (0 <= pt.x - c) || (pt.x + c < width)
                    || (0 <= pt.y - c) || (pt.y + c < height)
                );

                g2.dispose();
                back.flush();

                return (Void) null;
            }
        }

        @Override
        public void done() {
            // done() is completed on the EDT,
            // so for this small program, this
            // is the only place where synchronization
            // is necessary.
            // paintComponent will see the swap
            // happen the next time it is called.
            synchronized(DoubleBuffer.this) {
                swap();
            }

            isPainting = false;
            panel.repaint();
        }
    }
}

绘画例程只是打算绘制需要很长时间的垃圾:

双缓冲截图

于 2014-01-21T18:27:04.863 回答
2

对于紧密耦合的仿真,javax.swing.Timer是一个不错的选择。让计时器的侦听器调用您的实现paintComponent(),如此处和此处引用的示例所示

对于松散耦合的模拟,让模型在 a 的后台线程中演化,如此SwingWorker所示。在适合您的模拟时调用。publish()

选择部分取决于模拟的性质和模型的占空比。

于 2014-01-21T18:30:41.287 回答
1

为什么不直接使用来自测试平台的东西?它已经完成了一切。只需使用 JPanel、控制器和调试绘图。它使用Java 2D绘图。

有关进行缓冲渲染的 JPanel,请参见此处: https ://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/TestPanelJ2D.java

在这里进行调试绘制: https ://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/DebugDrawJ2D.java

查看 TestbedMain.java 文件以了解正常的测试平台是如何启动的,并删除您不需要的内容 :)

编辑:免责声明:我维护 jbox2d

这是测试平台框架的包:https ://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework

TestbedMain.java 在 j2d 文件夹中,这里: https ://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d

于 2014-01-22T18:46:55.507 回答