Question

I was working on a simple "Bouncing Ball"-Animation in Java. The idea is that it initally spawns a single ball moving in a straight line until hitting the panel border, which causes it to bounce off as you would expect. You can then spawn additional balls at position x,y with mouseclicks. So far so good.

My problem is that each ball starts its own thread, and each thread individually draws into the panel at their own intervals, causing the panel to flicker like crazy. I know that such problems can be solved by implementing double buffering, which I've read about, but never quite used myself.

I was wondering about how one would go about using double buffering here and if having many threads painting at the same time can be an issue (or conversely, even the norm)?

Thanks a lot in advance!

Here's the code:

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

class MyCanvas extends JPanel 
{
    MyCanvas() 
    {
        setBackground(Color.white);
        setForeground(Color.black);
    }

    public void paintComponent(Graphics g) 
    {
        super.paintComponent(g);      
    }

    public Dimension getMinimumSize()
    {
        return new Dimension(300,300);
    }

    public Dimension getPreferredSize()
    {
        return getMinimumSize();
    } 
}

public class BouncingBalls extends JFrame     // main class
{
    MyCanvas m_gamefield;

    public BouncingBalls() 
    {
        setLayout(new BorderLayout());
        m_gamefield = new MyCanvas();
        add("Center",m_gamefield);

        m_gamefield.addMouseListener(new MeinMausAdapter());

        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    public void letsgo()
    {
        Ball first = new Ball(m_gamefield,200,50);
        first.start();
    }

    class MeinMausAdapter extends MouseAdapter 
    {
        public void mousePressed(MouseEvent e) 
        {
            Ball next = new Ball(m_gamefield,e.getX(),e.getY()); 
            next.start(); 
        }
    }

    public static void main(String[] args)
    {
        BouncingBalls test = new BouncingBalls();
        test.setVisible(true);
        test.pack();
        test.letsgo();
    }
}

class Ball extends Thread 
{
    JPanel m_display;
    int m_xPos,m_yPos;
    int m_dx = 2;         // Steps into direction x or y
    int m_dy = 2; 

    Ball(JPanel c,int x,int y) 
    {
        m_display = c;
        m_xPos = x;
        m_yPos = y;
    }

    public void run() 
    {
        paintBall();         // Paint at starting position

        while(isInterrupted() == false) 
        {
            moveBall();

            try 
            { 
                sleep(20);
            } 

            catch(InterruptedException e) 
            {
                return;
            }
        }
    }

    void paintBall() 
    {
        Graphics g = m_display.getGraphics();
        g.fillOval(m_xPos, m_yPos, 20, 20);
        g.dispose(); 
    }

    void moveBall() 
    {
        int xNew, yNew;
        Dimension m;
        Graphics g;

        g = m_display.getGraphics();
        m = m_display.getSize();
        xNew = m_xPos + m_dx;
        yNew = m_yPos + m_dy;

        // Collision detection with borders, "bouncing off":

        if(xNew < 0)
        {
            xNew = 0;
            m_dx = -m_dx;
        }

        if(xNew + 20 >= m.width) 
        {
            xNew = m.width - 20;
            m_dx = -m_dx;
        }

        if(yNew < 0) 
        {
            yNew = 0;
            m_dy = -m_dy;
        }

        if(yNew + 20 >= m.height)
        {
            yNew = m.height - 20;
            m_dy = -m_dy; 
        }

        g.setColor(m_display.getBackground());                   // Erases last position by 
        g.fillRect(m_xPos-2, m_yPos-2, m_xPos+22, m_yPos+22);    // painting over it in white

        m_xPos = xNew;
        m_yPos = yNew;
        paintBall();           // paint new position of Ball
        g.dispose();
    }
}
Was it helpful?

Solution

Don't worry about double buffering when painting with Swing JComponents. They're double buffered by default.

You should, instead of creating each Ball on a different Thread, implement a Swing Timer for the animation. See more at How to Use Swing timers. You can see a good example here where Ball objects are added to a List of Balls and presents at different intervals.

Other Notes

