0

I am trying to create a 3D game engine. I am using the triangle rasterizing technique so I have a list of lines (that make up triangles) in my CanvasPanel class and each time that paintComponent() is called it goes through all the lines and displays them on the screen. The problem is in my main loop, since I want to remove all lines in order to update the scene but when I call it it's being called while the paintComponent() method is still running so I get a ConcurrentModificationException. I tried having a loop going that checks if it has finished drawing and only then clear the lines list but for some reason it works only when adding a print statement, otherwise it stops working after a few seconds. What should I do to make sure the timing is right?

Relevant code in Main:

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

public class Main
{
    public static int width = 1280, height = 720;
    
    public static void main(String[] args)
    {
        JFrame window = new JFrame("3D Game");
        CanvasPanel canvas = new CanvasPanel(width, height);
        
        window.setLayout(new FlowLayout());
        window.getContentPane().add(canvas);
        window.pack();
        window.setVisible(true);
        window.setResizable(false);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // Some irrelevant calculations
        
        while (true) // Main loop
        {
            // Some irrelevant calculations
            
            for (float[][] triangle: cubeMesh)
            {
                // Here I'm adding lines to the lines list.
            }

            canvas.repaint(); // update canvas
            
            // My attempt at making sure that clearing the lines list happens only after drawing
            while (true)
            {
                if (!canvas.isDrawing()}
                {
                    // Only works if I print something
                    break;
                }
            }

            canvas.clearLines(); // here is the problematic line
        }
    }
    
  
}

Relevant code in CanvasPanel:

public class CanvasPanel extends JPanel
{
    BufferedImage canvas;
    JLabel label;
    private ArrayList<Line> lines;
    private boolean drawing;
    
    public CanvasPanel(int width, int height)
    {
        canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        label = new JLabel(new ImageIcon(canvas));
        lines = new ArrayList<>();
        add(label);
        drawing = false;
    }

    public boolean isDrawing()
    {
        return drawing;
    }

    public void clearLines()
    {
        lines.clear();
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        drawing = true;
        super.paintComponent(g);
        
        g.setColor(Color.black);
        g.fillRect(0, 0, getWidth(), getHeight());
        
        for (Line line: lines)
        {
            g.setColor(line.color);
            g.drawLine(line.x1, line.y1, line.x2, line.y2);
        }
        drawing = false;
    }

    public void addLine(int x1, int y1, int x2, int y2, Color color)
    {
        lines.add(new Line(x1, y1, x2, y2, color));
    }
    
ScndPwa
  • 19
  • 4
  • I'm not understanding the question. Why would you want to immediately clear the drawing after it is painted? Normally you just add all the data to the ArrayList and the drawing will be painted. Then if you want to clear the painting you have a "Clear" button or something to clear the ArrayList. In any case I see no need for the "while true" logic. – camickr Dec 18 '21 at 19:05
  • If you need to dynamically update the ArrayList then the code must be invoked on the `Event Dispatch Thread (EDT)`. Currently your ArrayList is NOT created on the EDT. Read the Swing tutorial on [Concurrency](https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html) for more information. Maybe you need to use the `SwingWorker` to continually publish results? – camickr Dec 18 '21 at 19:08
  • I don't want to clear the screen, just the data so new data can be calculated and replace it (the while true is so the scene will change over time, each time it loops it's a new frame). Basically every frame I want to calculate the lines, draw them and then remove them from the list. – ScndPwa Dec 18 '21 at 19:10
  • 1
    Check out: https://stackoverflow.com/a/64196256/131872 for an animation of the Bubble Sort. Each iteration resets the ArrayList. You should not be using a loop. Use a Timer to schedule the change in data. – camickr Dec 18 '21 at 19:12
  • Okay, I'll read more about this. Thanks. – ScndPwa Dec 18 '21 at 19:21

1 Answers1

1

First, go read Performing Custom Painting and Painting in AWT and Swing to get a better understanding of how painting works in Swing.

Swing is lazy and makes use of a passive rendering workflow. This means that painting is done when the system decides it needs to be done. You can "request" a paint pass via the RepaintManager (normal via calling repaint on the component)

What this does is, if the RepaintManager agrees, places a "paint" event on the Event Dispatching Queue, which is, at some point in the future picked up by the Event Dispatching Thread and a paint pass is executed (this may be consolidated so that multiple components are painted in one pass).

There are two problems you're facing:

  1. Painting can occur for any number of reasons, many of which you don't control and you're notified by
  2. There's no reliable way to determine if or when a paint pass will/has occurred.

Now, add in the issue that Swing is not thread safe and you run into a mountain of other issues.

See Concurrency in Swing for more details.

So, what's the answer?

Well, you could use Worker Threads and SwingWorker or a Swing Timer to synchronise the updates depending on your needs, but honestly, I don't think making use of Swing's paint process is what you really need.

Another consideration would be use a backing buffer of your own. That is, use two or more BufferedImages onto which you draw the "state" and then pass these to the canvas to be drawn at some point in the future.

However, I would suggest taking control of the painting process directly yourself. Instead, have a look at BufferStrategy and BufferCapabilities (and the JavaDocs because it has a nice example).

This will provide you with direct control over the painting process so you can decide when and how it should be painted.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366