1

I made a program to display interference patterns for light waves. I did this by using the paint method on a JPanel to draw 2 sources and then drawing concentric circles around them. This would be double slit interference, so I allowed one of the sources to move around to experiment with the slit width.

The problem is that when I run this, my computer says it is using 80% of my CPU! There's really not much to it. Circle in the middle, circles around it, and it moves. My code follows.

main class:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;


public class main {
static final int UP = -1;
static final int DOWN = 1;
static final int RIGHT = 1;
static final int LEFT = -1;
static final int NOMOVEMENT = 0;


static int verticalMovement = NOMOVEMENT;
static int horizontalMovement = NOMOVEMENT;
public static void main(String[] args) {
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

    Point s1 = new Point((int)(screenSize.getWidth()/3), 50);
    Point s2 = new Point((int)(2*screenSize.getWidth()/3), 50);

    JFrame frame = new JFrame("Physics Frame");
    frame.setPreferredSize(screenSize);

    PhysicsPane pane = new PhysicsPane(screenSize, 15, s1, s2);
    pane.setPreferredSize(screenSize);

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.add(pane);
    frame.pack();

    Timer time = new Timer(1000 / 20, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            switch (verticalMovement){
                case UP:
                    s2.changeY(UP);
                    break;
                case DOWN:
                    s2.changeY(DOWN);
                    break;
                default:
                    break;
            }
            switch (horizontalMovement){
                case RIGHT:
                    s2.changeX(RIGHT);
                    break;
                case LEFT:
                    s2.changeX(LEFT);
                    break;
                default:
                    break;
            }
            pane.repaint();
        }
    });

    frame.addKeyListener(new KeyListener() {
        @Override
        public void keyTyped(KeyEvent e) {
        }

        @Override
        public void keyPressed(KeyEvent e) {
            if(e.getKeyCode()==37){
                horizontalMovement = LEFT;
            } else if (e.getKeyCode()==38){
                verticalMovement = UP;
            } else if (e.getKeyCode()==39){
                horizontalMovement = RIGHT;
            } else if (e.getKeyCode()==40){
                verticalMovement = DOWN;
            }

            if(e.getKeyChar()=='a'){
                pane.setWaveLength(2);
            }
            if(e.getKeyChar()=='s'){
                pane.setWaveLength(-2);
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
            switch (e.getKeyCode()){
                case 37:
                    horizontalMovement = NOMOVEMENT;
                    break;
                case 38:
                    verticalMovement = NOMOVEMENT;
                    break;
                case 39:
                    horizontalMovement = NOMOVEMENT;
                    break;
                case 40:
                    verticalMovement = NOMOVEMENT;
                    break;
            }
        }
    });
    frame.setVisible(true);
    time.start();

}
}

Panel class. If there's an inefficiency with the drawing method, it would be here.

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

public class PhysicsPane extends JPanel {
private Dimension size;
private Point[] pointArray = new Point[2];
private double waveLength;
public PhysicsPane(Dimension size, double wavelength, Point source1, Point source2) {
    this.size = size;
    pointArray[0] = source1;
    pointArray[1] = source2;
    setPreferredSize(size);
    this.waveLength = wavelength;
}

@Override
public void paintComponent(Graphics g){
    for (int i = 0; i < 2; i++) {
        g.setColor(Color.black);
        double x = this.pointArray[i].getX();
        double y = this.pointArray[i].getY();
        g.fillOval((int)x, (int)y, 2, 2);
        for (int j = (int)waveLength; j < 1500; j+=waveLength) {
            g.setColor(Color.red);
            g.drawOval((int)x-(j/2+1), (int)y-(j/2+1), 2 + j, 2 + j);
        }
    }
}

public void setWaveLength(double increment){
    this.waveLength+=increment;
}
}

Point Class: Really just puts x and y coordinates into one construct. Not important particularly

public class Point {
private double x;
private double y;

public Point(double x, double y) {
    this.x = x;
    this.y = y;
}

public double getX() {
    return x;
}

public void setX(double x) {
    this.x = x;
}

public double getY() {
    return y;
}

public void setY(double y) {
    this.y = y;
}

public void changeX(double dX){
    this.x+=dX;
}

public void changeY(double dY){
    this.y+=dY;
}
}

