开发者

Swing active rendering efficiency or how to combine active rendering with gui widgets

Continuing from a previous question, I keep searching for the optimal way to combine active rendering with textfields in Java. I tried several options, using BufferStrategy, VolatileImage or overriding update() and paint() in standard AWT, but I ended up using Swing.

I'm posting the current state of affairs here just in case someone happens to have new insights based on my code example, and perhaps others who are working on a similar app might benefit from my findings.

The target is to accomplish these three feats:

  • render an animating object on top of a background buffer that is updated only when necessary
  • use textfields on top of the rendered result
  • resize the window without problems

Below is the code of the demo application developed with the great help of stackoverflower trashgod.

Two notes:

1) Refreshing strictly the area that is invalidated by the previous step in the animation appears to be so much prone to visual errors that I gave up on it. This means I now redraw the entire background buffer every frame.

2) The efficiency of drawing a BufferedImage to screen is hugely dependent on the platform. The Mac implementation doesn't seem to support hardware acceleration properly, which makes repainting the background image to the output window a tedious task, depending of course on the size of the window.

I found the following results on my 2.93 GHz dualcore iMac:

Mac OS 10.5:

640 x 480: 0.9 ms, 8 - 9%

1920 x 1100: 5 ms, 35 - 40%

Windows XP:

640 x 480: 0.05 ms, 0%

1920 x 1100: 0.05 ms, 0%

Legend:

screen size: average time to draw a frame, CPU usage of the application.

As far as I can see, the code below is the most efficient way of accomplishing my goals. Any new insights, optimizations or test results are very welcome!

Regards, Mattijs

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsCon开发者_如何转开发figuration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

public class SwingTest extends JPanel implements 
 ActionListener, 
 Runnable 
{
 private static final long serialVersionUID = 1L;

 private BufferedImage backgroundBuffer;
    private boolean repaintbackground = true;

    private static final int initWidth = 640;
    private static final int initHeight = 480;
    private static final int radius = 25;
    private final Timer t = new Timer(20, this);
    private final Rectangle rect = new Rectangle(); 

    private long totalTime = 0;
    private int frames = 0;
    private long avgTime = 0;

    public static void main(String[] args) {
        EventQueue.invokeLater(new SwingTest());
    }

    public SwingTest() {
        super(true);
        this.setPreferredSize(new Dimension(initWidth, initHeight));
        this.setLayout(null);
        this.setOpaque(false);
        this.addMouseListener(new MouseHandler());
    }

    @Override
    public void run() {
        JFrame f = new JFrame("SwingTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.addComponentListener(new ResizeHandler());

/*      This extra Panel with GridLayout is necessary to make sure 
   our content panel is properly resized with the window.*/
        JPanel p = new JPanel(new GridLayout()); 
        p.add(this);
        f.add(p);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);

        createBuffer();     
        t.start();
    }    

    @Override
    public void actionPerformed(ActionEvent e) {
        this.repaint();
    }    

    @Override
    protected void paintComponent(Graphics g) {
     long start = System.nanoTime();
     super.paintComponent(g);

     if (backgroundBuffer == null) createBuffer();
     if (repaintbackground) {

/*   Repainting the background may require complex rendering operations, 
   so we don't want to do this every frame.*/       
      repaintBackground(backgroundBuffer);
            repaintbackground = false;
     }

/*  Repainting the pre-rendered background buffer every frame
       seems unavoidable. Previous attempts to keep track of the 
       invalidated area and repaint only that part of the background buffer 
       image have failed. */
     g.drawImage(backgroundBuffer, 0, 0, null);
     repaintBall(g, backgroundBuffer, this.getWidth(), this.getHeight());
     repaintDrawTime(g, System.nanoTime() - start);
    }

    void repaintBackground(BufferedImage buffer) {    
     Graphics2D g = buffer.createGraphics();
  int width = buffer.getWidth();
  int height = buffer.getHeight();

  g.clearRect(0, 0, width, height);
  for (int i = 0; i < 100; i++) {
   g.setColor(new Color(0, 128, 0, 100));
   g.drawLine(width, height, (int)(Math.random() * (width - 1)), (int)(Math.random() * (height - 1)));
  }
    }

    void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
     double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
        rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

        g.setColor(Color.BLUE);
        g.fillOval(rect.x, rect.y, rect.width, rect.height);
    }

    void repaintDrawTime(Graphics g, long frameTime) {
     if (frames == 32) {avgTime = totalTime/32; totalTime = 0; frames = 0;}
     else {totalTime += frameTime; ++frames; }
     g.setColor(Color.white);
     String s = String.valueOf(avgTime / 1000000d + " ms");
        g.drawString(s, 5, 16);
    }

    void createBuffer() {
        int width = this.getWidth();
        int height = this.getHeight();

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        GraphicsConfiguration gc = gs.getDefaultConfiguration();
        backgroundBuffer = gc.createCompatibleImage(width, height, Transparency.OPAQUE);        

        repaintbackground = true;
    }    

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            super.mousePressed(e);
            JTextField field = new JTextField("test");
            Dimension d = field.getPreferredSize();
            field.setBounds(e.getX(), e.getY(), d.width, d.height);
            add(field);
        }
    }

    private class ResizeHandler extends ComponentAdapter {

     @Override
     public void componentResized(ComponentEvent e) {
      super.componentResized(e);
      System.out.println("Resized to " + getWidth() + " x " + getHeight());
      createBuffer();
     }    
    }
}


I have a few observations:

  1. Your improved repaintDrawTime() is very readable, but it is a micro-benchmark and subject to the vagaries of the host OS. I can't help wondering if the XP results are an artifact of that system's limited clock resolution. I see very different results on Windows 7 and Ubuntu 10.

  2. If you don't use a null layout, you won't need the extra panel; the default layout for JPanel is FlowLayout, and f.add(this) simply adds it to the center of the frame's default BorderLayout.

  3. Repeated constructor invocations can be time consuming.

    Consider replacing

    g.setColor(new Color(0, 128, 0, 100));
    

    with

    private static final Color color = new Color(0, 128, 0, 100);
    ...
    g.setColor(color);
    

    Alternatively, a simple color lookup table, may be useful, e.g.

    private final Queue<Color> clut = new LinkedList<Color>();
    


I don't see a point for passing in the BufferedImage to this method:

void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
 double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
    rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);

    g.setColor(Color.BLUE);
    g.fillOval(rect.x, rect.y, rect.width, rect.height);
}

It doesn't seem like you use it ever in the method body.

I was able to transplant the graphics portion of this class into my own JPanel constructor class, and it improved the graphics of my game a lot, but I never needed to use a method like this where I pass in a BufferedImage as an argument but never use it.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