Java basic 2d game animation stutter

你说的曾经没有我的故事 提交于 2019-11-28 14:22:14
MadProgrammer

Don't update the state in the paintComponent method, painting can happen for any number reasons, many of which you don't initiate or will be notified about. Instead, the state should only be updated by your "main loop"

See Painting in AWT and Swing for more details about how painting works in Swing

Updated

Swing Timer based solution...

The example allows you to animate 1-10, 000 sprites, each sprite moves and spins independently. Obviously, I don't have collision detection, but the animation as a whole moves well

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

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

                PaintPane pane = new PaintPane();

                JSlider slider = new JSlider(1, 10000);
                slider.addChangeListener(new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        try {
                            pane.setQuantity(slider.getValue());
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                });
                slider.setValue(1);

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(pane);
                frame.add(slider, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static class PaintPane extends JPanel {

        private static final int SPOOL_DELTA = 100;

        private List<Sprite> pool;
        private List<Sprite> sprites;
        private int quantity;

        public PaintPane() {
            try {
                BufferedImage img = ImageIO.read(getClass().getResource("/resources/Pony.png"));

                pool = new ArrayList<>(128);
                sprites = new ArrayList<>(128);
                Timer timer = new Timer(40, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {

                        if (sprites.size() < quantity) {
                            List<Sprite> toAdd = new ArrayList<>(SPOOL_DELTA);
                            int required = quantity - sprites.size();
                            if (pool.isEmpty()) {
                                for (int index = 0; index < Math.min(SPOOL_DELTA, required); index++) {
                                    int x = (int)(Math.random() * getWidth());
                                    int y = (int)(Math.random() * getHeight());
                                    toAdd.add(new Sprite(img, new Point(x, y)));
                                }
                            } else {
                                toAdd.addAll(pool.subList(0, Math.min(SPOOL_DELTA, pool.size())));
                                pool.removeAll(toAdd);
                            }
                            sprites.addAll(toAdd);
                        } else if (sprites.size() > quantity) {
                            List<Sprite> toRemove = new ArrayList<>(SPOOL_DELTA);
                            int required = sprites.size() - quantity;
                            if (sprites.size() > required) {
                                toRemove.addAll(sprites.subList(0, Math.min(SPOOL_DELTA, required)));
                                sprites.removeAll(toRemove);
                                pool.addAll(toRemove);
                            }
                        }

                        for (Sprite sprite : sprites) {
                            sprite.update(getSize());
                        }
                        repaint();
                    }
                });
                timer.start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }

            setFont(getFont().deriveFont(Font.BOLD, 18f));
        }

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

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            for (Sprite sprite : sprites) {
                sprite.draw(g2d, this);
            }
            String text = NumberFormat.getNumberInstance().format(sprites.size());
            FontMetrics fm = g2d.getFontMetrics();
            int x = getWidth() - fm.stringWidth(text);
            int y = (getHeight() - fm.getHeight()) + fm.getAscent();
            g2d.drawString(text, x, y);
            g2d.dispose();
        }

        public void setQuantity(int value) throws IOException {
            this.quantity = value;
        }

    }

    public static class Sprite {

        private BufferedImage img;
        private Point location;
        private double angle;

        private Point delta;
        private double angleDelta;

        public Sprite(BufferedImage cache, Point location) {
            img = cache;
            this.location = new Point(location);
            delta = new Point(rnd(), rnd());
            while (angleDelta == 0) {
                angleDelta = (Math.random() * 5) - 2.5;
            }
        }

        protected int rnd() {
            int value = 0;
            while (value == 0) {
                value = (int) (Math.random() * 9) - 4;
            }
            return value;
        }

        public void update(Dimension size) {
            location.x += delta.x;
            location.y += delta.y;

            if (location.x < 0) {
                location.x = 0;
                delta.x *= -1;
            }
            if (location.x + img.getWidth() > size.width) {
                location.x = size.width - img.getWidth();
                delta.x *= -1;
            }
            if (location.y < 0) {
                location.y = 0;
                delta.y *= -1;
            }
            if (location.y + img.getHeight() > size.height) {
                location.y = size.height - img.getHeight();
                delta.y *= -1;
            }

            angle += angleDelta;
        }

        public void draw(Graphics2D g2d, JComponent parent) {
            Graphics2D g = (Graphics2D) g2d.create();
            AffineTransform at = AffineTransform.getTranslateInstance(location.x, location.y);
            at.rotate(Math.toRadians(angle), img.getWidth() / 2, img.getHeight() / 2);
            g.transform(at);
            g.drawImage(img, 0, 0, parent);
            g.dispose();
        }

    }

}

You could also use a "time" based animation, instead of linear based animation, for example

And if you're feeling really brave, Moving JLabel to other JLabels - GUI and Move image in a spiral fashion in java which are examples of key-frame based animations (time based)

Updated

This is an update to the original posted code from the question that is using a time based animation and adds in some rotation to the object (and some other graphical updates).

You'll note that I've used a ReentrantLock around the critical points where the shape is either updated or painted, this should prevent possible race conditions or dirty read/writes from occurring

The following is the same animation at 10, 5, 2 and 1 second durations

One thing I did note, was, the smaller the update range (ie window), te better the animation, so you might consider using something like repaint(Rectangle) to reduce the amount of area the component tries to update

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Main extends JPanel {

    double x = 0, y = 0;
    private Rectangle2D shape;
    private double angel = 0;

    private ReentrantLock updateLock = new ReentrantLock();

    public JFrame window = new JFrame("Window");

    public Main() {
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setSize(1000, 500);
        window.add(this);
        window.setVisible(true);
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
        g2d.setColor(Color.red);

        updateLock.lock();
        try {
            g2d.setTransform(AffineTransform.getRotateInstance(Math.toRadians(angel),
                    shape.getCenterX(),
                    shape.getCenterY()));
            g2d.fill(shape);
        } finally {
            updateLock.unlock();
        }
        g2d.dispose();
    }

    public void start() {
        shape = new Rectangle2D.Double(x, y, 50, 50);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                long startTime = System.nanoTime();
                long runTime = TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS);
                System.out.println(runTime);

                double rotateFrom = 0;
                double rotateTo = 720;
                while (true) {

                    long now = System.nanoTime();
                    long diff = now - startTime;
                    double progress = diff / (double) runTime;
                    if (progress > 1.0d) {
                        progress = 0d;
                        startTime = System.nanoTime();
                    }

                    x = (getWidth() * progress);

                    updateLock.lock();
                    try {
                        angel = rotateFrom + ((rotateTo - rotateFrom) * progress);
                        shape.setRect(x, y, 50, 50);
                    } finally {
                        updateLock.unlock();
                    }

                    repaint();
                    try {
                        Thread.sleep(8);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                Main game = new Main();

                game.start();
            }
        });
    }

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!