I have been trying to create a Java game loop that displays something simple to start with, but no matter what I try I can not get it too stop flickering. I have tried googling solutions but either my google-fu isn't quite up to scratch or I am doing something seriously wrong.
I'm posting this code here in the hope that I am simply doing something wrong that can be corrected. At this point I am tempted to start over in a different language, last time I used it SFML was nice. Apologies for the horrible code.
Main.java:
public class Main {
public static void main(String[] args) {
new GameFrame("Game");
}
}
GameFrame.java:
import java.awt.Color;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Toolkit;
import javax.swing.JFrame;
public class GameFrame extends JFrame {
protected GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
public GameFrame(String title) {
super(title);
init();
}
public void init() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
// Calculate the size of the window, taking into account the size of toolbars etc.
Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(device.getDefaultConfiguration());
setSize(
(int) ((device.getDefaultConfiguration().getBounds().width - insets.left - insets.right) * 0.8),
(int) ((device.getDefaultConfiguration().getBounds().height - insets.top - insets.bottom) * 0.8)
);
// Centre and start maximised.
setLocationRelativeTo(null);
setExtendedState(MAXIMIZED_BOTH);
if(false) {
// Important otherwise you get an ugly border.
super.setVisible(false);
setUndecorated(true);
device.setFullScreenWindow(this);
} else {
setUndecorated(false);
device.setFullScreenWindow(null);
super.setVisible(true);
}
GamePanel panel = new GamePanel();
panel.setBackground(Color.BLACK);
new Thread(panel).start();
add(panel);
}
}
GamePanel.java:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import javax.swing.JPanel;
public class GamePanel extends JPanel implements Runnable {
protected boolean running = false;
protected long lastTime;
protected int width = 0;
protected int height = 0;
protected Image image;
protected Graphics2D graphics;
protected double x = 0;
protected double y = 0;
protected void updateState(double delta) {
x = x + 0.00001 * delta;
y = y + 0.00001 * delta;
}
public void run() {
running = true;
lastTime = System.nanoTime();
while(width == 0 || height == 0) {
width = getBounds().width;
height = getBounds().height;
}
setDoubleBuffered(true);
while(running) {
long now = System.nanoTime();
long updateLength = now - lastTime;
double delta = updateLength / (10 ^ 9 / 60);
updateState(delta);
paintComponent(getGraphics());
lastTime = System.nanoTime();
try {Thread.sleep((now - lastTime + (10 ^ 9 / 60)) / (10 ^ 6));} catch (Exception e) {}
}
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
if(isVisible() && width > 0 && height > 0) {
setDoubleBuffered(true);
if(image == null) {
image = createImage(width, height);
graphics = (Graphics2D) image.getGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
graphics.setColor(Color.black);
graphics.fillRect(0, 0, width, height);
graphics.setColor(Color.WHITE);
graphics.fillRect(100, 20, 100, 100);
graphics.fillOval((int) x, (int) y, 30, 30);
g.drawImage(image, 0, 0, this);
}
}
}
Your program fragments are incorrect in several ways:
Swing GUI objects should be constructed and manipulated only on the event dispatch thread.
Don't use getGraphics(); the graphics context is only valid in paintComponent().
JPanel is double buffered by default.
Use an instance of javax.swing.Timer to pace the animation; a complete example is seen in this AnimationTest.
Related
The game runs at about 2000 to 3100 fps in normal window mode. If i set the JFrame component to fullscreen and scale up my JPanel to also the same resolution, the fps drops to 20-70.
(This is a prototype, hardcoded resolutions will be later swapped out)
This is my relevant code (if this is not enough, I can provide more):
Game.java
import javax.swing.JFrame;
public class Game {
public static void main(String[] args) {
JFrame window = new JFrame("Platformer Test");
window.setContentPane(new GamePanel(window));
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(true);
//window.setUndecorated(true);
window.pack();
window.setVisible(true);
}
}
GamePanel.java
package Main;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
// custom imports
import GameState.GameStateManager;
#SuppressWarnings("serial")
public class GamePanel extends JPanel implements Runnable, KeyListener{
// dimensions
public static final int WIDTH = 320;
public static final int HEIGHT = 240;
public static final int SCALE = 2;
// Graphic Device (used for fullscreen)
static GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];
private JFrame frame;
// game Thread
private Thread thread;
private boolean running;
private double GameTicks = 60;
// image
private BufferedImage image;
private Graphics2D g;
boolean renderFPS = false;
int frames = 0;
// game state manager
private GameStateManager gsm;
public GamePanel(JFrame frame) {
super();
this.frame = frame;
// set Window Size
setFocusable(true);
setFullscreen(true);
}
private void setFullscreen(boolean t) {
if(t) {
setPreferredSize(new Dimension(1920, 1080));
device.setFullScreenWindow(frame);
requestFocus();
}else {
setSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE));
requestFocus();
}
}
public void addNotify() {
super.addNotify();
if (thread == null) {
thread = new Thread(this);
addKeyListener(this);
thread.start();
}
}
private void init() {
// create image --> Game is drawn on here
image = new BufferedImage(
WIDTH, HEIGHT,
BufferedImage.TYPE_INT_RGB
);
// get graphics component of game image
g = (Graphics2D) image.getGraphics();
// starts game clock
running = true;
// adds new GameStateManager
gsm = new GameStateManager();
}
#Override
public void run() {
init();
//game loop setup
double ns = 1000000000 / GameTicks;
double delta = 0;
long lastTime = System.nanoTime();
long timer = System.currentTimeMillis();
int ticks = 0;
// game loop
while(running) {
long now = System.nanoTime();
delta += (now - lastTime) / ns;
lastTime = now;
while(delta >= 1) {
update();
ticks++;
delta--;
}
if(running)
render();
frames++;
if(System.currentTimeMillis() - timer > 1000) {
timer += 1000;
System.out.println("FPS: " + frames + ", ticks: " + ticks);
renderFPS = true;
frames = 0;
ticks = 0;
}
}
}
private void update() {
gsm.update();
}
private void render() {
gsm.render(g);
int fps = 0;
// Draw To Screen
Graphics g2 = getGraphics();
g2.drawImage(image, 0, 0, WIDTH * SCALE, HEIGHT * SCALE, null);
//g2.drawImage(image, 0, 0, 1920, 1080, null);
if(renderFPS) {
fps = frames;
}
g2.setColor(Color.red);
g2.drawString("FPS: " + fps, 100,100);
g2.dispose();
}
#Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
#Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
gsm.keyPressed(e.getKeyCode());
}
#Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
gsm.keyReleased(e.getKeyCode());
}
}
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();
}
}
}
class GraphicsExampleComponent extends JComponent
{
//#Override
public void paintComponent(Graphics g)
{
//make the first call to your recursive routine
drawSquare1(g, 0, 0, 80);
}
public void drawSquare1(Graphics g, int x, int y, int size)
{
//draw a rectangle
g.drawRect(x, y, size, size);
g.fillRect(x, y, size, size);
//reset the parameters
x = x + size + 10;
y = y + (size/4);
size = size/4;
//determine if you should call it again.
if (size<4 || x>600)
drawSquare1(g, x, y, size);
}
My assignment is to create disappearing squares that get 25% smaller as they move to the right. When I run the code, it just creates the one square and stops. Can anyone help me understand what I am doing wrong.
disappearing squares that get 25% smaller as they move to the right.
Ok, let's step back for a second and break this down a bit.
You need to know...
The amount of space to be covered
The amount of space already covered (by the square)
The original size of the square
The size of the square should be when it reaches the other side (25% smaller)
When you have all this, you can calculate the size of the square at any point along its journey.
To determine the amount of space, you can use the component's width, via getWidth().
To determine the space already covered, you could start by having a look at box's current x position
// Assuming that box is a instance of Rectangle
double progress = (double)box.x / (double)getWidth();
We could argue that we should look at the middle of the box, or the trailing edge, but both of those are easy to implement.
Next, we need know the range of change (from start to end size), we can then use that to calculate the delta to be applied to the box...
double range = startSize - endSize;
double value = (range * progress);
box.width = (int)startSize - (int)value;
box.height = (int)startSize - (int)value;
Soooo, this will provide with the means to determine the size of the component based on it's current location (horizontally) through the component.
Next, you need some way to update the box's position and update the UI.
One of the better solutions is to use a Swing Timer. This will allow you to perform a repeating action (with a specified delay between updates) which won't block the UI and will generate updates within the Event Dispatching Queue, which is important because Swing is not Thread safe.
Have a look at How to Use Swing Timers for more details.
And finally, all we need, is to update the component with current state, via it's paintComponent method ... easy :P
Runnable example
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public static class TestPane extends JPanel {
protected static double startSize = 50;
protected static double endSize = startSize * 0.25;
private Rectangle box;
private Timer timer;
public TestPane() {
box = new Rectangle(0, 100 - 25, 50, 50);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (box.x + box.width >= getWidth()) {
box.x = getWidth() - box.width;
box.width = (int)endSize;
box.height = (int)endSize;
timer.stop();
repaint();
}
box.x += 1;
double progress = (double)box.x / (double)getWidth();
double range = startSize - endSize;
double value = (range * progress);
box.width = (int)startSize - (int)value;
box.height = (int)startSize - (int)value;
repaint();
}
});
}
#Override
public void addNotify() {
super.addNotify();
box.x = 0;
timer.start();
}
#Override
public void removeNotify() {
super.removeNotify();
timer.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
g2d.fill(box);
g2d.dispose();
}
}
}
The condition if (size < 4 || x>600) is never true because when drawSquare1 is invoked for the first time size=80 and x=0.
Changing it to say if (size > 4 && x<600) will paint 3 squares on the screen without any noticeable animation.
To animate it we'll need to add some delay between paintings and remove previously painted squares.
To do so we use a swing Timer. We use the timer to repeatedly invoke drawSquare1.
drawSquare1 should modify the parameters controlling the painting, and call repaint.
import java.awt.*;
import javax.swing.*;
public class Main {
Main() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationByPlatform(true);
GraphicsExampleComponent board = new GraphicsExampleComponent();
frame.add(board);
frame.pack();
frame.setVisible(true);
board.animate();
}
public static void main(String[] args) {
new Main();
}
}
class GraphicsExampleComponent extends JComponent{
private final static int W = 200, H = 100, MIN_SIZE = 4, STEP = 10, DELAY = 2000;
private int x= 0, y = 0, size = 80;
private Timer timer;
void animate(){
timer = new Timer(DELAY, e->drawSquare());
timer.start();
}
public void drawSquare(){
//check stop criteria
if (size < MIN_SIZE || x >= getWidth()) {
timer.stop();
return;
}
//reset the parameters
x = x + size + STEP;
y = y + size/4;
size = size/4;
repaint();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//draw a rectangle
g.fillRect(x, y, size, size);
g.dispose();;
}
#Override
public Dimension preferredSize() {
return new Dimension(W, H);
}
}
(Test in online here)
Thank you everyone for the help. I unfortunately could not use a timer due to us not learning about it in class and we are not allowed to use methods/codes that we haven't gone over. To fix this I had to fix my base case by making it size >= 4 && x < 600.
I'm trying to create a simple panel where a 2-dimensional ball is bouncing up and down. I can't get it to work because for some reason I can't call the repaint method more than once a second. The design is basically that there is an object that can be given "an impulse" with the method move(). Everytime the evaluatePosition method is called, the current position will be calculated through the time that has passed, the velocity and the acceleration. The code for the panel is:
public class Display extends JPanel {
private MovableObject object = new MovableObject(new Ellipse2D.Double(5,5,50,50));
private static final int DELAY = 1000;
public Display(){
object.move(50,50);
Timer timer = new Timer(DELAY, new ActionListener(){
#Override
public void actionPerformed(ActionEvent e){
object.evaluatePosition();
repaint();
}
});
timer.start();
}
}
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.drawOval((int)object.getPosition().getX(), (int)object.getPosition.getY()
(int)object.getShape().getWidth(), object.getShape().getHeight());
}
This code works for DELAY=1000 but not for DELAY=100 or DELAY=10 and so on. I read some example code here on SO but they all seem to me like what I already did. So why is my code not working?
EDIT (2016-01-30):
Since it really seems to be a performance issue, here's the code for the MovableObject (I just thought it would be irrelevant and you will probably see why):
public class MovableObject {
// I would really like to use Shape instead of Ellipse2D so that
// objects of any shape can be created
private Ellipse2D.Double shape;
private Point position;
// Vector is my own class. I want to have some easy vector addition and
// multiplication and magnitude methods
private Vector velocity = new Vector(0, 0);
private Vector acceleration = new Vector(0, 0);
private Date lastEvaluation = new Date();
public MovableObject(Ellipse2D.Double objectShape){
shape = objectShape;
}
public void evaluatePosition(){
Date currentTime = new Date();
long deltaTInS = (currentTime.getTime()-lastEvaluation.getTime())/1000;
// s = s_0 + v*t + 0.5*a*t^2
position = new Point((int)position.getX()+ (int)(velocity.getX()*deltaTInS) + (int)(0.5*acceleration.getX()*deltaTInS*deltaTInS),
(int)position.getY()+ (int)(velocity.getY()*deltaTInS) + (int)(0.5*acceleration.getY()*deltaTInS*deltaTInS));
lastEvaluation = currentTime;
}
}
public void move(Vector vector){
velocity = velocity.add(vector);
evaluatePosition();
}
public Point getPosition(){
return position;
}
public Ellipse2D.Double getShape(){
return shape;
}
My move method does not change position but velocity. Please notice that I just changed the shape Object from Shape to Ellipse2D for testing if my code has a performance issue because of the additional code. So if you think this is more complex than it needs to be: I actually want to add some complexity so that the MovableObject can have the shape of any subclass of shape. I've seen a lot of code that seemed more complex to me and rendered fast. So I'd like to know what's wrong with this (besides the fact that it's a bit too complex for just rendering an ellipse).
Okay, so this is a simple example, based on the out-of-context code snippet you left which doesn't seem to have any problems. It has variable speed controlled by a simple slider...
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.util.Random;
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 Display extends JPanel {
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();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Display());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
private MovableObject object = new MovableObject(new Ellipse2D.Double(5, 5, 50, 50));
private int delay = 40;
private Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
object.evaluatePosition(getSize());
repaint();
}
});
private JSlider slider = new JSlider(5, 1000);
public Display() {
object.move(50, 50);
slider = new JSlider(5, 1000);
slider.setSnapToTicks(true);
slider.setMajorTickSpacing(10);
slider.setMinorTickSpacing(5);
setLayout(new BorderLayout());
add(slider, BorderLayout.SOUTH);
// This is simply designed to put an artificate delay between the
// change listener and the time the update takes place, the intention
// is to stop it from pausing the "main" timer...
Timer delay = new Timer(250, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (timer != null) {
timer.stop();
}
timer.setDelay(slider.getValue());
timer.start();
}
});
slider.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
delay.restart();
repaint();
}
});
slider.setValue(40);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.draw(object.getTranslatedShape());
FontMetrics fm = g2.getFontMetrics();
String text = Integer.toString(slider.getValue());
g2.drawString(text, 0, fm.getAscent());
g2.dispose();
}
public class MovableObject {
private Shape shape;
private Point location;
private int xDelta, yDelta;
public MovableObject(Shape shape) {
this.shape = shape;
location = shape.getBounds().getLocation();
Random rnd = new Random();
xDelta = rnd.nextInt(8);
yDelta = rnd.nextInt(8);
if (rnd.nextBoolean()) {
xDelta *= -1;
}
if (rnd.nextBoolean()) {
yDelta *= -1;
}
}
public void move(int x, int y) {
location.setLocation(x, y);
}
public void evaluatePosition(Dimension bounds) {
int x = location.x + xDelta;
int y = location.y + yDelta;
if (x < 0) {
x = 0;
xDelta *= -1;
} else if (x + shape.getBounds().width > bounds.width) {
x = bounds.width - shape.getBounds().width;
xDelta *= -1;
}
if (y < 0) {
y = 0;
yDelta *= -1;
} else if (y + shape.getBounds().height > bounds.height) {
y = bounds.height - shape.getBounds().height;
yDelta *= -1;
}
location.setLocation(x, y);
}
public Shape getTranslatedShape() {
PathIterator pi = shape.getPathIterator(AffineTransform.getTranslateInstance(location.x, location.y));
GeneralPath path = new GeneralPath();
path.append(pi, true);
return path;
}
}
}
You could also have a look at
Swing animation running extremely slow
Rotating multiple images causing flickering. Java Graphics2D
Java Bouncing Ball
for some more examples...
I am trying to make a simple animated intro. I have an image I am trying to move from the bottom left of the screen to the center of the screen in a clockwise spiral motion. This is the code that I am using for now. It just moves the image upward to the center:
static ImageLoader il = new ImageLoader();
private static BufferedImage logo = il.load("/logoNew.png");
private static Image power = il.gif("http://i.stack.imgur.com/KSnus.gif");
static double y = 1024.0;
public static void render(Graphics g){
if(y>(486/2)-128){
y = y-0.25;
}
if(draw){
g.drawImage(logo,(864/2)-128,(int)y,null);
g.setColor(Color.WHITE);
g.drawImage(power,10,10,null);
}
}
The if(draw) statement is activated by something else.
How do I go about moving the image. Do I just increment the x and the y differently at different points?
** EDIT **
I didn't make it clear on the motion. Its going from the bottom left to the top left to the top right to the bottom right to the bottom center (centre) to the center (centre) of the screen
Animation is the illusion of movement over time. Normally I would use something like the Timing Framework (or Trident or Universal Tween Engine) as the base of the animation, these provide better support for things like ease-in and ease-out.
The following example just makes uses of a simple javax.swing.Timer. I use this because it's safer to use with Swing, as it allows me to update the state of the UI from within the context of the Event Dispatching Thread, but doesn't block it (preventing it from updating the screen).
The following example uses a concept of a timeline and key frames. That is, at some point in time, something must happen. The timeline then provides the means for blending between those "key" points in time.
I, personally, like to work in abstract concepts, so the timeline is simply measured in a percentage from 0-1, which allows me to provide a variable time span. This allows me to adjust the speed of the animation without the need to change anything.
As you (should) be able to see, the last two legs only need to move half the distance, so they are slower than the other three legs, so, technically, they only need half the time to complete...but I'll leave it up to you to nut out the maths for that ;)
import java.awt.Dimension;
import java.awt.EventQueue;
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.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
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();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
protected static final int PLAY_TIME = 6000;
private Timeline timeline;
private long startTime;
private Point imgPoint;
private BufferedImage img;
public TestPane() {
try {
img = ImageIO.read(new File("C:/Neko.png"));
imgPoint = new Point(0, 200 - img.getHeight());
timeline = new Timeline();
timeline.add(0f, imgPoint);
timeline.add(0.2f, new Point(0, 0));
timeline.add(0.4f, new Point(200 - img.getWidth(), 0));
timeline.add(0.6f, new Point(200 - img.getWidth(), 200 - img.getHeight()));
timeline.add(0.8f, new Point(100 - (img.getWidth() / 2), 200 - img.getHeight()));
timeline.add(1f, new Point(100 - (img.getWidth() / 2), 100 - (img.getHeight() / 2)));
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
long duration = System.currentTimeMillis() - startTime;
float progress = (float) duration / (float) PLAY_TIME;
if (progress > 1f) {
startTime = System.currentTimeMillis();
progress = 0;
((Timer) (e.getSource())).stop();
}
System.out.println(progress);
imgPoint = timeline.getPointAt(progress);
repaint();
}
});
startTime = System.currentTimeMillis();
timer.start();
} catch (IOException exp) {
exp.printStackTrace();
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (img != null && imgPoint != null) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.drawImage(img, imgPoint.x, imgPoint.y, this);
g2d.dispose();
}
}
}
public static class Timeline {
private Map<Float, KeyFrame> mapEvents;
public Timeline() {
mapEvents = new TreeMap<>();
}
public void add(float progress, Point p) {
mapEvents.put(progress, new KeyFrame(progress, p));
}
public Point getPointAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
float max = keyFrames[1].progress - keyFrames[0].progress;
float value = progress - keyFrames[0].progress;
float weight = value / max;
return blend(keyFrames[0].getPoint(), keyFrames[1].getPoint(), 1f - weight);
}
public KeyFrame[] getKeyFramesBetween(float progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected Point blend(Point start, Point end, float ratio) {
Point blend = new Point();
float ir = (float) 1.0 - ratio;
blend.x = (int) (start.x * ratio + end.x * ir);
blend.y = (int) (start.y * ratio + end.y * ir);
return blend;
}
public class KeyFrame {
private float progress;
private Point point;
public KeyFrame(float progress, Point point) {
this.progress = progress;
this.point = point;
}
public float getProgress() {
return progress;
}
public Point getPoint() {
return point;
}
}
}
}
I want to represent a timer by having a filled circle which is fully drawn over the course of the timer by segment. I.e. If the circle is filled in every 1 second for a timer of 4 seconds the first will show a quarter of a circle then a half then three-quarters and finally a full circle.
Is there a way to draw these slices of a circle in java? I've looked into arbitrary shapes in the graphics API but not sure if this is the right way to go or if there is something written into the language which can easily produce these type of shapes?
Any help much appreciated.
Yes, it's possible. Yes, Graphics2D has the ability to do this for you
Take a look at Drawing Geometric Primitives (look for the Arc2D section).
To "animate" the progress, it would probably be easiest to use a javax.swing.Timer, but your requirements might require you to use a SwingWorker instead. Have a look at Currency in Swing and How to use Swing Timers for more information.
The following example is relatively simple. It assumes a progress of 0-100% and generates a arc as required. It would be a simple matter of changing the color of the Graphics2D context and using draw(Shape) to draw an out line of the circle should you want one.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class ArcProgress {
public static void main(String[] args) {
new ArcProgress();
}
private float progress;
public ArcProgress() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
final ArcProgressPane p1 = new ArcProgressPane();
p1.setForeground(Color.RED);
final ArcProgressPane p2 = new ArcProgressPane();
p2.setForeground(Color.BLUE);
p2.setFillProgress(true);
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());
frame.add(p1);
frame.add(p2);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
progress += 0.01f;
if (progress >= 1f) {
progress = 1f;
((Timer) e.getSource()).stop();
}
p1.setProgress(progress);
p2.setProgress(progress);
}
});
timer.setRepeats(true);
timer.setCoalesce(true);
timer.start();
}
});
}
public class ArcProgressPane extends JPanel {
private boolean fillProgress = false;
private float progress;
public ArcProgressPane() {
}
public void setFillProgress(boolean value) {
if (fillProgress != value) {
this.fillProgress = value;
firePropertyChange("fillProgress", !fillProgress, fillProgress);
}
}
public boolean isFillProgress() {
return fillProgress;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}
public void setProgress(float value) {
if (progress != value) {
float old = progress;
this.progress = value;
firePropertyChange("progress", old, progress);
repaint();
}
}
public float getProgress() {
return progress;
}
#Override
protected 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);
Insets insets = getInsets();
int width = getWidth() - (insets.left + insets.right);
int height = getHeight() - (insets.bottom + insets.top);
int raidus = Math.min(width, height);
int x = insets.left + ((width - raidus) / 2);
int y = insets.right + ((height - raidus) / 2);
double extent = 360d * progress;
g2d.setColor(getForeground());
Arc2D arc = null;
if (isFillProgress()) {
arc = new Arc2D.Double(x, y, raidus, raidus, 90, -extent, Arc2D.PIE);
} else {
extent = 360 - extent;
arc = new Arc2D.Double(x, y, raidus, raidus, 90, extent, Arc2D.PIE);
}
g2d.fill(arc);
g2d.dispose();
}
}
}