4

I am trying to learn Java, coming from a C/assembly embedded systems background. After a few weeks of learning, I thought it would be fun to try and make a game, but I am having some problems with a JPanel being repainted at an inconsistent rate.

My "game" GUI consists of a single JFrame which contains a JPanel. As you can see, the main thread for the JFrame sleeps for 30 milliseconds and then updates "game" logic and redraws the JFrame and JPanel. Each time the JPanel is redrawn, I check that it took about 30 milliseconds. As you would expect, it never takes more than about 32 milliseconds between frame redraws.

In spite of the fact that the JPanel is definitely repainted every 30 milliseconds or so, The animation in it can be very jerky. On Ubuntu 14.10, it is extremely obvious, even though the time between calls to my JPanel's repaint() are still never more than 32ms apart.

The most vexing thing is that if I uncomment the lines

//this.resize(300 + a, 300);
//a = (a == 1)?(0):1;

in SimFrame.java, everything is perfectly smooth, which implies that my computer has the capability to update the JPanel at the rate I want.

Is there some way I can force the JFrame to update at the rate I want it to without doing the absurd .resize call?

Main.java

package jdemo;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class Main
{
    public static void main(String[] args)
    {
        SimFrame s = new SimFrame();
        Thread t = new Thread(s);
        t.start();
    }
}

Sim.java

package jdemo;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.Timer;
import javax.swing.JPanel;
import javax.swing.JFrame;



public class Sim extends JPanel implements KeyListener
{
        /**
         * 
         */
        private static final long serialVersionUID = 1L;
        private int keys[] = new int[1024];

        double x = 100;
        double y = 100;
        double dx = 0;
        double dy = 0;
        double theta = 0.0;

        private int size = 0;
        private int dsize = 1;
        public Sim()
        {
                this.addKeyListener(this);
                this.setFocusable(true);
        }


        long prevmillis;
        public void paintComponent(Graphics g)
        {
                super.paintComponent(g);
                Graphics2D drawing = (Graphics2D)g;

                //clear the background.
                drawing.setColor(Color.WHITE);
                drawing.fillRect(0, 0, this.getWidth() - 1, this.getHeight() - 1);

                //System.out.printf("dt = %d\n", System.currentTimeMillis() - prevmillis);
                prevmillis = System.currentTimeMillis();

                drawing.setColor(Color.BLACK);
                drawing.drawRect(0, 0, size, size);

                drawing.setColor(Color.BLACK);
                int[] xpoints = {(int)(x + 10 * Math.cos(Math.toRadians(theta))),
                                                 (int)(x + 5 * Math.cos(Math.toRadians(theta - 150))),
                                                 (int)(x + 5 * Math.cos(Math.toRadians(theta + 150)))};
                int[] ypoints = {(int)(y + 10 * Math.sin(Math.toRadians(theta))),
                                                 (int)(y + 5 * Math.sin(Math.toRadians(theta - 150))),
                                                 (int)(y + 5 * Math.sin(Math.toRadians(theta + 150)))};
                drawing.drawPolygon(xpoints, ypoints, 3);
        }

        public void updateLogic()
        {
                if(keys[KeyEvent.VK_UP] == 1)
                {
                        size++;
                }
                else if(keys[KeyEvent.VK_DOWN] == 1)
                {
                        size--;
                }

                //update theta.
                if(keys[KeyEvent.VK_LEFT] == 1)
                {
                        theta += 5;
                }
                if(keys[KeyEvent.VK_RIGHT] == 1)
                {
                        theta -= 5;
                }
                if(theta > 360.1)
                {
                        theta -= 360;
                }
                if(theta < -0.1)
                {
                        theta += 360;
                }


                //update acceleration
                if(keys[KeyEvent.VK_SPACE] == 1)
                {
                        dx += 0.08* Math.cos(Math.toRadians(theta));
                        dy += 0.08 * Math.sin(Math.toRadians(theta));
                }

                dx *= 0.99;
                dy *= 0.99;
                //update position
                x = x + dx;
                y = y + dy;
                System.out.printf("%f, %f\n", dx, dy);


                //update size
                if(size > 150)
                {
                    dsize = -1;
                }
                if(size < 10)
                {
                    dsize = 1;
                }
                size += dsize;
        }

