Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating endless number of objects in JPanel and draw them through PaintComponent in Java

I have one dilemma , how to realize application. I have JPanel with width 288 and height 512, then I created two objects ( images ) and drew them through paintComponent using coordinates

 drawImage (Image1,288,128,this) ;
 drawImage (Image2, 288, 384, this);

. They are decrementing simultaneously in the X axis and when it reaches x = 144 , new (same) images should be drawn at the coordinates β€˜( x = 288 , y = (int)Math.random()* 512 )’ and begin decrement as well as first ones should still decrements. And this process should be endless. Every new objects reaching x = 144 should build new ones . I tried to create ArrayList with adding coordinates in it

ArrayList arrayX = new ArrayList(); 
arrayX.add(288)
arrayY.add((int) Math.random()* 512 )

and then extract values through

array.get()

But that was unsuccessfully. I saw video where man did it using JavaScript through the array

var position = []
position = ({
X : 288
Y : 256
 })

And then implemented through the loop like this

 function draw() {

 for (int i = 0; i < position.length; i++ ){
 cvs.drawImage(Image1,position[i].x , position[i].y)
 cvs.drawImage(Image2,position[i].x , position[i].y + 50)

 position [i] .x - -;
 if(position[i].x == 128)
 position.push({
 X : 288
 Y : Math.floor(Math.random()*512 })
 })
 }
 }

I don’t know how to do this in Java. May be I should use array too to keep variables with coordinates , or arraylist but in different way. Help me please . Thanks in advance

like image 790
Gipsy King Avatar asked Jan 27 '23 21:01

Gipsy King


1 Answers

Conceptually the idea is simple enough, the problem is, Swing is signal thread and NOT thread safe.

See Concurrency in Swing for more details.

This means you can run a long running or blocking operation (like a never ending loop) inside the Event Dispatching Thread, but also, you shouldn't update the UI (or properties the UI depends on) from outside the context of the EDT.

While there are a number of possible solutions to the problem, the simplest is probably to use a Swing Timer, which provides a means to schedule a delay safely (that won't block the EDT) and which will trigger it's updates within the context of the EDT, allowing you to update the UI from within it.

See How to Use Swing Timers for more details.

Now, because you're in a OO language, you should leverage the power it provides, to me, this means encapsulation.

You have a image, you want drawn at a specific location, but whose location change be changed based on some rules, this just screams Plain Old Java Old (POJO)

Normally, I'd start with a interface to describe the basic properties and operations, but for brevity, I've jumped straight for a class...

public class Drawable {

    private int x, y;
    private Color color;

    public Drawable(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public Color getColor() {
        return color;
    }

    public void update() {
        x--;
        if (x <= 144) {
            reset();
        }
    }

    protected void reset() {
        x = 288;
        y = (int) (Math.random() * (512 - 20));
    }

    public void paint(Graphics2D g2d) {
        Graphics2D copy = (Graphics2D) g2d.create();
        copy.translate(getX(), getY());
        copy.setColor(getColor());
        copy.drawOval(0, 0, 20, 20);
        copy.dispose();
    }
}

But wait, you say, it's using Color instead of image!? Yes, I didn't have any small images at hand, besides, I need to leave you something to do ;)

Now, the animation is a sequence of updating and painting repeatedly until a desired state is reached.

In this case, you don't care about the end state so much, so you can just keep it running.

The "update" cycle is handled by a Swing Timer, which loops over a List of Drawable objects, calls their update methods and then schedules a repaint, which triggers the JPanels paintComponent where by the Drawable objects are painted, simple 😝...

public class TestPane extends JPanel {

    private List<Drawable> drawables;

    public TestPane() {
        drawables = new ArrayList<>(2);
        drawables.add(new Drawable(288, 128, Color.RED));
        drawables.add(new Drawable(288, 384, Color.RED));

        Timer timer = new Timer(5, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (Drawable drawable : drawables) {
                    drawable.update();
                }
                repaint();
            }
        });
        timer.start();
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(288, 512);
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (Drawable drawable : drawables) {
            Graphics2D g2d = (Graphics2D) g.create();
            drawable.paint(g2d);
            g2d.dispose();
        }
    }

}

Putting it altogether - runnable example...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class Drawable {

        private int x, y;
        private Color color;

        public Drawable(int x, int y, Color color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public Color getColor() {
            return color;
        }

        public void update() {
            x--;
            if (x <= 144) {
                reset();
            }
        }

        protected void reset() {
            x = 288;
            y = (int) (Math.random() * (512 - 20));
        }

        public void paint(Graphics2D g2d) {
            Graphics2D copy = (Graphics2D) g2d.create();
            copy.translate(getX(), getY());
            copy.setColor(getColor());
            copy.drawOval(0, 0, 20, 20);
            copy.dispose();
        }
    }

    public class TestPane extends JPanel {

        private List<Drawable> drawables;

        public TestPane() {
            drawables = new ArrayList<>(2);
            drawables.add(new Drawable(288, 128, Color.RED));
            drawables.add(new Drawable(288, 384, Color.RED));

            Timer timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    for (Drawable drawable : drawables) {
                        drawable.update();
                    }
                    repaint();
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(288, 512);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Drawable drawable : drawables) {
                Graphics2D g2d = (Graphics2D) g.create();
                drawable.paint(g2d);
                g2d.dispose();
            }
        }

    }

}

"Is there a simpler solution"? Yes, of course, I always go to the hardest possible way to solve a problem first πŸ€ͺ. First, good animation is hard. Seriously. I've been playing around with this sought of thing more nearly 20 years, making a good animation engine which is flexible to meet all the possible needs it might be put to is near impossible mission, especially in a framework which isn't really designed for it.

