4

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

c0der
  • 18,467
  • 6
  • 33
  • 65
Gipsy King
  • 186
  • 2
  • 2
  • 14
  • 3
    Consider storing all the images to be painted in a collection (`ArrayList`) , and use [Timer](https://docs.oracle.com/javase/tutorial/uiswing/misc/timer.html) to fire an [animation](https://stackoverflow.com/a/53701618/3992939) every interval. For more help post [mcve]. – c0der Jan 12 '19 at 05:09
  • `function draw() {` Huh? What language is being used here? That looks more like JavaScript than Java. – Andrew Thompson Jan 12 '19 at 16:35
  • @AndrewThompson yeah, that was an example how it was implemented by someone it in JavaScript, for more clear presenting of issue – Gipsy King Jan 12 '19 at 18:35
  • @c0der actually , using List and set coordinates as arguments and then get them , which I took from your example ,helped me. I needed easier solution. So your first example turned out useful – Gipsy King Jan 14 '19 at 18:46

3 Answers3

5

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

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();
            }
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
2

My answer is completely based on MadProgrammer's answer (A comprehensive tutorial actually).
From what I read in the post : "Every new objects reaching x = 144 should build new ones", I think the desired implementation is slightly different:

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class ImageAnimator {

    public ImageAnimator() {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Testing");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new AnimationPane());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    public static class Drawable {

        private int x;
        private final int y;
        private static final Image image = image();

        //construct with a random y value
        public Drawable(int x) {
            this(x, -1);
        }

        public Drawable(int x, int y) {
            this.x = x;
            this.y =  y < 0 ? (int) (Math.random() * (512 - 20)) : y;
        }

        public int getX() { return x;  }

        public int getY() { return y; }

        public void update() {  x--; }

        public Image getImage(){  return image; }

        public static Image image() {

            URL url = null;
            try {
                 //5.SEP.2021 replaced dead link 
                //url = new URL("https://dl1.cbsistatic.com/i/r/2017/09/24/b2320b25-27f3-4059-938c-9ee4d4e5cadf/thumbnail/32x32/707de8365496c85e90c975cec8278ff5/iconimg241979.png");
                url = new URL("https://cdn3.iconfinder.com/data/icons/softwaredemo/PNG/32x32/Circle_Green.png");
                return ImageIO.read(url);

            } catch ( IOException ex) {
                ex.printStackTrace();
                return null;
            }
        }
    }

    public class AnimationPane extends JPanel {

        private final List<Drawable> drawables;
        private static final int W = 288, H = 512, CYCLE_TIME = 5;

        public AnimationPane() {
            drawables = new ArrayList<>(2);
            drawables.add(new Drawable(W, H/4));
            drawables.add(new Drawable(W, 3*H/4));

            Timer timer = new Timer(CYCLE_TIME, e ->  animate());
            timer.start();
        }

        private void animate() {

          for (Drawable drawable : new ArrayList<>(drawables)) {

              drawable.update();
              if(drawable.getX() == W/2) {
                  drawables.add(new Drawable(W)); //random Y
              }
              if(drawable.getX() <= 0) {
                  drawables.remove(drawable);
              }
          }
          repaint();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(W, H);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Drawable drawable : drawables ) {
                g.drawImage(drawable.getImage(),drawable.getX(), drawable.getY(), null);
            }
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(()->new ImageAnimator());
    }
}

enter image description here

c0der
  • 18,467
  • 6
  • 33
  • 65
  • *"Every new objects reaching x = 144 should build new ones"* - That's actually a really good point. This was a bit of a head scratcher for me. But, assuming your right (which I think you are), creating and destroying short lived objects will put a stain on the GC engine. A better solution is to pool the objects for re-use, this will reduce the GC overhead and prevent possible "random" slow downs. Also, I'd move your update logic out of the `paintComponent` method and into the `Timer`, as painting should only paint the current state as fast as possible – MadProgrammer Jan 12 '19 at 21:44
  • @MadProgrammer I see what you mean. When I wrote it I thought about reusing `drawable` buy using a queue, but I thought it may make the code a bit more difficult to follow. I'll move the calculation out of `paintComponent`, and maybe add a version with reusable `drawable`s. – c0der Jan 13 '19 at 05:07
  • @MadProgrammer you guys are amazing . And I really thankful for your responses , but the problem is that I m just begginer and this code seems confusing for me I thought solution will be much easier , but I was wrong . But that’s okey , difficult solutions makes us stronger. I’m definitely gonna look over all code and deal with it, but it will take a while . P.s. actually I was creating copy of game Flappy Bird with two pipes which appears at the up and down . I little bit changed my issue so that it would be easier to explain :) – Gipsy King Jan 13 '19 at 06:32