  • Never use getGraphics of your components. All painting should be done within the Graphics context passed to the paintComponent method. I see you have the method in place. Use it. You can have a draw method in your Ball class that take a Graphics argument, and call that method from within the paintComponent method, passing to it the Graphics context. Example can also be seen in the link above.

You can see more examples here and here and here and here and here and here.

OTHER TIPS

Thanks to peeskillet's excellent references, I've changed the code around a bit by using Swing timers. It's a lot shorter now and forfeits the use of multithreading completely. Also, due to calculating all of the ball positions before actually drawing them (in a single sweeping repaint() as opposed to many smaller ones), the flickering has stopped.

I'm still a bit curious why it is considered bad form to use getGraphics(), though. Does it always lead to flickering (which I had imagined could be removed with an additional layer of of double buffering)? And doesn't paintComponent() become rather bloated in more complex animations if it directs every single act of painting? I'm still fairly new to this, if anybody is wondering.

Here's the new code for those interested:

import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;

public class BouncingBalls extends JFrame     // main class
{
    MyCanvas m_gamefield;
    public ArrayList<Ball> balls;
    public Timer timer = null;

    public BouncingBalls() 
    {
            setLayout(new BorderLayout());
        m_gamefield = new MyCanvas();
            add("Center",m_gamefield);

            balls = new ArrayList<Ball>();

            timer = new Timer(30, new ActionListener() 
        {
                    public void actionPerformed(ActionEvent e) 
                    {
                        for (Ball b : balls) 
                        {
                                b.move();
                            }
                            repaint();
                        }
                });

            m_gamefield.addMouseListener(new MeinMausAdapter());

            setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    class MeinMausAdapter extends MouseAdapter 
    {
            public void mousePressed(MouseEvent e) 
            {
                balls.add(new Ball(m_gamefield,e.getX(),e.getY()));
            }
    }

    class MyCanvas extends JPanel 
    {
        MyCanvas() 
        {
            setBackground(Color.white);
                setForeground(Color.black);
        }

        public void paintComponent(Graphics g) 
        {
                super.paintComponent(g);

                for (Ball b : balls)
                {
                    b.draw(g);
                }                     
        }

        public Dimension getMinimumSize()
        {
            return new Dimension(300,300);
        }

        public Dimension getPreferredSize()
        {
            return getMinimumSize();
        } 
    }

    public void letsgo()
    {
        balls.add(new Ball(m_gamefield,200,50));
        timer.start();  
        }

        public static void main(String[] args)
    {
        BouncingBalls test = new BouncingBalls();
        test.setVisible(true);
        test.pack();
        test.letsgo();
    }         
}

class Ball
{
    JPanel m_display;
    int m_xPos,m_yPos;
    int m_dx = 2;         // Steps into direction x or y 
    int m_dy = 2; 

    Ball(JPanel c,int x,int y) 
    {
            m_display = c;
            m_xPos = x;
            m_yPos = y;
    }

    void draw(Graphics g) 
    {
            g.fillOval(m_xPos, m_yPos, 20, 20);
    }

    void move() 
    {
            int xNeu, yNeu;
            Dimension m;

            m = m_display.getSize();
        xNeu = m_xPos + m_dx;
            yNeu = m_yPos + m_dy;


        // Collision detection with borders, "bouncing off":

            if(xNeu < 0)
            {
                xNeu = 0;
                m_dx = -m_dx;
            }

            if(xNeu + 20 >= m.width) 
            {
                xNeu = m.width - 20;
                m_dx = -m_dx; 
            }

            if(yNeu < 0) 
            {
                yNeu = 0;
                m_dy = -m_dy;       
            }

            if(yNeu + 20 >= m.height) 
            {
                yNeu = m.height - 20;
                m_dy = -m_dy; 
            }

            m_xPos = xNeu;
            m_yPos = yNeu;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top