swing 中的多线程渲染

问题描述 投票:0回答:2

我只是想尝试为我的 java2d 游戏在 swing 中创建一个多线程渲染器,其中每个线程负责渲染自己的 swing 组件,并想出了一个简单的程序来尝试实现这一点。

*我知道 Thread.sleep 不是首选方法,但它有 在我使用的单线程渲染上工作顺利 主动渲染,我还没有使用 Swingtimer 进行测试,但对我来说 知识 Thread.sleep 使调用线程休眠,因此不能 问题。

问题)该程序在四个线程中创建四个面板,每个面板中都有一个弹跳球,但只有第一个创建的线程执行任何操作,其他线程根本没有启动(可以使用 run 方法中的 sysout 进行验证)。因此,只有一个线程进行渲染,其他线程永远没有机会运行,系统输出:

System.out.println("Ballbouncer: " + this + " running.");
证实了这一点,视觉效果也是如此(添加了图像)。

BallBouncer(可运行)

public class BallBouncer implements Runnable {
private ColoredPanel ballContainer;
private List<Ellipse2D.Double> balls;
public static final Random rnd = new Random();
private double speedX, speedY;

public BallBouncer(ColoredPanel container) {
    this.ballContainer = container;
    this.balls = new ArrayList<>();
    balls.add(container.getBall());
    this.speedX = 10 * rnd.nextDouble() - 5;
    this.speedY = 10 * rnd.nextDouble() - 5;
}

public BallBouncer(List<ColoredPanel> containers) {
    for (ColoredPanel p : containers) {
        new BallBouncer(p).run();
    }
}

@Override
public void run() {
    while (true) {
        System.out.println("Ballbouncer: " + this + " running.");
        moveBall();
        ballContainer.repaint();
        try {
            Thread.sleep(15);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private void moveBall() {
    for (Ellipse2D.Double ball : balls) {
        ball.x += speedX;
        ball.y += speedY;

        if (ball.x < 0
                || ball.x + ball.getWidth() > ballContainer.getWidth()) {
            speedX *= -1;
        }
        if (ball.y < 0
                || ball.y + ball.getHeight() > ballContainer.getHeight()) {
            speedY *= -1;
        }
    }


}

集装箱

public class ColoredPanel extends JPanel {
private Ellipse2D.Double circle;

public ColoredPanel(Color color) {
    circle = new Ellipse2D.Double(0, 0, 10, 10);
    setBackground(color);
}

public Ellipse2D.Double getCircle() {
    return circle;
}

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g.create();
    g2d.setColor(getBackground().darker());
    g2d.fill(circle);
}

@Override
@Transient
public Dimension getPreferredSize() {
    return new Dimension(400, 400);
}

public Double getBall() {
    return getCircle();
}

主要

public class ColoredPanelContainer extends JPanel {

private List<ColoredPanel> panels = new ArrayList<>();

public ColoredPanelContainer() {
    setUpPanels();
    setBackground(Color.black);
}

private void setUpPanels() {
    for (int i = 0; i < 4; i++) {
        Color color = new Color(BallBouncer.rnd.nextInt(256),
                BallBouncer.rnd.nextInt(256), BallBouncer.rnd.nextInt(256));
        panels.add(new ColoredPanel(color));
    }
    for (int i = 0; i < 4; i++) {
        add(panels.get(i));
    }
}

@Override
@Transient
public Dimension getPreferredSize() {
    return new Dimension(1000, 1000);
}

public List<ColoredPanel> getPanels() {
    return panels;
}

public static void main(String[] args) {
    JFrame frame = new JFrame();
    ColoredPanelContainer container = new ColoredPanelContainer();
    frame.getContentPane().add(container);
    frame.pack();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setVisible(true);
    new BallBouncer(container.getPanels());
}
}

请注意,球仅在左侧面板中弹跳(第一个启动的线程),其他面板始终保持静止。 enter image description here

java swing graphics2d
2个回答
4
投票

你正在这样做:

public BallBouncer(List<ColoredPanel> containers) {
    for (ColoredPanel p : containers) {
        new BallBouncer(p).run();
    }
}

这不是启动线程的正确方法。您所做的就是直接并

顺序运行
run方法。这意味着代码在调用构造函数的同一线程中的循环中运行。

您应该阅读本教程。它解释了如何在 Swing 中使用线程。即如何使用javax.swing.SwingWorker

SwingUtilities.invoke*
还有这个教程,解释了如何使用 Swing Timer
 类。 

并且,只是为了进一步了解线程:

以下是当您不使用 swing 时在 Java 中启动线程的方法当您编写 Swing 应用程序时,您不想使用这些示例


0
投票
即使在使用 start() 而不是 run() 解决了问题之后,您现在也

执行多线程绘画(仅仅因为所有 4 个面板现在都在更新,并不意味着它们正在不同的线程中进行绘画!)。

您似乎担心“repaint()”执行绘画(又名渲染)!它不是。它只是将组件标记为“需要”重新绘制。事实上,重绘总是发生在 swing EventDispatchThread 上,并且每个面板只能通过该方法一个接一个地重绘。你无法改变这一点。

但是,如果一个或多个组件的绘制成本非常昂贵,那么您可以通过一种方法“欺骗”Swing,使其看起来像是在进行多线程渲染。您喜欢的任何线程都可以将您的(例如)ExpenseComponent 渲染为 BufferedImage。这是这样执行的:

Graphics2D g2d = bufferedImage.createGraphics(); // doPaint() does what paintComponent() is doing in your example expensiveComponent.doPaint(g2d); // remember this call to repaint() does not do any painting! It just marks the component as requiring a repaint, as and when the EventDispatchThread sees fit. expensiveComponent.repaint();

现在您可以重写 ExpenseComponent#paint(Graphics g),以便它只在图形上绘制图像:
@Override public void paint(Graphics g) {
    g.drawImage(bufferedImage, 0, 0, null);
}

(只需重写paint而不是paintComponent()并且不调用super——假设你的组件没有子组件,会比重写paintComponent()稍微更有效。)
现在,

假设

您没有做任何花哨的事情,例如在早些时候在图形上设置变换(例如缩放)或AlphaComposite,那么从 BufferedImage 到图形的渲染应该非常快(对于你的 GPU)。

请注意,实际渲染到屏幕仍然由 EventDispatchThread 以单线程方式执行。你无法回避这个问题。但繁重的工作已经完成,并且可以使用每个组件一个线程来执行。

这不是一个完整的解决方案,例如,为了有效地做到这一点,您希望为每个 doPaint 操作重用相同的 BufferedImage...但是每当您的 ExpenseComponent 调整大小时,您都必须用更大或更小的新的 BufferedImage 替换 BufferedImage (提示:向其中添加一个 ComponentListener)。您可能还想协调四个组件的绘制(例如,使用 ThreadPoolExecutor 和 CompletionService,或 CyclicBarrier,或仅递减 CountdownLatch),以便在所有 4 个线程完成其任务后,仅在父组件上调用一次 repaint()工作。如果是游戏,您可能希望 doPaint 线程立即再次开始渲染下一帧(没有计时器,没有 sleep(),除非您故意“限制帧速率”)。

虽然完全协调,但这并不是“不一定”可取的。您可能只想调用parentComponent.repaint(expenseComponent.getBounds()) 来标记父级“部分脏”。如果某些昂贵组件比其他组件贵得多,那么“可能”效果会更好。您需要进行一些尝试和错误,才能了解什么最适合您。

© www.soinside.com 2019 - 2024. All rights reserved.