I have a simple Java/Swing application that tries to animate a box by moving it from left to right:
public class TestFrame extends JFrame {
protected long startTime = new Date().getTime();
public class MainPanel extends JPanel {
#Override
protected void paintComponent(Graphics g) {
// calculate elapsed time
long currentTime = new Date().getTime();
long timeDiff = currentTime - TestFrame.this.startTime;
// animation time dependent
g.fillRect((int) (timeDiff / 100), 10, 10, 10);
}
}
public class MainLoop implements Runnable {
#Override
public void run() {
while (true) {
// trigger repaint
TestFrame.this.repaint();
}
}
}
public static void main(String[] args) {
new TestFrame();
}
public TestFrame() {
// init window
this.setTitle("Test");
this.setSize(500, 500);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.add(new MainPanel());
this.setVisible(true);
// start render loop
Thread loop = new Thread(new MainLoop());
loop.start();
}
}
The problem is that the animation is not clean and the box jumps (sometimes) a few pixels. I already did some researches and according to them it should work fine if using paintComponent (instead of paint) and doing a time based animation (not frame based). I did both but the animation is still not clean.
Could anybody give me a hint what is going wrong?
You should give your while-true-loop a little rest. You're kind of burning your CPU! You're generating a tremendous amount of paint events; at some time which the thread scheduler decides, the scheduler hands off to the event dispatching thread, which as far as I recall may collapse your trillon of paint events into a single one and eventually execute paintComponent.
In the following example, the thread sleeps 20ms, which gives you a maximum frame rate of 50fps. That should be enough.
while (true) {
// trigger repaint
TestFrame.this.repaint();
try {
Thread.sleep(20);
} catch(InterruptedException exc() {
}
}
I made a few changes to your code.
I called the SwingUtilities invokeLater method to create and use your Swing components on the Event Dispatch thread.
I called the System currentTimeinMillis method to get the current time.
Instead of setting the JFrame size, I set the size of the JPanel and packed the JFrame. I reduced the size of the JPanel to speed up the repainting.
I added a delay in the while(true) loop, as fjf2002 suggested in his answer.
Here's the revised and formatted code:
package com.ggl.testing;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class TestFrame extends JFrame {
private static final long serialVersionUID = 272649179566160531L;
protected long startTime = System.currentTimeMillis();
public class MainPanel extends JPanel {
private static final long serialVersionUID = 5312139184947337026L;
public MainPanel() {
this.setPreferredSize(new Dimension(500, 30));
}
#Override
protected void paintComponent(Graphics g) {
// calculate elapsed time
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - TestFrame.this.startTime;
// animation time dependent
g.fillRect((int) (timeDiff / 100), 10, 10, 10);
}
}
public class MainLoop implements Runnable {
#Override
public void run() {
while (true) {
// trigger repaint
TestFrame.this.repaint();
try {
Thread.sleep(20L);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestFrame();
}
});
}
public TestFrame() {
// init window
this.setTitle("Test");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.add(new MainPanel());
this.pack();
this.setVisible(true);
// start render loop
Thread loop = new Thread(new MainLoop());
loop.start();
}
}
Your painting on my machine is rather smooth but it is burning a lot of performance in that loop, but on a slower machine I could imagine that the animation is jumpy if your application is busy executing the while or handling the paint events instead of rendering.
It may be better to update the position based on how much time has elapsed per render, and I am unsure how accurate it is to compare times for animation purposes through Date objects, so comparing small time differences using System.nanoTime() may be better. For example:
long currentNanoTime = System.nanoTime();
long timeElapsed = currentNanoTime - lastUpdateNanoTime;
lastUpdateNanoTime = currentNanoTime;
...
int newXPosition = oldXPosition + (velocityXInPixelsPerNanoSecond * timeElapsed);
Related
I'm trying to create a game loop in java which has a frame cap which can be set while the game is running. The problem im having is I have a render() function and a update() function. Just setting frame cap for both render() and update() means that the speed of the game logic will change when you change the frame cap. Any idea how to have a frame cap which can be set in game while not affecting the speed of the game logic (update())?
As stated in the comments: You can create two threads one of which is responsible for updating and the other is responsible for rendering.
Try to think of an architecture that suits your needs and your game. You can try something like:
import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Runner extends JPanel {
private static final long serialVersionUID = -5029528072437981456L;
private JFrame window;
private Renderer renderer;
public Runner() {
setPreferredSize(new Dimension(WindowData.WIDTH, WindowData.HEIGHT));
setFocusable(true);
requestFocus();
window = new JFrame(WindowData.TITLE);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.add(this);
window.setResizable(false);
window.pack();
window.setLocationRelativeTo(null);
window.setVisible(true);
renderer = new Renderer(getGraphics());
renderer.start();
}
public static void main(String[] args) {
new Runner();
}
}
import java.awt.Graphics;
public class Renderer implements Runnable {
private Graphics context;
private Thread thread;
private boolean running;
public Renderer(Graphics context) {
this.context = context;
thread = new Thread(this, "Renderer");
running = false;
}
public void start() {
if (running)
return;
running = true;
thread.start();
}
public void render() {
context.clearRect(0, 0, WindowData.WIDTH, WindowData.HEIGHT);
context.fillRect(50, 50, 100, 100);
}
public void run() {
while (running) {
render();
// ** DO YOUR TIME CONTROL HERE ** \\
}
}
}
This code will actually lead to serious performance issues because you are not in control over the rendering ( repaint() ) time.
But this is just a demo to show you how you can use different threads. Do your own architecture.
I am trying to show a label from another class. However when I add it to the frame it will not show. I have tried drawing it from the counter class itself by passing in the Frame which I would assume is not good practice (ignoring the fact it didn't work). As well as what is in the code below. Can anybody help me and explain why my solution will not show the created label? As you can most likely tell i'm very new to using JPanel.
CookieChaser Class
public class CookieChaser extends JPanel {
public static void main(String[] args) throws InterruptedException {
JFrame frame = new JFrame("Cookie Chaser");
CookieChaser game = new CookieChaser();
frame.add(game);
frame.setSize(1000, 1000);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
counter Score = new counter(frame);
cookie Cookie = new cookie();
JLabel item = counter.getLabel();
frame.add(item);
frame.setVisible(true);
while (true) {
game.repaint();
Thread.sleep(10);
}
}
#Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
}
Counter Class
public class counter {
int count;
static JLabel text;
public counter(JFrame frame){
count = 0;
text = new JLabel(String.valueOf(count));
text.setLocation(0,0);
text.setSize(50,50);
}
public static JLabel getLabel(){
return text;
}
I modified your code to create the following Swing GUI.
Whenever I create a Swing game or application, I use the model / view / controller pattern. This means I create a GUI model. The GUI model contains all of the fields that my GUI needs. Next, I create a GUI view which reads the values from the GUI model. Finally, I create one or more GUI controllers, which update the GUI model and refresh / repaint the GUI view.
I made the following changes to your code:
I created a GUI model. I created the Counter class. All the Counter class does is hold a counter value.
I created a GUI view, which uses the GUI model. I created the JFrame, JPanel, and JLabel all in the view class. You may use more than one class to create the view. Since this view was simple, I created everything in one class.
All Swing applications must start with a call to the SwingUtilities invokeLater method. The invokeLater method puts the creation and updating of the Swing components on the Event Dispatch thread. Oracle and I insist that all Swing applications start this way.
I created a separate Animation runnable so that you can see the JLabel updates. I increment the counter once a second.
The repaint method in the Animation class calls the SwingUtilities invokeLater method to ensure that the JLabel update is done on the Event Dispatch thread. The animation loop runs in a separate thread to keep the GUI responsive.
Here's the revised code.
package com.ggl.testing;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class CookieChaser implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new CookieChaser());
}
private JLabel counterLabel;
#Override
public void run() {
Counter counter = new Counter();
JFrame frame = new JFrame("Cookie Chaser");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel mainPanel = new JPanel();
counterLabel = new JLabel(" ");
mainPanel.add(counterLabel);
frame.add(mainPanel);
frame.setSize(300, 200);
frame.setVisible(true);
new Thread(new Animation(this, counter)).start();
}
public void setCounterLabel(String text) {
counterLabel.setText(text);
}
public class Counter {
private int counter;
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
public void incrementCounter() {
this.counter++;
}
}
public class Animation implements Runnable {
private Counter counter;
private CookieChaser cookieChaser;
public Animation(CookieChaser cookieChaser, Counter counter) {
this.cookieChaser = cookieChaser;
this.counter = counter;
}
#Override
public void run() {
while (true) {
counter.incrementCounter();
repaint();
sleep(1000L);
}
}
private void repaint() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
cookieChaser.setCounterLabel(Integer.toString(counter
.getCounter()));
}
});
}
private void sleep(long duration) {
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
}
}
}
}
I'm changing "views" with cardLayout (this class has a JFrame variable). When a user clicks a new game button this happens:
public class Views extends JFrame implements ActionListener {
private JFrame frame;
private CardLayout cl;
private JPanel cards;
private Game game;
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
if (command.equals("New game")) {
cl.show(cards, "Game");
game.init();
this.revalidate();
this.repaint();
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
game.loop();
}
});
}
}
}
Game's loop method and heading of class:
public class Game extends JPanel implements KeyListener {
public void loop() {
while (player.isAlive()) {
try {
this.update();
this.repaint();
// first class JFrame variable
jframee.getFrame().repaint();
// first class JFrame variable
jframee.getFrame().revalidate();
Thread.sleep(17);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void update() {
System.out.println("updated");
}
}
I'm painting using paintComponent()
public void paintComponent(Graphics g) {
System.out.println("paint");
...
}
Actually it's not painting anything. When I do not call loop() method (so it paints it just once) all images are painted correctly. But when I call loop() method, just nothing is happening in the window. (Even close button on JFrame doesn't work.)
How to fix that? (When I was creating JFrame inside game class everything worked fine, but now I want to have more views so I need JFrame in other class.)
Thanks.
Precursor: The Event Dispatch Thread (EDT).
Swing is single-threaded. What does this mean?
All processing in a Swing program begins with an event. The EDT is a thread that processes these events in a loop along the following lines (but more complicated):
class EventDispatchThread extends Thread {
Queue<AWTEvent> queue = ...;
void postEvent(AWTEvent anEvent) {
queue.add(anEvent);
}
#Override
public void run() {
while (true) {
AWTEvent nextEvent = queue.poll();
if (nextEvent != null) {
processEvent(nextEvent);
}
}
}
void processEvent(AWTEvent theEvent) {
// calls e.g.
// ActionListener.actionPerformed,
// JComponent.paintComponent,
// Runnable.run,
// etc...
}
}
The dispatch thread is hidden from us through abstraction: we generally only write listener callbacks.
Clicking a button posts an event (in native code): when the event is processed, actionPerformed is called on the EDT.
Calling repaint posts an event: when the event is processed, paintComponent is called on the EDT.
Calling invokeLater posts an event: when the event is processed, run is called on the EDT.
Everything in Swing begins with an event.
Event tasks are processed in sequence, in the order they are posted.
The next event can only be processed when the current event task returns. This is why we cannot have an infinite loop on the EDT. actionPerformed (or run, as in your edit) never returns, so the calls to repaint post paint events but they are never processed and the program appears to freeze.
This is what it means to "block" the EDT.
There are basically two ways to do animation in a Swing program:
Use a Thread (or a SwingWorker).
The benefit to using a thread is that the processing is done off the EDT, so if there is intensive processing, the GUI can still update concurrently.
Use a javax.swing.Timer.
The benefit to using a timer is that the processing is done on the EDT, so there is no worry about synchronization, and it is safe to change the state of the GUI components.
Generally speaking, we should only use a thread in a Swing program if there's a reason to not use a timer.
To the user, there is no discernible difference between them.
Your call to revalidate indicates to me that you are modifying the state of the components in the loop (adding, removing, changing locations, etc.). This is not necessarily safe to do off the EDT. If you are modifying the state of the components, it is a compelling reason to use a timer, not a thread. Using a thread without proper synchronization can lead to subtle bugs that are difficult to diagnose. See Memory Consistency Errors.
In some cases, operations on a component are done under a tree lock (Swing makes sure they are thread-safe on their own), but in some cases they are not.
We can turn a loop of the following form:
while ( condition() ) {
body();
Thread.sleep( time );
}
in to a Timer of the following form:
new Timer(( time ), new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if ( condition() ) {
body();
} else {
Timer self = (Timer) evt.getSource();
self.stop();
}
}
}).start();
Here is a simple example demonstrating animation both with a thread and a timer. The green bar moves cyclically across the black panel.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class SwingAnimation implements Runnable{
public static void main(String[] args) {
SwingUtilities.invokeLater(new SwingAnimation());
}
JToggleButton play;
AnimationPanel animation;
#Override
public void run() {
JFrame frame = new JFrame("Simple Animation");
JPanel content = new JPanel(new BorderLayout());
play = new JToggleButton("Play");
content.add(play, BorderLayout.NORTH);
animation = new AnimationPanel(500, 50);
content.add(animation, BorderLayout.CENTER);
// 'true' to use a Thread
// 'false' to use a Timer
if (false) {
play.addActionListener(new ThreadAnimator());
} else {
play.addActionListener(new TimerAnimator());
}
frame.setContentPane(content);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
abstract class Animator implements ActionListener {
final int period = ( 1000 / 60 );
#Override
public void actionPerformed(ActionEvent ae) {
if (play.isSelected()) {
start();
} else {
stop();
}
}
abstract void start();
abstract void stop();
void animate() {
int workingPos = animation.barPosition;
++workingPos;
if (workingPos >= animation.getWidth()) {
workingPos = 0;
}
animation.barPosition = workingPos;
animation.repaint();
}
}
class ThreadAnimator extends Animator {
volatile boolean isRunning;
Runnable loop = new Runnable() {
#Override
public void run() {
try {
while (isRunning) {
animate();
Thread.sleep(period);
}
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
};
#Override
void start() {
isRunning = true;
new Thread(loop).start();
}
#Override
void stop() {
isRunning = false;
}
}
class TimerAnimator extends Animator {
Timer timer = new Timer(period, new ActionListener() {
#Override
public void actionPerformed(ActionEvent ae) {
animate();
}
});
#Override
void start() {
timer.start();
}
#Override
void stop() {
timer.stop();
}
}
static class AnimationPanel extends JPanel {
final int barWidth = 10;
volatile int barPosition;
AnimationPanel(int width, int height) {
setPreferredSize(new Dimension(width, height));
setBackground(Color.BLACK);
barPosition = ( width / 2 ) - ( barWidth / 2 );
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int width = getWidth();
int height = getHeight();
int currentPos = barPosition;
g.setColor(Color.GREEN);
g.fillRect(currentPos, 0, barWidth, height);
if ( (currentPos + barWidth) >= width ) {
g.fillRect(currentPos - width, 0, barWidth, height);
}
}
}
}
What does update do? You probably shouldnt call game.loop() on the EDT. You are running a loop on EDT, your repaint wont ever be invoked since repaint queues an event on EDT and it seems kind busy. Try moving game.loop() to another thread
new Thread(new Runnable() {
#override
public void run() {
game.loop();
}
}).start();
This way you wont block the EDT while the repaint still gets to be executed on the EDT.
Move game.loop() method invocation to something like:
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
game.loop()
}
});
Thanks.
I am trying to get a simple fast forward button to work. My understanding is initializing a timer as such
timer = new Timer(setspeed, listener);
sets the delay between timer events to the int setspeed in milliseconds.
i have a fast forward button that has the following code:
public void doFastForward()
{
speedcounter++;
setspeed = speed / speedcounter;
System.out.print(speedcounter + " " + setspeed + ". "); //checker
timer.stop();
timer.setDelay((setspeed));
timer.start();
System.out.print(timer.getDelay() + ".. "); //checker
}
which is supposed to cut the speed by half, third, fourth, etc, with every button press. Issue is its totally not doing that with my simulation. Is there something I'm missing here?
timer.setDelay() works for me. Here's a quick example that moves a red square across the screen. Pressing the fast-forward button makes it move faster by calling setDelay on the timer (you'll notice the logic in my ActionListener is identical to yours, albeit different variable names):
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class TimerTest extends JFrame {
public static void main(String[] args) { new TimerTest().setVisible(true); }
private static final int DEFAULT_SPEED = 500;
private int speedCounter = 1;
private int currentSpeed = DEFAULT_SPEED / speedCounter;
private int squareX = 150;
public TimerTest() {
super("Test");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setMinimumSize(new Dimension(300, 200));
setLayout(new BorderLayout());
setLocationRelativeTo(null);
JPanel displayPanel = new JPanel(null) {
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.RED);
g.fillRect(squareX, getHeight() / 2, 25, 25);
}
};
final Timer timer = new Timer(currentSpeed, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
squareX = (squareX + 15) % getWidth();
repaint();
}
});
timer.setRepeats(true);
JButton fastForwardButton = new JButton(new AbstractAction(">>") {
#Override
public void actionPerformed(ActionEvent e) {
speedCounter++;
currentSpeed = DEFAULT_SPEED / speedCounter;
timer.stop();
timer.setDelay(currentSpeed);
timer.start();
}
});
add(displayPanel, BorderLayout.CENTER);
add(fastForwardButton, BorderLayout.SOUTH);
timer.start();
}
}
One noticeable flaw with this approach is that starting/stopping the timer will cause an additional delay. For example, say the timer was set to fire every 2 seconds and it has been 1.5 seconds since the last frame. If you set the delay to 1 second and restart the timer, 2.5 seconds will have passed before the next frame fires. This could explain why "it didn't work" for you.
To avoid this issue, you could create a timer that fires at some fixed rate but only executes your logic if enough time has passed. Then change what "enough time" is when the user fast-forwards. For example, the timer could fire every 50ms, but you only execute your logic if 500ms has passed since the last execution. When the user fast-forwards, you could then execute your logic if 250ms has passed instead (and so on).
I was wondering if there was a function like void draw() which Processing programming language uses that gets called every frame. Or even just a function that loops infinitely when it gets called but only runs through it every time there is a new frame. I heard of something called a runnable in java how do i go about using this? also is there a better way then having a runnable with a delay like a function that is hardcoded to run every frame. Oh and also what is the function call that will allow me to see how much time (in milliseconds preferably) since the application has started running that way i can make my runnables / frame calls much more precise so that the game runs about the same speed on every computer regardless of the frame rate.
Perhaps you need something like this
import java.awt.Graphics;
import java.awt.Point;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class Repainter extends JPanel {
private Point topLeft;
private int increamentX = 5;
public Repainter() {
topLeft = new Point(100, 100);
}
public void move() {
topLeft.x += increamentX;
if (topLeft.x >= 200 || topLeft.x <= 100) {
increamentX = -increamentX;
}
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawRect(topLeft.x, topLeft.y, 100, 100);
}
public void startAnimation() {
SwingWorker<Object, Object> sw = new SwingWorker<Object, Object>() {
#Override
protected Object doInBackground() throws Exception {
while (true) {
move();
Thread.sleep(100);
}
}
};
sw.execute();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Repaint Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 400);
Repainter repainter = new Repainter();
frame.add(repainter);
repainter.startAnimation();
frame.setVisible(true);
}
});
}
}