        @Override
        public void keyPressed(KeyEvent arg0)
        {
                // TODO Auto-generated method stub
                keys[arg0.getKeyCode()] = 1;
                System.out.printf("%d\n", arg0.getKeyCode());
        }

        @Override
        public void keyReleased(KeyEvent arg0)
        {
                // TODO Auto-generated method stub
                keys[arg0.getKeyCode()] = 0;
        }

        @Override
        public void keyTyped(KeyEvent arg0)
        {
                // TODO Auto-generated method stub

        }
}

SimFrame.java

package jdemo;


import jdemo.Sim;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class SimFrame extends JFrame implements Runnable
{
        private Sim s;
        public SimFrame()
        {
                this.s = new Sim();
                this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                this.setContentPane(this.s);
                this.pack();
                this.setLocationRelativeTo(null);
                this.setSize(200, 200);
        }

        @Override
        public void run()
        {
                int a = 0;
                this.setVisible(true);
                while(true)
                {
                        //repaint
                        s.updateLogic();
                        this.getContentPane().revalidate();
                        this.repaint();
                        //this.resize(300 + a, 300);
                        //a = (a == 1)?(0):1;

                        try
                        {
                                Thread.sleep(30);
                        }
                        catch(InterruptedException e)
                        {
                                System.out.printf("failed");
                                break;
                        }
                }
        }
}

Thank you very much.

mKorbel
  • 109,525
  • 20
  • 134
  • 319
John M
  • 1,484
  • 1
  • 14
  • 27
  • why do you want to resize the panel when you repaint every30ms – Asura May 30 '15 at 19:29
  • If I do that, the animations in the frame become smooth. – John M May 30 '15 at 19:32
  • you start at size 200,200 but your update set it to 300,300 that is strange in itself.You could set the preferred size of the Jframe instead(not in the loop) – Asura May 30 '15 at 19:39
  • @Asura I don't think you understand. If I remove the lines that resize the JFrame (which I understand are extremely unorthodox), the animation in my JPanel becomes jerky. I don't want those lines to be there, but they are the only way so far I have been able to make the animation in my JPanel acceptably smooth. I want to know how to keep the animation smooth when I remove the calls to JFrame.resize(). Sorry if my question is unclear. – John M May 30 '15 at 19:43
  • The animation can be made smooth with Toolkit sync, but I don't know *why* it is not in this case without that. The frame rate is not abnormal for animations. – kiheru May 30 '15 at 19:45
  • Thank you for your response @kiheru, but I believe that I already tried this after finding something similar on google. Adding the line Toolkit.getDefaultToolkit().sync(); After this.repaint() in SimFrame.java doesn't help at all. – John M May 30 '15 at 19:49
  • Curiously enough it does make a difference on my system (both on your code and on a simplified (and thread safe) version I wrote for testing). – kiheru May 30 '15 at 19:59
  • @kiheru Thanks for checking. You mean the .sync() method helps on your system? the Toolkit.getDefaultToolkit.sync() helps a little on my Windows, but on my Ubuntu, it does nothing. Moreover, on both systems, the constant resize-on-each-redraw strategy performs the best by far. – John M May 30 '15 at 20:04
  • Despite me not seeing and frame-rate issues, I have a hard time understanding what you're trying to achieve with sending the thread to sleep. The painting is done best if left unrestricted and consecutive calls to it will be combined anyway so there is not much point in controlling it so tightly. – user1803551 May 31 '15 at 06:14
  • @user1803551 I have that so that I don't eat up too much processor power with my application. Maybe I will experiment with taking it out. What do you mean when you say "consecutive calls to it will be combined anyway"? Can you please elaborate? – John M Jun 01 '15 at 06:02
  • Look [here](http://www.oracle.com/technetwork/java/painting-140037.html) in the "Note" under "Paint Processing" and then under "Synchronous Painting". You might need to read some parts before if you want to understand more. In any case, I see now that you don't delay the painting anyway as I initially thought. – user1803551 Jun 01 '15 at 09:21
  • 1
    maybe related. https://stackoverflow.com/questions/19480076/java-animation-stutters-when-not-moving-mouse-cursor – phil294 Jan 27 '18 at 21:15

1 Answers1

1

Try this and if it works I'll transform it into a true answer (I just can't know if it will work on your system better than your current code):

public class SimFrame extends JFrame {

