创建图像的新实例会减慢动画速度并产生闪烁

问题描述 投票:0回答:1

我对 Java 编程还比较陌生。我正在尝试创建落在 x 轴不同位置的物体(苹果)。然而,每次向 ArrayList 添加新内容时,图像都会变慢并闪烁

import org.w3c.dom.ls.LSOutput;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GamePanel extends JPanel {

    private final int FPS = 60;
    private final int originalSize = 16;
    private final int scale = 3;
    private final int tileSIze = originalSize * scale;
    private final int maxScreenCol = 16;
    private final int maxScreenRow = 12;
    private final int screenWidth = tileSIze * maxScreenCol;
    private final int screenHeight = tileSIze * maxScreenRow;
    private BufferedImage image;
    private Thread thread;
    private boolean start = true;
    private ArrayList<Apple> apples;

    public GamePanel() throws IOException {
        setPanelCharacteristics();
        image = ImageIO.read(new File("C:\\Users\\Gebruiker\\IdeaProjects\\AppleCatcher\\src\\apple.png"));
        initApples();
        start();
    }


    public void start() throws IOException {

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                double targetTime = 1000000000 / FPS;
                double delta = 0;
                double lastTime = System.nanoTime();
                long currentTime;
                while (start) {
                    currentTime = System.nanoTime();
                    delta += (currentTime - lastTime) / targetTime;
                    lastTime = currentTime;
                    if (delta >= 1) {
                        try {
                            update();

                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                        delta--;
                        repaint();
                    }
                }
            }
        });
        thread.start();

    }

    public void initApples() {
        apples = new ArrayList<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (start) {
                    try {
                        addApples();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();
    }

    public void setPanelCharacteristics() {
        setPreferredSize(new Dimension(screenWidth, screenHeight));
        setDoubleBuffered(true);
    }

    public void addApples() throws IOException {
        Random random = new Random();
        Apple apple = new Apple(screenWidth, screenHeight, image);
        apple.setSize(50, 50);
        int randomX = random.nextInt((screenWidth - apple.getWidth()));
        apple.setLocationX(randomX);
        apples.add(apple);
    }

    public synchronized void update() throws IOException {
        Random random = new Random();
        for (int i = 0; i < apples.size(); i++) {
            Apple apple = apples.get(i);
            if (apple != null) {
                apple.setVelocity(2);
                apple.animation();
                if (apple.getIsRemoved()) {
                    apples.remove(i);
                    i--;
                }
            }
        }
    }

@Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics g2D = g.create();
        for (int i = 0; i < apples.size(); i++) {
            Apple apple = apples.get(i);
            if (apple != null) {
                apple.drawImage(g2D);
            }
        }
        g2D.dispose();
    }
}


import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.*;

/**
 * @author Gebruiker
 */
public class Apple {

    private final BufferedImage apple;
    private final int heightParent;
    private final int widthParent;
    private int width;
    private int height;
    private int yVelocity = 1;
    private int x;
    private int y = -height;
    private boolean isRemoved = false;

    public Apple(int widthParent, int heightParent, BufferedImage image) throws IOException {
        this.heightParent = heightParent;
        this.widthParent = widthParent;
        this.apple = image;

    }

//    public Apple(BufferedImage apple, int heightParent, int widthParent) {
//        this.apple = apple;
//        this.heightParent = heightParent;
//        this.widthParent = widthParent;
//    }

    public void setLocationX(int x) {
        this.x = x;
    }

    public int getLocationX() {
        return x;
    }

    public void setSize(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public void setVelocity(int yVelocity) {
        this.yVelocity = yVelocity;
    }

    public int getWidth() {
        return width;
    }

    public void drawImage(Graphics g) {
        Graphics g2D = (Graphics2D) g;
        g2D.drawImage(apple.getScaledInstance(this.width, this.height, java.awt.Image.SCALE_SMOOTH), this.x, this.y, null);
    }

    public boolean getIsRemoved() {
        return isRemoved;
    }

    public synchronized void animation() {
        if (this.y >= this.heightParent) {
//            int randomX = random.nextInt(widthParent - width);
//            this.setLocationX(randomX);
            this.isRemoved = true;
        }
        this.y = this.y + yVelocity;
    }

}

我尝试更改图像渲染的方式,但问题仍然存在。我不知道是否有更好的渲染和添加新对象的管理..

java graphics2d
1个回答
0
投票

让我们从显而易见的开始:

