So, I have been working on a 2d rpg for some time now and I can\'t seem to fix this one problem. The graphics seem to \"jump\" or stutter every few seconds for an unknown re
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 pool;
private List 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 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 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();
}
});
}
}