So what is wrong with my program? Why is such a simple animation consuming so much of my processor?

UPDATED CODE SECTION:

@Override
public void paintComponent(Graphics g){
    BufferedImage bi = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_RGB);
    Graphics graphics = bi.getGraphics();
    for (int i = 0; i < 2; i++) {
        graphics.setColor(Color.black);
        double x = this.pointArray[i].getX();
        double y = this.pointArray[i].getY();
        graphics.fillOval((int)x, (int)y, 2, 2);
        for (int j = (int)waveLength; j < 1500; j+=waveLength) {
            graphics.setColor(Color.red);
            graphics.drawOval((int)x-(j/2+1), (int)y-(j/2+1), 2 + j, 2 + j);
        }
    }
    g.drawImage(bi, 0, 0, new ImageObserver() {
        @Override
        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
            return false;
        }
    });
}
Chris
  • 566
  • 2
  • 7
  • 22
  • 2
    I'd discourage you from using `paint` and override `paintComponent` instead. You should also be maintaining the expected painting contract by calling the paint methods super method – MadProgrammer Feb 25 '16 at 00:20
  • I have fixed that. And while it does consume far less power, it still consumes around 1/2 of my CPU. Is this par for the course with animation? – Chris Feb 25 '16 at 00:28
  • 2
    There's a lot going on your paint code, it might be better to generate a off screen buffer (`BufferedImage`) which you update and paint that in your paint method. I'd also encourage the use of [Key Bindings](http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html) over `KeyListener`, which will solve the potentional focus related issues as well as the use of `KeyEvent.VK_LEFT` instead of 37, as it makes your intentions much easier to understand (it took me some button mashing to find key 37) – MadProgrammer Feb 25 '16 at 00:30
  • When running your code I didn't see that kind of consumption, but you have to remember that Swing uses a passive rendering algorithm, this means that paints may occur at any time for any reason, mostly without your knowledge or influence, so it might be putting in additional painting cycles. Having the problem full screen like it is, also means that there is a greater area for the API to have to try and update. Generally blitting (painting a single image) is faster than performing numerous graphic operations. In the fast – MadProgrammer Feb 25 '16 at 00:34
  • I've been able to animate 10, 000+ objects in Swing, it's not the greatest of performance, but it can be done. If you really need more control over the painting process consider having a look at [`BufferStrategy`](http://docs.oracle.com/javase/7/docs/api/java/awt/image/BufferStrategy.html) and [BufferStrategy and BufferCapabilities](https://docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html) – MadProgrammer Feb 25 '16 at 00:35
  • For reference, you might also examine the `KineticModel` cited [here](http://stackoverflow.com/a/6794843/230513). – trashgod Feb 25 '16 at 00:54
  • I updated my code. Is this what was intended in your off screen buffer? – Chris Feb 25 '16 at 03:00
  • From my own work in advanced painting in Swing, I'd imagine this is down to you not throttling updates at all. Ergo `repaint()` is being called as often as possible. If you want to control the painting mechanic I recommend setting up a threaded model that controls the rate of `repaint()`. This also gives you the advantage of targeting a particular refresh rate (i.e. 60 - 62FPS for most screens). You're still going to get moderate CPU usage though (I get up to 20% in my own code) - you're animating a scene in a similar fashion to how games work. – Gorbles Feb 25 '16 at 11:11

1 Answers1

1

The improvement that I recommend is removal of unnecessary tasks being performed, notably how your code updates the pane being drawn on, even when there aren't changes.

The following update reduced CPU usage from 12% to 0% (static frame):

Timer time = new Timer(1000 / 20, new ActionListener() {

    @Override
    public void actionPerformed(ActionEvent e) {

        boolean refresh = false;

        switch (verticalMovement) {
        case UP:
            s2.changeY(UP);
            refresh = true;
            break;
        case DOWN:
            s2.changeY(DOWN);
            refresh = true;
            break;
        default:
            break;
        }

        switch (horizontalMovement) {
        case RIGHT:
            s2.changeX(RIGHT);
            refresh = true;
            break;
        case LEFT:
            s2.changeX(LEFT);
            refresh = true;
            break;
        default:
            break;
        }

        if (refresh == true) {
            pane.repaint();
        }
    }
});
Osmund Francis
  • 771
  • 10
  • 27