1

The following solution is based on my previous answer.
I add it in response to MadProgrammer's comment: "A better solution is to pool the objects for re-use".
DrawAblesProducer produces drawable objects on-demand. It also stores surplus object, to prevent producing too many such objects.
I post it as a separate answer because the additional functionality comes with somewhat higher complexity:

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class ImageAnimator {

    private static final int W = 288, H = 512;

    public ImageAnimator() {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Testing");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new AnimationPane(new DrawAblesProducer()));
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    public class AnimationPane extends JPanel {

        private final List<Drawable> drawables;
        private static final int CYCLE_TIME = 5;
        private final DrawAblesProducer producer;

        public AnimationPane(DrawAblesProducer producer) {
            this.producer = producer;
            drawables = new ArrayList<>(2);
            drawables.add(producer.issue(W, H/4));
            drawables.add(producer.issue(W, 3*H/4));

            Timer timer = new Timer(CYCLE_TIME, e ->  animate());
            timer.start();
        }

        private void animate() {

            for (Drawable drawable : new ArrayList<>(drawables)) {
                drawable.update();
                if(drawable.getX() == W/2) {
                    drawables.add(producer.issue(W)); //random Y
                }else if(drawable.getX() <= 0) {
                    drawables.remove(drawable);
                    producer.retrn(drawable);
                }
            }
            repaint();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(W, H);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Drawable drawable : drawables ) {
                g.drawImage(drawable.getImage(),drawable.getX(), drawable.getY(), null);
            }
        }
    }

    //produces `drawable` objects on-demand. stores surplus object, to prevent producing 
    //too many such objects
    public class DrawAblesProducer {

        private final Queue<Drawable> warehouse = new LinkedList<>();

        public Drawable issue(int x){
            return issue(x, -1);
        }

        public Drawable issue(int x, int y){

            Drawable drawable = warehouse.poll();
            if(drawable != null ) {
                drawable.setX(x); drawable.setY(y);
                return  drawable;
            }
            return new Drawable(x, y);
        }

        public void retrn(Drawable drawable){
            warehouse.add(drawable);
        }
    }

    public static class Drawable {

        //made static so image is reused for all instances
        private static final Image image = image();

        private int x, y;

        //construct with a random y value
        public Drawable(int x) {
            this(x, -1);
        }

        public Drawable(int x, int y) {
            setX(x);
            setY(y);
        }

        public int getX() { return x;  }
        public void setX(int x) { this.x = x;}

        public int getY() { return y; }
        public void setY(int y) {
            this.y = y < 0 ? randomY() : y  ;
        }

        private int randomY() {
            int iHeight = image.getHeight(null);
            return iHeight + (int) (Math.random() * (H - iHeight));
        }

        public void update() {  x--; }

        public Image getImage(){  return image; }

        public static Image image() {

            URL url = null;
            try {
                //5.SEP.2021 replaced dead link 
                //url = new URL("https://dl1.cbsistatic.com/i/r/2017/09/24/b2320b25-27f3-4059-938c-9ee4d4e5cadf/thumbnail/32x32/707de8365496c85e90c975cec8278ff5/iconimg241979.png");
                url = new URL("https://cdn3.iconfinder.com/data/icons/softwaredemo/PNG/32x32/Circle_Green.png");    
                return ImageIO.read(url);

            } catch ( IOException ex) { ex.printStackTrace();   }
            return null;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(()->new ImageAnimator());
    }
}
c0der
  • 18,467
  • 6
  • 33
  • 65