  • ArrayList
    不是线程安全的,您有多个线程访问和更新列表,这可能会导致问题。
  • Swing 不是线程安全的。当尝试更新 UI 时,这可能会导致问题,尤其是当 UI 所依赖的状态是从事件调度线程外部修改时。
  • getScaledInstance(...)
    (甚至
    List#add
    )的通话费用可能会很高。除非图像的大小在其生命周期内动态变化,否则在创建
    Apple
    时缩放图像一次。
  • 重复创建和删除对象可能会增加垃圾收集的开销,从而导致速度变慢。如果需要,也许可以创建一个可以从中提取的实体池,仅在需要时创建它们。对于示例

如果您绝对需要控制渲染管道,则需要使用

BufferStrategy
来代替。

在尝试之前,我建议尝试使其在 Swing 渲染管道中工作,因为这会简化流程。

以下示例:

  • 将渲染工作流程与状态和更新工作流程解耦
  • 使用 Swing
    Timer
    作为“主循环”,触发模型更新并安排绘制过程。
  • 添加线程隔离。这意味着更新和绘制周期是在同一线程上完成的。这也意味着添加新苹果是作为更新过程的一部分完成的。

可运行的示例...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    // If the assets fail to load, there's no point
                    // in doing anything else
                    Asset.INSTANCE.prepare();
                    // Decouple the model/state from the UI
                    Model model = new Model();
                    RenderPane renderPane = new RenderPane(model);
                    JFrame frame = new JFrame("Test");
                    frame.add(renderPane);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);

                    renderPane.start();
                } catch (IOException ex) {
                    Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                    JOptionPane.showMessageDialog(null, "Failed to load required assets", "Error", JOptionPane.ERROR_MESSAGE);
                }
            }
        });
    }

    public class RenderPane extends JPanel {        
        private Timer timer;
        private Model model;

        public RenderPane(Model model) {
            this.model = model;
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    // This now allows the panel to be dynamically
                    // resizable, although if you make the window
                    // smaller, the apples which are now outside
                    // the viewable range will still be updated,
                    // but Swing's pretty well optimised for that
                    model.update(getSize());
                    repaint();
                }
            });
        }

        public void start() {
            timer.start();
        }

        public void stop() {
            timer.stop();
        }

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

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            List<Apple> apples = model.getApples();
            for (Apple apple : apples) {
                apple.paint(g2d);
            }
            String text = Integer.toString(apples.size());
            FontMetrics fm = g2d.getFontMetrics();
            g2d.setColor(Color.RED);
            g2d.drawString(text, getWidth() - fm.stringWidth(text) - 8 , getHeight() - fm.getHeight() + fm.getAscent() - 8);
            g2d.dispose();
        }
    }

    public class Model {
        private List<Apple> apples;
        private Instant lastAppleUpdate;

        public Model() {
            apples = new ArrayList<>(128);
        }

        public List<Apple> getApples() {
            return Collections.unmodifiableList(apples);
        }

        public void update(Dimension size) {
            // This will add a new apple approximately every
            // two seconds
            if (lastAppleUpdate == null || Duration.between(lastAppleUpdate, Instant.now()).getSeconds() >= 2) {
                apples.add(new Apple());
                lastAppleUpdate = Instant.now();
            }

            for (int index = apples.size() - 1; index >= 0; index--) {
                Apple apple = apples.get(index);
                apple.update(size);
                if (apple.isRemoved) {
                    apples.remove(index);
                }
            }
        }
    }

    // This is just an example, you don't need to do this,
    // but you might consider building some kind of asset
    // management system if you're dealing with multiple
    // assets
    enum Asset {
        INSTANCE;

        private BufferedImage apple;

        private Asset() {
        }

        public void prepare() throws IOException {

            apple = ImageUtilities.getScaledInstanceToFit(
                    ImageIO.read(getClass().getResource("/resources/apple.png")), 
                    new Dimension(48, 48)
            );
        }

        public BufferedImage getApple() {
            return apple;
        }
    }

    // This demonstrates a "better quality" scaling
    // approach, as apposed to using `getScaledInstance`
    public class ImageUtilities {

        public static BufferedImage getScaledInstanceToFit(BufferedImage img, Dimension size) {
            float scaleFactor = getScaleFactorToFit(img, size);
            return getScaledInstance(img, scaleFactor);
        }

        protected static float getScaleFactorToFit(BufferedImage img, Dimension size) {
            float scale = 1f;
            if (img != null) {
                int imageWidth = img.getWidth();
                int imageHeight = img.getHeight();
                scale = getScaleFactorToFit(new Dimension(imageWidth, imageHeight), size);
            }
            return scale;
        }

        protected static float getScaleFactorToFit(Dimension original, Dimension toFit) {
            float scale = 1f;
            if (original != null && toFit != null) {
                float dScaleWidth = getScaleFactor(original.width, toFit.width);
                float dScaleHeight = getScaleFactor(original.height, toFit.height);
                scale = Math.min(dScaleHeight, dScaleWidth);
            }
            return scale;
        }

        protected static float getScaleFactor(int iMasterSize, int iTargetSize) {
            float scale = 1;
            if (iMasterSize > iTargetSize) {
                scale = (float) iTargetSize / (float) iMasterSize;
            } else {
                scale = (float) iTargetSize / (float) iMasterSize;
            }
            return scale;
        }

        public static BufferedImage getScaledInstance(BufferedImage img, double dScaleFactor) {
            BufferedImage imgBuffer = null;
            imgBuffer = getScaledInstance(img, dScaleFactor, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
            return imgBuffer;
        }

        protected static BufferedImage getScaledInstance(BufferedImage img, double dScaleFactor, Object hint, boolean higherQuality) {

            int targetWidth = (int) Math.round(img.getWidth() * dScaleFactor);
            int targetHeight = (int) Math.round(img.getHeight() * dScaleFactor);

            int type = (img.getTransparency() == Transparency.OPAQUE)
                    ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;

            BufferedImage ret = (BufferedImage) img;

            if (targetHeight > 0 || targetWidth > 0) {
                int w, h;
                if (higherQuality) {
                    w = img.getWidth();
                    h = img.getHeight();
                } else {
                    w = targetWidth;
                    h = targetHeight;
                }

                do {
                    if (higherQuality && w > targetWidth) {
                        w /= 2;
                        if (w < targetWidth) {
                            w = targetWidth;
                        }
                    }

                    if (higherQuality && h > targetHeight) {
                        h /= 2;
                        if (h < targetHeight) {
                            h = targetHeight;
                        }
                    }

                    BufferedImage tmp = new BufferedImage(Math.max(w, 1), Math.max(h, 1), type);
                    Graphics2D g2 = tmp.createGraphics();
                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
                    g2.drawImage(ret, 0, 0, w, h, null);
                    g2.dispose();

                    ret = tmp;
                } while (w != targetWidth || h != targetHeight);
            } else {
                ret = new BufferedImage(1, 1, type);
            }
            return ret;
        }
    }

    public class Apple {        
        private static Random RANDOM = new Random();

        private int yVelocity = 1;
        private boolean isRemoved = false;
        private Rectangle bounds;

        public Apple() {
        }

        public Rectangle getBounds() {
            return bounds;
        }

        public void setVelocity(int yVelocity) {
            this.yVelocity = yVelocity;
        }

        public void paint(Graphics2D g) {
            if (bounds == null) {
                return;
            }
            Graphics g2D = (Graphics2D) g.create();
            g2D.drawImage(Asset.INSTANCE.getApple(), bounds.x, bounds.y, null);
            g2D.dispose();
        }

        public boolean getIsRemoved() {
            return isRemoved;
        }

        public synchronized void update(Dimension size) {
            if (bounds == null) {
                BufferedImage apple = Asset.INSTANCE.getApple();
                this.bounds = new Rectangle(RANDOM.nextInt(size.width - apple.getWidth()), -apple.getHeight(), apple.getWidth(), apple.getHeight());
            }
            if (bounds.getY() >= size.height) {
                this.isRemoved = true;
            }
            bounds.setLocation(bounds.x, bounds.y + yVelocity);
        }

    }
}

顺便说一句,我在测试代码时犯了一个错误,每次更新都会添加一个新的苹果(所以每 5 毫秒一个)。其峰值达到 648 个实体,没有任何放缓或问题。

© www.soinside.com 2019 - 2024. All rights reserved.