    public SimFrame() {

        setContentPane(new Sim());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private class Sim extends JPanel {

        double x = 100;
        double y = 100;
        double dx = 0;
        double dy = 0;
        double theta = 0.0;
        private int size = 0;
        private int dsize = 1;

        public Sim() {

            addKeyListener(new Controller());
            setFocusable(true);
            setBackground(Color.WHITE);

            new Timer(30, new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {

                    dx *= 0.99;
                    dy *= 0.99;

                    // update position
                    x = x + dx;
                    y = y + dy;

                    // update size
                    if (size > 150)
                        dsize = -1;
                    if (size < 10)
                        dsize = 1;
                    size += dsize;

                    // update theta.
                    if (theta > 360.1) {
                        theta -= 360;
                    }
                    if (theta < -0.1) {
                        theta += 360;
                    }

                    repaint();
                }
            }).start();
        }

        @Override
        public Dimension getPreferredSize() {

            return new Dimension(200, 200);
        };

        @Override
        public void paintComponent(Graphics g) {

            super.paintComponent(g);
            Graphics2D drawing = (Graphics2D) g;

            drawing.setColor(Color.BLACK);
            drawing.drawRect(0, 0, size, size);

            drawing.setColor(Color.BLACK);
            int[] xpoints = {(int) (x + 10 * Math.cos(Math.toRadians(theta))), (int) (x + 5 * Math.cos(Math.toRadians(theta - 150))),
                                (int) (x + 5 * Math.cos(Math.toRadians(theta + 150)))};
            int[] ypoints = {(int) (y + 10 * Math.sin(Math.toRadians(theta))), (int) (y + 5 * Math.sin(Math.toRadians(theta - 150))),
                                (int) (y + 5 * Math.sin(Math.toRadians(theta + 150)))};
            drawing.drawPolygon(xpoints, ypoints, 3);
        }

        private class Controller extends KeyAdapter {

            @Override
            public void keyPressed(KeyEvent evt) {

                switch (evt.getKeyCode()) {
                    case KeyEvent.VK_UP:
                        size++;
                        break;
                    case KeyEvent.VK_DOWN:
                        size--;
                        break;
                    case KeyEvent.VK_LEFT:
                        theta += 5;
                        break;
                    case KeyEvent.VK_RIGHT:
                        theta -= 5;
                        break;
                    case KeyEvent.VK_SPACE:
                        dx += 0.08 * Math.cos(Math.toRadians(theta));
                        dy += 0.08 * Math.sin(Math.toRadians(theta));
                        break;
                }
            }
        }
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {

                new SimFrame();
            }
        });
    }
}

And yes, I know there's the OS's delay on holding a key, we'll get to it if it works.


Edit:

Simpler animation:

public class CopyOfSimFrame extends JFrame {

    public CopyOfSimFrame() {

        setContentPane(new Sim());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private class Sim extends JPanel {

        private int size = 0;
        private int dsize = 1;

        public Sim() {

            setBackground(Color.WHITE);

            new Timer(30, new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {

                    // update size
                    if (size >= 150)
                        dsize = -1;
                    if (size <= 0)
                        dsize = 1;
                    size += dsize;

                    repaint();
                }
            }).start();
        }

        @Override
        public Dimension getPreferredSize() {

            return new Dimension(150, 150);
        };

        @Override
        public void paintComponent(Graphics g) {

            super.paintComponent(g);
            g.setColor(Color.BLACK);
            g.drawRect(0, 0, size, size);
        }
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {

                new CopyOfSimFrame();
            }
        });
    }
}
Community
  • 1
  • 1
user1803551
  • 12,965
  • 5
  • 47
  • 74
  • I really appreciate that you took the time to answer my question with this great SSCCE. Unfortunately, it didn't help at all. In fact, your example was somewhat laggier on my Ubuntu 14.10 system. – John M Jun 01 '15 at 06:00
  • @johnny_boy Then it seems your problem is not related to the code. I added a simpler animation below, Try it. I would also try other simple animation examples found on this site (just search). If they don't work well for you, then again, it's not code related. – user1803551 Jun 01 '15 at 09:41
  • Thanks. I guess there is either an issue with my Ubuntu install or with the Ubuntu version of the jvm. – John M Jun 07 '15 at 20:14
  • @johnny_boy If it works well on one platform it should work well on others - Java is OS independent. – user1803551 Jun 07 '15 at 21:04