0

Consider the following Swing timer :

timer = new Timer (ballSpeed, tc);

ballSpeed is initially at 10. tc is an Action Listener class that increments the x value of an object painted on the screen. The variable ballSpeed is kind of a misnomer because the lower the value, the faster the object moves.

Now I want the object's movement to look as smooth as possible. Therefore I will only increment the x value one by one in the ActionListener. That is the object should only move move pixel by pixel. I use x++ instead of x+=10. Therefore I will not modify the ball's speed this way.

Now since the first argument of Timer will only accept an integer, it doesn't give me a great deal of control over the object's speed. I can only use 10,9,8,etc. The object either moves too fast or too slow.

To summarize, millisecond precision is not sufficient.

Is there a way around this? Or is there an overall better way to implement object movement on the screen?

Vikram
  • 227
  • 2
  • 5
  • 14
  • 1
    Do you mean that millisecond precision is not sufficient? Or do you have problems rounding your float? – Guillaume Polet Nov 30 '12 at 16:44
  • Millisecond precision is not sufficient. – Vikram Nov 30 '12 at 16:46
  • From what I can tell, attempting to time in better precision than millis may not be easy. Perhaps an easier way to go about it would be to accumulate rounded off decimals, and catch up when it adds up to a whole millisecond? (ie, if the increments go by at 10.4 ms each time: Store .4ms roundoff and wait 10ms, Store another .4ms to make .8ms and wait 10 ms, Store another .4 to make 1.2, add the whole ms, and wait 11ms storing .2ms now, etc. etc.). There is a good chance, however, that even this is well outside the precision you are actually getting from your times, considering overhead. – femtoRgon Nov 30 '12 at 17:13
  • 1
    Saying that millisecond precision is not sufficient is the same as saying that a thousand hertz refresh rate would not be enough for a HD TV. In reality, 240 Hz is considered a highly upgraded refresh rate, probably overkill. I strongly suspect a problem in how your program is using the Timer, perhaps in the float to pixel conversion. I suggest producing a minimal example that e.g. just tries to move a point across a window smoothly, and post it if it does not work. – Patricia Shanahan Nov 30 '12 at 17:14
  • if this is a game I'd suggest doing all the moving in the main loop. – Jimmt Nov 30 '12 at 17:38
  • More on platform-dependent timer resolution cited [here](http://stackoverflow.com/a/9356021/230513). – trashgod Nov 30 '12 at 17:56

2 Answers2

2

A javax.swing.Timer does not have sufficient accuracy or precision to generate the smooth graphics you are trying to achieve. Furthermore, the swing timer is affected by anything else in the swing event queue:

Second, its automatic thread sharing means that you don't have to take special steps to avoid spawning too many threads. Instead, your timer uses the same thread used to make cursors blink, tool tips appear, and so on.

If you don't want to use some media framework or use APIs that describe the motion of objects (rather than actually moving the objects), you should use the swing timer as a way to schedule the next computation, but determine the time elapsed since last computation by looking at the difference between System.nanoTime() now and the nanoTime during the last computation.

Using this approach, you will have more jaggered but more correct animation on underpowered machines.

Dilum Ranatunga
  • 13,254
  • 3
  • 41
  • 52
2

OK, so if you really want to go for the nanosecond, at the end of this answer is a way to do it using a ScheduledThreadPool. But this is just pure madness, this may lead to tons of problems and the result is disappointing. I would really not go down that road.

With 50Hz (ie, 50 refresh per second) you should be able to achieve a decent result. The thing is that I would rather drop the assumption that you can move only pixel per pixel and link your ball speed to your move increases.

Here is an example (just drag the slider to see the result):

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class TestAnimation2 {

    private static final int NB_OF_IMAGES_PER_SECOND = 50;
    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;

    private static final int MIN = 0;
    private static final int MAX = 100;

    private double speed = convert(50);

    private double dx;
    private double dy;

    private double x = WIDTH / 2;
    private double y = HEIGHT / 2;

    private JFrame frame;
    private CirclePanel circle;
    private Runnable job;

    private long lastMove = System.currentTimeMillis();

    protected void initUI() throws MalformedURLException {
        frame = new JFrame(TestAnimation2.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(null);
        circle = new CirclePanel();
        circle.setSize(20, 20);
        frame.add(circle);
        frame.setSize(WIDTH, HEIGHT);
        dx = speed;
        dy = speed;
        final JSlider slider = new JSlider(MIN, MAX);
        slider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                speed = convert(slider.getValue());
                if (dx > 0) {
                    dx = speed;
                } else {
                    dx = -speed;
                }
                if (dy > 0) {
                    dy = speed;
                } else {
                    dy = -speed;
                }

            }
        });
        slider.setValue(50);
        slider.setLocation(0, 0);
        slider.setSize(slider.getPreferredSize());
        frame.add(slider);
        frame.setVisible(true);
        Timer t = new Timer(1000 / NB_OF_IMAGES_PER_SECOND, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                move();
            }
        });
        t.start();
    }

    protected double convert(double sliderValue) {
        return sliderValue + 1;
    }

    protected void move() {
                x += dx;
                y += dy;
                if (x + circle.getWidth() > frame.getContentPane().getWidth()) {
                    x = frame.getContentPane().getWidth() - circle.getWidth();
                    dx = -speed;
                } else if (x < 0) {
                    x = 0;
                    dx = speed;
                }
                if (y + circle.getHeight() > frame.getContentPane().getHeight()) {
                    y = frame.getContentPane().getHeight() - circle.getHeight();
                    dy = -speed;
                } else if (y < 0) {
                    y = 0;
                    dy = speed;
                }
                circle.setLocation((int) x, (int) y);
                circle.repaint();
    }

    public static class CirclePanel extends JPanel {

        public CirclePanel() {
            super();
            setOpaque(false);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(Color.RED);
            g.fillOval(0, 0, getWidth(), getHeight());
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
            UnsupportedLookAndFeelException {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                try {
                    new TestAnimation2().initUI();
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

Here is an example using a scheduled thread pool (very dangerous and disappointing)

import java.awt.Color;
import java.awt.Graphics;
import java.net.MalformedURLException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class TestAnimation2 {
    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;
    private static final int SLOWEST_RATE = 10000000;
    private static final int FASTEST_RATE = 1000;
    private static final int RANGE = SLOWEST_RATE - FASTEST_RATE;

    private static final int MIN = 0;
    private static final int MAX = 100;

    private double dx;
    private double dy;

    private double x = WIDTH / 2;
    private double y = HEIGHT / 2;

    private volatile long delay = convert(50);
    private JFrame frame;
    private CirclePanel circle;
    private Runnable job;

    private long lastMove = System.currentTimeMillis();

    protected void initUI() throws MalformedURLException {
        frame = new JFrame(TestAnimation2.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(null);
        circle = new CirclePanel();
        circle.setSize(20, 20);
        frame.add(circle);
        frame.setSize(WIDTH, HEIGHT);
        dx = 1;
        dy = 1;
        final ScheduledExecutorService sheduledThreadPool = Executors.newScheduledThreadPool(1);
        final JSlider slider = new JSlider(MIN, MAX);
        slider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                delay = convert(slider.getValue());
            }
        });
        slider.setValue(50);
        slider.setLocation(0, 0);
        slider.setSize(slider.getPreferredSize());
        frame.add(slider);
        frame.setVisible(true);
        job = new Runnable() {

            @Override
            public void run() {
                move();
                sheduledThreadPool.schedule(job, delay, TimeUnit.NANOSECONDS);
            }
        };
        sheduledThreadPool.schedule(job, delay, TimeUnit.NANOSECONDS);
    }

    protected long convert(float sliderValue) {
        return (long) (SLOWEST_RATE - sliderValue / (MAX - MIN) * RANGE);
    }

    protected void move() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                System.err.println("Ellapsed " + (System.currentTimeMillis() - lastMove) + " delay is " + (double) delay / 1000000 + " ms");
                x += dx;
                y += dy;
                if (x + circle.getWidth() > frame.getContentPane().getWidth()) {
                    x = frame.getContentPane().getWidth() - circle.getWidth();
                    dx = -1;
                } else if (x < 0) {
                    x = 0;
                    dx = 1;
                }
                if (y + circle.getHeight() > frame.getContentPane().getHeight()) {
                    y = frame.getContentPane().getHeight() - circle.getHeight();
                    dy = -1;
                } else if (y < 0) {
                    y = 0;
                    dy = 1;
                }
                circle.setLocation((int) x, (int) y);
                frame.repaint();
                lastMove = System.currentTimeMillis();
            }
        });
    }

    public static class CirclePanel extends JPanel {

        public CirclePanel() {
            super();
            setOpaque(false);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(Color.RED);
            g.fillOval(0, 0, getWidth(), getHeight());
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
            UnsupportedLookAndFeelException {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                try {
                    new TestAnimation2().initUI();
                } catch (MalformedURLException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        });
    }
}
Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117