Reputation: 6870
I am just toying around with the idea of creating a multithreaded renderer in swing for my java2d games where each thread is responsible for rendering its own swingcomponent and came up with a simple program to try and and achieve this.
*I am aware that Thread.sleep is not the preferred method but it has worked without a hitch on my single-threaded renderings that use active rendering, I haven't tested with a swingtimer but to my knowledge Thread.sleep sleeps the calling thread so that cannot be the issue.
(Problem) The program creates four panels in four threads with a bouncing ball in each but only the first created thread ever does anything, the others are simply not started at all (verifiable with sysout in run methods). So only one thread does the rendering, the others are never given a chance to run, the sysout: System.out.println("Ballbouncer: " + this + " running.");
confirms this and so does the visual (image added).
BallBouncer (runnable)
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;
}
}
}
Container
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();
}
Main
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());
}
}
Notice the ball only bouncing in the left panel (first started thread), the others are stationary at all times.
Upvotes: 0
Views: 1303
Reputation: 1152
Even after fixing the problem with using start() instead of run(), you are not now performing multithreaded painting (just because all 4 panels are now updating does not mean they are being painted in distinct threads!).
You seem to be under the apprehension that "repaint()" performs painting (aka rendering)! It does not. It simply marks the component as requiring a repaint. The repainting in fact always takes place on the swing EventDispatchThread, and each of your panels will only ever be repainted one after the other by that method. And there's nothing you can do to change that.
However, if one or more of your components is very expensive to paint there is a way that you can "cheat" Swing into seemingly doing multithreaded rendering. Any thread you like can render your (say) ExpensiveComponent to a BufferedImage. This is performed thus:
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();
Now you can override ExpensiveComponent#paint(Graphics g) so that it just draws the image on the graphics:
@Override public void paint(Graphics g) {
g.drawImage(bufferedImage, 0, 0, null);
}
(Just override paint not paintComponent() and don't call super -- assuming your component does not have child components, will be slightly more efficient than overriding paintComponent().)
Now, provided that you aren't doing anything fancy such as setting a transform (e.g. scaling) or AlphaComposite on the Graphics at some earlier point, then rendering from the BufferedImage to the Graphics should be extremely quick (a trivial operation, in some circumstances performed via your GPU).
Note that the actual rendering to the screen is still performed in a single threaded fashion by the EventDispatchThread. You can't get around that. But the heavy work is already done, and can be performed using one thread per component.
That's not quite a full solution, for example to do this efficiently you want to reuse the same BufferedImage for each doPaint operation... but then you'll have to replace the BufferedImage with a new larger or smaller one whenever your ExpensiveComponent is resized (hint: add a ComponentListener to it). You may also want to coordinate the painting of the four components (e.g. use a ThreadPoolExecutor and CompletionService, or a CyclicBarrier, or just decrement a CountdownLatch) so that repaint() is just called once on the parent component after all 4 threads have done their work. If it's a game, you probably want the doPaint threads to start rendering the next frame again immediately (no timer, no sleep(), unless you are deliberately "capping the framerate").
Though full coordination this isn't necessarily desirable. You might just want to call parentComponent.repaint(expensiveComponent.getBounds()) which marks the parent "partially dirty". That may work better if some ExpensiveComponents are a lot more expensive than others. Some trial and error required on your part to see what actually works best for you.
P.S. Re "You can't get around that". Well, technically you can force immediate painting in your own thread using paintImmediately(). But all the advice I have found is "don't". It's highly unsafe, and won't play nicely with any of your other components (may cause flickering etc.). Try it if you like but I've always stayed well clear. Painting to an image and letting the EDT draw the image itself is safe and plenty fast enough.
Upvotes: 0
Reputation: 67300
You're doing this:
public BallBouncer(List<ColoredPanel> containers) {
for (ColoredPanel p : containers) {
new BallBouncer(p).run();
}
}
Which is not the proper way to start a thread. All you're doing is running the run
method directly and sequentially. That means that the code runs in that loop in the same thread that calls the constructor.
You should read this tutorial. It explains how to use threads in Swing. Namely, how to use javax.swing.SwingWorker
and SwingUtilities.invoke*
. There is also this tutorial, which explains how to use the Swing Timer
class.
And, just for further education about threads: Here are ways to start threads in java when you're not using swing. You do not want to use these examples when you're writing a Swing application
Upvotes: 4