我对 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;
}
}
我尝试更改图像渲染的方式,但问题仍然存在。我不知道是否有更好的渲染和添加新对象的管理..
让我们从显而易见的开始:
ArrayList
不是线程安全的,您有多个线程访问和更新列表,这可能会导致问题。getScaledInstance(...)
(甚至List#add
)的通话费用可能会很高。除非图像的大小在其生命周期内动态变化,否则在创建 Apple
时缩放图像一次。如果您绝对需要控制渲染管道,则需要使用
BufferStrategy
来代替。
在尝试之前,我建议尝试使其在 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 个实体,没有任何放缓或问题。