Swing is not thread safe, you shouldn't be updating the UI, or the state the UI relies on, from outside the context of the Event Dispatching Thread. This means you shouldn't be using Thread
as your "game loop".
See Concurrency in Swing for more details and How to Use Swing Timers for the most common solution.
Don't use JPanel#getGraphics
, this is not how painting in Swing is done. Instead, override paintComponent
. See Painting in AWT and Swing
and Performing Custom Painting for more details.
Don't use KeyListener
, seriously, it's just not worth all the hacking around to make it work. Instead, use the key bindings API
The following, simple, example runs at roughly 171 updates a second (it separates the "timer ticks" and "paint ticks", as in Swing, it's not really possible to know when something is actually rendered to the screen) in both windowed and full screen
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public final class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];
JFrame frame = new JFrame();
frame.add(new GamePanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// device.setFullScreenWindow(frame);
}
});
}
public class GamePanel extends JPanel {
private Timer timer;
private int ticksPerSecond = 0;
private int paintsPerSecond = 0;
private int xDelta = 1;
private Rectangle boxy = new Rectangle(0, 0, 50, 50);
// Graphic Device (used for fullscreen)
public GamePanel() {
timer = new Timer(5, new ActionListener() {
private Instant lastTick;
private int ticks = 0;
@Override
public void actionPerformed(ActionEvent e) {
if (lastTick == null) {
lastTick = Instant.now();
}
if (Duration.between(lastTick, Instant.now()).toMillis() >= 1000) {
ticksPerSecond = ticks;
lastTick = Instant.now();
ticks = 0;
}
ticks++;
boxy.x += xDelta;
if (boxy.x + boxy.width > getWidth()) {
boxy.x = getWidth() - boxy.width;
xDelta *= -1;
} else if (boxy.x < 0) {
boxy.x = 0;
xDelta *= -1;
}
boxy.y = (getHeight() - boxy.height) / 2;
repaint();
}
});
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
public void addNotify() {
super.addNotify();
timer.start();
}
@Override
public void removeNotify() {
timer.stop();
super.removeNotify();
}
private Instant lastPaint;
private int paints = 0;
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastPaint == null) {
lastPaint = Instant.now();
}
if (Duration.between(lastPaint, Instant.now()).toMillis() >= 1000) {
paintsPerSecond = paints;
lastPaint = Instant.now();
paints = 0;
}
paints++;
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
g2d.drawString("Ticks p/s " + ticksPerSecond, 10, 10 + fm.getAscent());
g2d.drawString("Paints p/s " + paintsPerSecond, 10, 10 + fm.getAscent() + fm.getHeight());
g2d.fill(boxy);
g2d.dispose();
}
}
}
If you "really" need absolute control over the painting process, then you should be using a BufferStrategy
, see BufferStrategy and BufferCapabilities and the JavaDocs which has an excellent example of how it should be used.
Lots of things might effect the performance of the paint process, for example
A crazy experiment...
So, this example allows you to change the number of entities been rendered on the screen. Each entity is moving in it's own direction and is rotating (no collision detection).
I can get this to run up to roughly 20-25, 0000 entities before I start seeing a (significant) change in the number of updates per second. I rolled it to 100, 000 and it dropped to roughly 42 updates per second.
import java.awt.BorderLayout;
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.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public final class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];
GamePanel gamePanel = new GamePanel();
JSlider slider = new JSlider(1, 100000);
slider.setValue(100);
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (slider.getValueIsAdjusting()) {
return;
}
gamePanel.setBoxCount(slider.getValue());
}
});
JFrame frame = new JFrame();
frame.add(gamePanel);
frame.add(slider, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
device.setFullScreenWindow(frame);
}
});
}
public class GamePanel extends JPanel {
private Timer timer;
private int ticksPerSecond = 0;
private int paintsPerSecond = 0;
private List<Box> boxes = new ArrayList<>(100);
// Graphic Device (used for fullscreen)
public GamePanel() {
for (int index = 0; index < 100; index++) {
boxes.add(new Box());
}
timer = new Timer(5, new ActionListener() {
private Instant lastTick;
private int ticks = 0;
@Override
public void actionPerformed(ActionEvent e) {
if (lastTick == null) {
lastTick = Instant.now();
}
if (Duration.between(lastTick, Instant.now()).toMillis() >= 1000) {
ticksPerSecond = ticks;
lastTick = Instant.now();
ticks = 0;
}
ticks++;
for (Box box : boxes) {
box.update(getSize());
}
repaint();
}
});
}
public void setBoxCount(int count) {
if (count < boxes.size()) {
boxes = boxes.subList(0, count);
return;
}
while (boxes.size() < count) {
boxes.add(new Box());
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
public void addNotify() {
super.addNotify();
timer.start();
}
@Override
public void removeNotify() {
timer.stop();
super.removeNotify();
}
private Instant lastPaint;
private int paints = 0;
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastPaint == null) {
lastPaint = Instant.now();
}
if (Duration.between(lastPaint, Instant.now()).toMillis() >= 1000) {
paintsPerSecond = paints;
lastPaint = Instant.now();
paints = 0;
}
paints++;
Graphics2D g2d = (Graphics2D) g.create();
for (Box box : boxes) {
box.paint(g2d);
}
FontMetrics fm = g2d.getFontMetrics();
int yPos = 10 + fm.getAscent();
g2d.drawString("Ticks p/s " + ticksPerSecond, 10, yPos + fm.getAscent());
yPos += fm.getHeight();
g2d.drawString("Paints p/s " + paintsPerSecond, 10, yPos + fm.getAscent());
yPos += fm.getHeight();
g2d.drawString("Count " + boxes.size(), 10, yPos + fm.getAscent());
g2d.dispose();
}
}
private List<Color> colors = new ArrayList<>(Arrays.asList(new Color[]{
Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GRAY, Color.GREEN,
Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.WHITE,
Color.YELLOW
}));
public class Box {
private Color fill;
private Rectangle shape;
private int x;
private int y;
private int xDelta;
private int yDelta;
private int rotationDelta;
private int angle = 0;
public Box() {
Random rnd = new Random();
int width = rnd.nextInt(45) + 5;
int height = rnd.nextInt(45) + 5;
x = rnd.nextInt(400) - width;
y = rnd.nextInt(400) - height;
xDelta = rnd.nextInt(2) + 1;
yDelta = rnd.nextInt(2) + 1;
if (rnd.nextBoolean()) {
xDelta *= -1;
}
if (rnd.nextBoolean()) {
yDelta *= -1;
}
rotationDelta = rnd.nextInt(5);
if (rnd.nextBoolean()) {
rotationDelta *= -1;
}
shape = new Rectangle(x, y, width, height);
Collections.shuffle(colors);
fill = colors.get(0);
shape = new Rectangle(0, 0, width, height);
}
public void update(Dimension bounds) {
x += xDelta;
y += yDelta;
angle += rotationDelta;
if (x + getWidth() > bounds.width) {
x = bounds.width - getWidth();
xDelta *= -1;
} else if (x < 0) {
x = 0;
xDelta *= -1;
}
if (y + getWidth() > bounds.height) {
y = bounds.height - getWidth();
yDelta *= -1;
} else if (y < 0) {
y = 0;
yDelta *= -1;
}
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return shape.width;
}
public int getHeight() {
return shape.height;
}
public void paint(Graphics2D g2d) {
g2d = (Graphics2D) g2d.create();
g2d.translate(x, y);
g2d.rotate(Math.toRadians(angle), getWidth() / 2, getHeight() / 2);
g2d.setColor(fill);
g2d.fill(shape);
g2d.dispose();
}
}
}