If you don't belief me, you could have a look at

  • How do I change Swing Timer Delay inside actionPerformed()
  • How can I fade out or fade in by command JPanel, its components and its color

which are just a couple of examples how complicated animation can be

Sorry, you'd be amazed how often I get asked "can it be simpler" when it comes to animation ;)

"Every new objects reaching x = 144 should build new ones

So, apparently I may be confused about this particular point. If this means "adding new objects after reaching 144" then this raises some new issues. The primary issue is one over GC overhead, which cause slow downs in the animation. Sure, we're only dealing with about 4-6 objects, but it's one of those things which can come back to byte you if you're not careful.

So I took the above example and made some modifications to the update cycle. This adds a reusePool where old objects are placed and can be re-used, reducing the GC overhead of repeatedly creating and destroying short lived objects.

The decaying List simply ensures that once an object passes the swanPoint, it won't be consider for re-spawning new objects. Sure you could put a flag on the POJO itself, but I don't think this is part of the POJOs responsibility

public TestPane() {
    drawables = new ArrayList<>(2);
    reusePool = new ArrayList<>(2);
    decaying = new ArrayList<>(2);

    timer = new Timer(5, new ActionListener() {
        private List<Drawable> spawned = new ArrayList<>(5);

        @Override
        public void actionPerformed(ActionEvent e) {
            spawned.clear();
            Iterator<Drawable> it = drawables.iterator();
            int swapnPoint = getWidth() / 2;
            while (it.hasNext()) {
                Drawable drawable = it.next();
                drawable.update();

                if (drawable.getX() > 0 && drawable.getX() < swapnPoint) {
                    if (!decaying.contains(drawable)) {
                        decaying.add(drawable);
                        Drawable newDrawable = null;
                        if (reusePool.isEmpty()) {
                            newDrawable = new Drawable(
                                            getWidth() - 20,
                                            randomVerticalPosition(),
                                            randomColor());
                        } else {
                            newDrawable = reusePool.remove(0);
                            newDrawable.reset(getWidth() - 20,
                                            randomVerticalPosition(),
                                            randomColor());
                        }
                        spawned.add(newDrawable);
                    }
                } else if (drawable.getX() <= -20) {
                    System.out.println("Pop");
                    it.remove();
                    decaying.remove(drawable);
                    reusePool.add(drawable);
                }
            }
            drawables.addAll(spawned);

            repaint();
        }
    });
}

This will now allow objects to travel the whole width of the width, spawning new objects as they pass the half way point. Once they pass beyond the visual range of the view, they will be placed into the reuse List so they can be reused again when new objects are required.

Runnable example...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                TestPane testPane = new TestPane();
                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(testPane);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);

                EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        testPane.start();
                    }
                });
            }
        });
    }

    public class Drawable {

        private int x, y;
        private Color color;

        public Drawable(int x, int y, Color color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public Color getColor() {
            return color;
        }

        public void update() {
            x--;
        }

        protected void reset(int x, int y, Color color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }

        public void paint(Graphics2D g2d) {
            Graphics2D copy = (Graphics2D) g2d.create();
            copy.translate(getX(), getY());
            copy.setColor(getColor());
            copy.fillOval(0, 0, 20, 20);
            copy.setColor(Color.BLACK);
            copy.drawOval(0, 0, 20, 20);
            copy.dispose();
        }
    }

    public class TestPane extends JPanel {

        private List<Drawable> drawables;
        private List<Drawable> decaying;
        private List<Drawable> reusePool;

        private Color[] colors = new Color[]{Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.YELLOW};
        private Random rnd = new Random();

        private Timer timer;

        public TestPane() {
            drawables = new ArrayList<>(2);
            reusePool = new ArrayList<>(2);
            decaying = new ArrayList<>(2);

            timer = new Timer(40, new ActionListener() {
                private List<Drawable> spawned = new ArrayList<>(5);

                @Override
                public void actionPerformed(ActionEvent e) {
                    spawned.clear();
                    Iterator<Drawable> it = drawables.iterator();
                    int swapnPoint = getWidth() / 2;
                    while (it.hasNext()) {
                        Drawable drawable = it.next();
                        drawable.update();

                        if (drawable.getX() > 0 && drawable.getX() < swapnPoint) {
                            if (!decaying.contains(drawable)) {
                                decaying.add(drawable);
                                Drawable newDrawable = null;
                                if (reusePool.isEmpty()) {
                                    System.out.println("New");
                                    newDrawable = new Drawable(
                                                    getWidth() - 20,
                                                    randomVerticalPosition(),
                                                    randomColor());
                                } else {
                                    System.out.println("Reuse");
                                    newDrawable = reusePool.remove(0);
                                    newDrawable.reset(getWidth() - 20,
                                                    randomVerticalPosition(),
                                                    randomColor());
                                }
                                spawned.add(newDrawable);
                            }
                        } else if (drawable.getX() <= -20) {
                            System.out.println("Pop");
                            it.remove();
                            decaying.remove(drawable);
                            reusePool.add(drawable);
                        }
                    }
                    drawables.addAll(spawned);

                    repaint();
                }
            });
        }

        public void start() {
            drawables.add(new Drawable(getWidth(), 128, randomColor()));
            drawables.add(new Drawable(getWidth(), 384, randomColor()));
            timer.start();
        }

        protected int randomVerticalPosition() {
            return rnd.nextInt(getHeight() - 20);
        }

        protected Color randomColor() {
            return colors[rnd.nextInt(colors.length - 1)];
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(288, 512);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Drawable drawable : drawables) {
                Graphics2D g2d = (Graphics2D) g.create();
                drawable.paint(g2d);
                g2d.dispose();
            }
        }

    }

}
like image 103
MadProgrammer Avatar answered Jan 31 '23 18:01

MadProgrammer