I'm making a game in Java Swing. In it I have fairly simple graphics and not that many logic operations currently so I was confused why my frameTimes never went below 15ms, even I ran it at bare minimum.
Here's the declaration of one of my Timers as well as the bare minimum call I mentioned before.
logicTimer = new Timer(1, new TickTimer());
logicTimer.setInitialDelay(0);
...
class TickTimer implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Logic processing took "+ (System.currentTimeMillis()-previousTime)+" ms");
tick();
previousTime = System.currentTimeMillis();
}
}
public void tick() {}
I have an essentially identical Timer for rendering as well. I render into a JPanel which is contained in a JFrame
For some additional background I'm having these results on a gaming laptop (Lenovo Y720) with a GTX 1060 MaxQ, an i7-7700HQ and 16GB RAM. I also have a similarly specced PC (1070, 7700K) that didn't have this issue if my memory serves right, but I can't test it right now. The laptop does have 60Hz screen, but that shouldn't matter as far as I know.
So I tried running my Timers with the bare minimum calls, but the frameTime was still either 15ms, 16ms, 30ms or 31ms, even though I set the delay to the minimum 1ms with no initial delay. I tried reducing the game's window size but that also had no effect. I tried unplugging my monitor that is connected to the laptop but the issue persisted. I tried closing any background windows and setting the JDK executable to high and realtime priorities in Windows to no avail.
You might say "15ms? THats still 60 fps, what is there to complain about?" but the problem is that I will have to present this game in a few months, likely on this laptop, and while the performance is just OK now, if I add complicated/lots of logic or visuals then a spike would be immediately noticable. This is assuming that this 15-30ms is a baseline and that if I had 1ms of processing that would result in 1ms higher base latency/frametime too.
After reading some previous questions regarding this issue lots of them came to the conclusion that this is an inherent and unfixable issue, but more modern threads talked about how this was solved with newer version of Swing where better precision Timers were used, so the source of the issue on modern hardware is the use a 32-bit JVM, but I'm fairly sure that I'm running 64-bit, as checked with System.getProperty("sun.arch.data.model"). Not sure if relevant but my JDK version is "17.0.2".
You might be running the application on Windows, where the default timer resolution is 15.6 ms.
JDK can temporarily increase the resolution of a system timer, when you call Thread.sleep with an interval not multiple to 10ms:
HighResolutionInterval(jlong ms) {
resolution = ms % 10L;
if (resolution != 0) {
MMRESULT result = timeBeginPeriod(1L);
}
}
Note: this is not a documented feature, and may be changed in any future version of JDK.
So the workaround is to start a background thread that calls Thread.sleep with an odd interval:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class HiResTimer {
public static void enableHiResTimer() {
Thread thread = new Thread("HiResTimer") {
#Override
public void run() {
while (true) {
try {
sleep(Integer.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
};
thread.setDaemon(true);
thread.start();
}
public static void main(String[] args) throws Exception {
enableHiResTimer();
new javax.swing.Timer(1, new ActionListener() {
long lastTime;
#Override
public void actionPerformed(ActionEvent e) {
System.out.println((System.nanoTime() - lastTime) / 1e6);
lastTime = System.nanoTime();
}
}).start();
Thread.currentThread().join();
}
}
However, this is a hack, and I'd suggest against doing so. The higher timer resolution increases CPU usage and power consumption.
Bare bones example, running at 1 millisecond
Top text is the time between timer ticks and the middle is the time between repaints.
This demonstrates that there is a 1 millisecond delay between repaints (it does bounce a bit, so it might 1-3 milliseconds). I prefer to use a 5 millisecond delay as there are overheads with the thread scheduling which might make it ... "difficult" to achieve 1 millisecond precision
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JLabel;
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() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private Instant lastRepaintTick;
private Instant lastTick;
private JLabel label = new JLabel();
public TestPane() {
setLayout(new BorderLayout());
label.setHorizontalAlignment(JLabel.CENTER);
add(label, BorderLayout.NORTH);
Timer timer = new Timer(1, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (lastTick != null) {
label.setText(timeBetween(lastTick, Instant.now()));
}
repaint();
lastTick = Instant.now();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastRepaintTick != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = (getHeight() - fm.getHeight()) / 2;
g2d.drawString(text, x, y + fm.getAscent());
g2d.dispose();
}
lastRepaintTick = Instant.now();
}
}
}
Note, this is not the "simplest" workflow, it's doing a lot work, creating a bunch of short lived objects, which, in of itself, could cause a drag on the frame rate. This leads me to think that your frame calculations are off.
I also tried...
System.out.println("Logic processing took " + (System.currentTimeMillis() - previousTime) + " ms");
if (lastTick != null) {
label.setText(timeBetween(lastTick, Instant.now()));
}
repaint();
lastTick = Instant.now();
previousTime = System.currentTimeMillis();
which prints 1-2 milliseconds. But please note System.out.println adds additional overhead and might slow down your updates
Java is going to be limited by the underlying hardware/software/driver/JVM implementation. Swing has been using DirectX or OpenGL pipelines for years (at least +5). If you're experiencing issues on one machine and not another, I'd be trying to explorer the differences between those two machines. I've had (lots) of issues with integrated video cards over the years and this tends to come down to the driver on the OS.
15ms is roughly 66fps, so it "could" be a delay in the rendering pipeline limited by the screen/driver/video hardware (guessing), but all my screens are running at 60hz and I don't have issues (I'm running 3 off my MacBook Pro), although I am running Java 16.0.2.
I've also been playing around with this example over the past few weeks. It's a simple concept of using "line boundary checking for hit detection". The idea was to use a "line of sight" projected through a field of animated objects to determine which ones could be "seen". The original question was suggesting that their algorithm has issues (frame drops) with < 100 entities.
The test I have has 15, 000 animated entities (and 18 fixed entities) and tends to run between 130-170fps (7.69-5.88 milliseconds) with a Timer set to a 5 millisecond interval. This example is NOT optimised and I found that drawing the "hits" was causing most of the slow down, but I don't honestly know exactly why.
My point been, that a relatively efficient model shouldn't see a large "degradation" of frame rate and a relatively efficient model should also be able to compensate for it.
If frame rate is really super important to you, then you're going to need to explore using a "active rendering" model (as apposed to the passive rendering model use by Swing), see BufferStrategy and BufferCapabilities for more details
I just tried your barebones 1ms example and its showing essentially the same results: 0s.01X where the X is 5, 6 or 7. This virtually guarantees that my problem doesn't have to do with my code, what other fixes can I try?
Well, nothing really. This seems to be a limitation of the hardware/drivers/OS/rendering pipeline.
The question, though, why is it so important? Generally speaking, 15ms is roughly 60fps. Why would you need more?
If you absolutely, without question, must have, the fastest rendering through put, then I'd suggest having a look at using a BufferStrategy. This takes you out of the overhead of the Swing rendering workflows, for example...
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
Frame frame = new Frame();
frame.setTitle("Make it so");
Canvas canvas = new Canvas() {
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
};
frame.add(canvas);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
canvas.createBufferStrategy(3);
Instant lastRepaintTick = null;
Instant lastTick = null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSSS");
do {
BufferStrategy bs = canvas.getBufferStrategy();
while (bs == null) {
System.out.println("buffer");
bs = canvas.getBufferStrategy();
}
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
System.out.println("draw");
Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
// Render to graphics
// ...
g2d.setColor(Color.LIGHT_GRAY);
g2d.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
if (lastRepaintTick != null) {
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
int y = (canvas.getHeight() - fm.getHeight()) / 2;
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(formatter);
x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
}
lastRepaintTick = Instant.now();
g2d.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (bs.contentsRestored());
System.out.println("show");
// Display the buffer
bs.show();
// Repeat the rendering if the drawing buffer was lost
} while (bs.contentsLost());
System.out.println("done");
try {
Thread.sleep(5);
} catch (InterruptedException ex) {
}
} while (true);
}
protected static String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
}
I did have this running without a loop, but in general, "wild loops" are a bad idea, as they tend to starve other threads.
BufferStrategy example does finally result in frametimes lower than 15ms, so I guess the limitation is somewhere in Timer on my hardware.
Just be careful with what's been displayed here. The BufferStrategy example is displaying the time between each LOOP cycle, not each frame. AFAIK there's no way to determine when a frame is actually painted on the screen (or if it is).
You could "try" doing a side by side comparison, but you might find it hard to tell the difference between the two, again, the time between the BufferStrategy rendering a page and presenting might have some drift, so the value "presented" by the BufferStrategy may be stale. Keep it in mind.
Canvas on top
import java.awt.BorderLayout;
import java.awt.Canvas;
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.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.swing.JFrame;
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() {
JFrame frame = new JFrame();
frame.setLayout(new GridLayout(2, 1));
TestCanvas testCanvas = new TestCanvas();
frame.add(testCanvas);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
testCanvas.start();
}
});
}
private static final long TIME_DELAY = 5;
private static final Dimension PREFERRED_SIZE = new Dimension(200, 35);
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.nnnnnnnnn");
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
public class TestPane extends JPanel {
private Instant lastRepaintTick;
private Instant lastTick;
public TestPane() {
setLayout(new BorderLayout());
Timer timer = new Timer((int) TIME_DELAY, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
repaint();
lastTick = Instant.now();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return PREFERRED_SIZE;
}
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastRepaintTick != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = 0;
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(TIME_FORMATTER);
x = (getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
g2d.dispose();
}
lastRepaintTick = Instant.now();
}
}
public class TestCanvas extends Canvas {
private Thread thread;
public TestCanvas() {
thread = new Thread(new Runnable() {
#Override
public void run() {
createBufferStrategy(3);
Instant lastRepaintTick = null;
do {
BufferStrategy bs = getBufferStrategy();
while (bs == null) {
bs = getBufferStrategy();
}
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
// Render to graphics
// ...
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
if (lastRepaintTick != null) {
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = 0;
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(TIME_FORMATTER);
x = (getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
}
lastRepaintTick = Instant.now();
g2d.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (bs.contentsRestored());
// Display the buffer
bs.show();
// Repeat the rendering if the drawing buffer was lost
} while (bs.contentsLost());
Instant tickTime = Instant.now();
try {
Thread.sleep(TIME_DELAY);
} catch (InterruptedException ex) {
}
} while (true);
}
});
}
public void start() {
thread.start();
}
#Override
public Dimension getPreferredSize() {
return PREFERRED_SIZE;
}
}
}
Related
I am trying to draw a circle around a point while allowing me to still manually move the cursor. I figured out how to draw the circle around a point but if I bump my mouse a little bit it screws up big time. I know that if I move the mouse a bit while making the circle it won't likely come out as a perfect circle but that's fine. My goal is to be able to drag the mouse around with it constantly trying to make circles around that moving point. Here is the code I have so far (I have tried multiple iterations).
int radius = 100;
for (double i = 0; i < (2 * Math.PI) + Math.PI / 6; i = i + Math.PI / 6) {
PointerInfo pointerA = MouseInfo.getPointerInfo();
Point a = pointerA.getLocation();
int yStart = (int) a.getY();
int xStart = (int) a.getX();
robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
robot.mouseMove((int) ((xStart) + (radius * Math.cos(i))), (int) ((yStart) + (radius * Math.sin(i))));
robot.delay(68);
}
This is what it looks like when I don't move my cursor at all...
This is what it looks like when I move my cursor a bit...
I also know that this will only make one circle but I can't figure out how to run this code when my nativeMousePressed event occurs and then stop it when the nativeMouseReleased event occurs. I tried to run the code above in a method that contains a while loop but that did not work. I assigned a boolean to true when the mouse is clicked and then assigned it to false when the mouse button is released but I think the while loop was working on a different thread or something because none of the code would work besides from the infinite while loop. Needless to say, I removed the while loop, for now, to try and figure out why the circle was not printing right :/
Here is a little edit since I may not have made what I am trying to do clear. I am trying to write code that creates circles while allowing me to move my mouse around the screen (I don't want it to recenter). Now assuming I moved my cursor to the right and the code ran in a while loop instead of only creating 1 circle it should look something like this. (I also need to figure out why all those lines appeared because I even printed the locations which ended up looking nearly the same as when I did not move the mouse).
This problem is caused by the 68ms delay in which the user is free to control the mouse. In the ideal case, you would use JNI or JNA to temporarily disable the mouse on an OS level. That however comes with its own challenges, including that you might lock yourself out while testing. Since you are okay with a bit of jitter, there is an easier alternative. Instead of waiting 68ms, you could spam-place the mouse to the desired location, decreasing the time a user has to move the mouse before it gets set again. You can do that as follows:
for(int i = 0; i < 68; i++) {
robot.mouseMove(...);
robot.delay(1);
}
Or if the 68ms has to be somewhat precise:
long startTime = System.currentTimeMillis();
while(System.currentTimeMillis() - startTime < 68) {
robot.mouseMove(...);
robot.delay(1);
}
You can play around with the delay to see what happens. A zero delay might be heavy on the CPU.
This is a really basic demonstration.
The "problem" here is, the MouseMotionListener can't tell the difference between the Robot moving the mouse or the user moving the mouse. So, if you move the mouse while it's drawing, it will add those points as well (you can see it demonstrated below)
One thing I did note was the fact that you never release the mouse button, so it will continue to trigger mouseDragged events to the underlying window/canvas even after your loop has completed.
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (AWTException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
}
public class TestPane extends JPanel {
private List<List<Point>> points = new ArrayList<List<Point>>(25);
private List<Point> activeList;
private Robot bot;
volatile private boolean isRunning = false;
public TestPane() throws AWTException {
bot = new Robot();
addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
if (!isRunning) {
return;
}
System.out.println("...mousePressed");
activeList = new ArrayList<>(25);
points.add(activeList);
}
#Override
public void mouseReleased(MouseEvent e) {
if (!isRunning) {
return;
}
System.out.println("...mouseReleased");
activeList = null;
}
});
addMouseMotionListener(new MouseAdapter() {
#Override
public void mouseDragged(MouseEvent e) {
if (!isRunning) {
return;
}
System.out.println("...mouseDragged");
activeList.add(e.getPoint());
repaint();
}
#Override
public void mouseMoved(MouseEvent e) {
if (!isRunning) {
return;
}
System.out.println("...mouseMoved");
}
});
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
am.put("start", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() {
#Override
public void run() {
isRunning = true;
drawCircle();
isRunning = false;
}
}).start();
}
});
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "start");
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (points.isEmpty()) {
return;
}
Graphics2D g2d = (Graphics2D) g.create();
for (List<Point> sequence : points) {
List<Point> copy = new ArrayList<>(sequence);
Point last = copy.remove(0);
for (Point next : copy) {
g2d.drawLine(last.x, last.y, next.x, next.y);
last = next;
}
}
g2d.dispose();
}
protected void drawCircle() {
int radius = 100;
PointerInfo pointerA = MouseInfo.getPointerInfo();
Point a = pointerA.getLocation();
bot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
int xStart = (int) a.getX();
int yStart = (int) a.getY();
bot.delay(5);
for (double i = 0; i < (2 * Math.PI) + Math.PI / 6; i = i + Math.PI / 6) {
int xPos = (int) ((xStart) + (radius * Math.cos(i)));
int yPos = (int) ((yStart) + (radius * Math.sin(i)));
System.out.println(xPos + "x" + yPos);
bot.mouseMove(xPos, yPos);
bot.delay(5);
}
bot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
bot.delay(5);
bot.mouseMove(xStart, yStart);
}
}
}
So, you problem is a little understated in it's context/intent, so it's difficult to really make a suitable suggestion, but remember, you're not dealing with an actual "API" you can inject information into, you're dealing with posting events in the system event queue which are then been consumed by the window which currently has focus
This is what I have in my paintComponent (most other things omitted, just the stuff that pertains to an Item object called chalice with a polygon field, the explicit parameters of the if statement are not important for this question)
Currently, it shows up as solid white because I set the color to all 255, but I want to make it gradually transition to different colors smoothly, not strobing, more like pulsating but I don't really know what that is called. I was thinking about replacing the explicit parameters of the Color with arrays that cycle through numbers in that array and somehow link that to a TimerListener, but I am new to graphics so I am not sure if that is the best way to go about this.
public void paintComponent(Graphics g) {
Graphics2D sprite = (Graphics2D) g;
if (chalice.getHolding() == true || roomID == chalice.getRoomDroppedIn()) {
sprite.setColor(new Color(255, 255, 255));
sprite.fill(chalice.getPoly());
}
}
Some basic concepts...
A pulsating effect needs to move in two direction, it needs to fade in AND out
In order to know how much "effect" should be applied, two things need to be known. First, how long the overall effect takes to cycle (from fully opaque to fully transparent and back again) and how far through the cycle the animation is.
This is not a simple thing to manage, there's a lot of "stateful" information which needs to be managed and maintained, and normally, done so separately from other effects or entities.
To my mind, the simplest solution is to devise some kind of "time line" which manages key points (key frames) along the time line, calculates the distance between each point and the value that it represents.
Take a step back for a second. We know that at:
0% we want to be fully opaque
50% we want to be fully transparent
100% we want to be fully opaque
The above takes into consideration that we want to "auto reverse" the animation.
The reason for working with percentages, is that it allows us to define a timeline of any given duration and the timeline will take care of the rest. Where ever possible, always work with normalised values like this, it makes the whole thing a lot simpler.
TimeLine
The following is a pretty simple concept of a "timeline". It has a Duration, the time over which the timeline is played, key frames, which provide key values over the duration of the timeline and the means to calculate a specific value at a specific point over the life of the timeline.
This implementation also provides "auto" replay-ability. That is, if the timeline is played "over" it's specified Duration, rather then stopping, it will automatically reset and take into consideration the amount of time "over" as part of it's next cycle (neat)
public class TimeLine {
private Map<Float, KeyFrame> mapEvents;
private Duration duration;
private LocalDateTime startedAt;
public TimeLine(Duration duration) {
mapEvents = new TreeMap<>();
this.duration = duration;
}
public void start() {
startedAt = LocalDateTime.now();
}
public boolean isRunning() {
return startedAt != null;
}
public float getValue() {
if (startedAt == null) {
return getValueAt(0.0f);
}
Duration runningTime = Duration.between(startedAt, LocalDateTime.now());
if (runningTime.compareTo(duration) > 0) {
runningTime = runningTime.minus(duration);
startedAt = LocalDateTime.now().minus(runningTime);
}
long total = duration.toMillis();
long remaining = duration.minus(runningTime).toMillis();
float progress = remaining / (float) total;
return getValueAt(progress);
}
public void add(float progress, float value) {
mapEvents.put(progress, new KeyFrame(progress, value));
}
public float getValueAt(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;
float blend = blend(keyFrames[0].getValue(), keyFrames[1].getValue(), 1f - weight);
return blend;
}
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 float blend(float start, float end, float ratio) {
float ir = (float) 1.0 - ratio;
return (float) (start * ratio + end * ir);
}
public class KeyFrame {
private float progress;
private float value;
public KeyFrame(float progress, float value) {
this.progress = progress;
this.value = value;
}
public float getProgress() {
return progress;
}
public float getValue() {
return value;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
Setting up the timeline is pretty simple...
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
We give a specified Duration and setup the key frame values. After that we just need to "start" it and get the current value from the TimeLine based on how long it's been playing.
This might seem like a lot of work for what seems like a simple problem, but remember, this is both dynamic and re-usable.
It's dynamic in that you can provide any Duration you want, thus changing the speed, at it will "just work", and re-usable, as you can generate multiple instances for multiple entities and it will be managed independently.
Example...
The following example simply makes use of Swing Timer to act as the "main-loop" for the animation. On each cycle, it asks the TimeLine for the "current" value, which simply acts as the alpha value for the "pulsating" effect.
The TimeLine class itself is decoupled enough that it won't matter "how" you've establish your "main-loop", you simply start it running and pull the "current" value from it when ever you can...
import java.awt.AlphaComposite;
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 java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.TreeMap;
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 {
private TimeLine timeLine;
private float alpha = 0;
public TestPane() {
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (!timeLine.isRunning()) {
timeLine.start();
}
alpha = timeLine.getValue();
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive(alpha));
g2d.setColor(Color.RED);
g2d.fill(new Rectangle(45, 45, 110, 110));
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(getBackground());
g2d.fill(new Rectangle(50, 50, 100, 100));
g2d.setColor(Color.BLACK);
g2d.draw(new Rectangle(50, 50, 100, 100));
g2d.dispose();
}
}
}
I would tie up the TimeLine as part of the effect for the specified entity. This would tie the TimeLine to a specific entity, meaning that many entities could all have their own TimeLines calculating a verify of different values and effects
"Is there a simpler solution?"
This is a subjective question. There "might" be a "simpler" approach which will do the same job, but which won't be as scalable or re-usable as this kind of approach.
Animation is a complex subject, trying to make it work in a complex solution, running multiple different effects and entities just compounds the problem
I've toyed with the idea of making the TimeLine generic so it could be used to generate a verity of different values based on the desired result, making it a much more flexible and re-usable solution.
Color blending ....
I don't know if this is a requirement, but if you have a series of colors you want to blend between, a TimeLine would also help you here (you don't need the duration so much). You could set up a series of colors (acting as key frames) and calculate which color to use based on the progression of the animation.
Blending colors is somewhat troublesome, I've spent a lot of time trying to find a decent algorithm which works for me, which is demonstrated at Color fading algorithm? and Java: Smooth Color Transition
So basically I'm trying to create countdown when my code is run,
which is fine and working the only problem I have is that instead of the
numbers disappearing and being replaced by the new value of i, they stay the new value overlaps the existing number. I tried to use repaint () to fix this but it's not working. I'm wondering if there's something I'm missing or if I should try a different way altogether.
here's the code for the timer:
public void timer (Graphics g,int x){
x=550;
for (int i = time;i>0;i--){
g.setColor(deepPlum);
g.setFont(new Font("Monospaced", Font.PLAIN, 25));
g.drawString("time: "+i, x, 650);
repaint();
}
}
At a "guess"
I don't like guessing, but it seems we're not going to get a runnable example which would provide a clear understand of how the problem is been generated.
Evidence...
I do call the super paint method but in the paint method. I know that is fine because all of my other graphics are working fine.
This "suggests" that timer is been called as part of the normal Swing "paint" cycle. If that's true, it explains the reason for the issue.
This...
public void timer (Graphics g,int x){
x=550;
for (int i = time;i>0;i--){
g.setColor(deepPlum);
g.setFont(new Font("Monospaced", Font.PLAIN, 25));
g.drawString("time: "+i, x, 650);
repaint();
}
}
is simply painting all the numbers over each other, each time a paint cycle occurs, which really isn't how a count down timer should be painted.
Instead, it should simply be painting the remaining time, a single value, and not try and paint all of them.
You can think of a "paint cycle" as a "frame", it paints a snap-shot of the current state of the component. You need to paint a number of these "frames" in order to animate the changes from one point in time to another.
I would, again, strongly, recommend that you take the time to read through Performing Custom Painting and Painting in AWT and Swing to better understand how painting actually works.
Without more evidence, I would suggest, a Swing Timer would be a better solution. This could be used to calculate the remaining time and schedule a repaint on a regular bases.
Because I seem to do this kind of thing a lot, I wrapped up the core functionality into a simple Counter class, which has a "duration" (or runtime) and can calculate the duration it's been running, the remaining time and the progress as a percentage of the duration and runTime, probably more then you need, but it gives you the basics from which to start.
Because painting is a imprecise art (the time between paint cycles is variable, even when using a Timer), I've used a "time" based approach, rather then a simple "counter". This gives you a much higher level of precision when dealing with these type of scenarios
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javax.swing.JComponent;
import javax.swing.JFrame;
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());
// The following two lines just set up the
// "default" size of the frame
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JComponent {
private Counter counter;
public TestPane() {
Timer timer = new Timer(1000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (counter == null) {
counter = new Counter(Duration.of(10, ChronoUnit.SECONDS));
counter.start();
}
repaint();
}
});
timer.start();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (counter != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
long time = counter.getRemaining().getSeconds();
int x = (getWidth() - fm.stringWidth(Long.toString(time))) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g2d.drawString(Long.toString(time), x, y);
g2d.dispose();
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public static class Counter {
private Duration length;
private LocalDateTime startedAt;
public Counter(Duration length) {
this.length = length;
}
public Duration getLength() {
return length;
}
public void start() {
startedAt = LocalDateTime.now();
}
public void stop() {
startedAt = null;
}
public Duration getDuration() {
if (startedAt == null) {
return null;
}
Duration duration = Duration.between(startedAt, LocalDateTime.now());
if (duration.compareTo(getLength()) > 0) {
duration = getLength();
}
return duration;
}
public Double getProgress() {
if (startedAt == null) {
return null;
}
Duration duration = getDuration();
return Math.min(1.0, (double) duration.toMillis() / length.toMillis());
}
public Duration getRemaining() {
if (startedAt == null) {
return null;
}
Duration length = getLength();
Duration time = getDuration();
LocalDateTime endTime = startedAt.plus(length);
LocalDateTime runTime = startedAt.plus(time);
Duration duration = Duration.between(runTime, endTime);
if (duration.isNegative()) {
duration = Duration.ZERO;
}
return duration;
}
}
}
Before someone tells me how clearRect would solve the issue, it's important to note that clearRect will do two things. One, it would only display the "last" value of the loop, which is clearly not what the OP wants, and it will fill the specified area with the current background color of the Graphics context, which "may" not be what the OP want's (as it will paint a nice rectangle over the top of what ever else was painted before it).
If used properly, the Swing painting system will prepare the Graphics context BEFORE the component's paint method is called, rendering the need for using clearRect mute
Call clearRect to restore the background for a rectangular region before drawing again
Even after using Java Swing for over a year, it still seems like magic to me. How do I correctly use a BufferStrategy, in particular, the method createBufferSrategy()?
I would like to have a JFrame and a Canvas that gets added to it and then painted. I would also like to be able to resize (setSize()) the Canvas. Every time I resize the Canvas it seems my BufferStrategy gets trashed or rather, turns useless, since using show() on the BufferStrategy does not actually do anything. Also, createBufferStrategy() has a weird non-deterministic behaviour and I don't know how to synchronize it correctly.
Here's what I mean:
public class MyFrame extends JFrame {
MyCanvas canvas;
int i = 0;
public MyFrame() {
setUndecorated(false);
setVisible(true);
setSize(1100, 800);
setLocation(100, 100);
setDefaultCloseOperation(EXIT_ON_CLOSE);
canvas = new MyCanvas();
add(canvas);
canvas.makeBufferStrat();
}
#Override
public void repaint() {
super.repaint();
canvas.repaint();
//the bigger threshold's value, the more likely it is that the BufferStrategy works correctly
int threshold = 2;
if (i < threshold) {
i++;
canvas.makeBufferStrat();
}
}
}
MyCanvas has a method makeBufferStrat() and repaint():
public class MyCanvas extends Canvas {
BufferStrategy bufferStrat;
Graphics2D g;
public MyCanvas() {
setSize(800, 600);
setVisible(true);
}
public void makeBufferStrat() {
createBufferStrategy(2);
//I'm not even sure whether I need to dispose() those two.
if (g != null) {
g.dispose();
}
if (bufferStrat != null) {
bufferStrat.dispose();
}
bufferStrat = getBufferStrategy();
g = (Graphics2D) (bufferStrat.getDrawGraphics());
g.setColor(Color.BLUE);
}
#Override
public void repaint() {
g.fillRect(0, 0, 100, 100);
bufferStrat.show();
}
}
I simply call MyFrame's repaint() method from a while(true) loop in the main method.
When threshold is small (i.e. 2), bufferStrat.show() in about 70% of all cases doesn't do anything - the JFrame just remains gray upon starting the program. The remaining 30% it paints the rectangle how it's supposed to. If I do threshold = 200;, the painting succeeds close to 100% of the time I execute the program. Javadoc says that createBufferStrategy() may take a while, so I assume that's the issue here. However, how do I synchronize and use it properly? Clearly, I'm doing something wrong here. I can't imagine that's how it's supposed to be used.
Does anyone have a minimal working example?
The way you create the BufferStrategy is "okay", you could have a look at the JavaDocs for BufferStrategy which has a neat little example.
The way you're using it, is questionable. The main reason for using a BufferStrategy is because you want to take control of the painting process (active painting) away from Swing's painting algorithm (which is passive)
BUT, you seem to trying to do both, which is why it's causing your issues. Instead, you should have a "main" loop which is responsible for deciding what and when the buffer should paint, for example...
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferStrategy;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
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();
}
TestPane testPane = new TestPane();
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(testPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// The component needs to be attached to displayed window before
// the buffer can be created
testPane.startPainting();
}
});
}
public class TestPane extends Canvas {
private AtomicBoolean painting = new AtomicBoolean(true);
private PaintCycle paintCycle;
private Rectangle clickBounds;
public TestPane() {
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (clickBounds != null && clickBounds.contains(e.getPoint())) {
painting.set(false);
}
}
});
}
public void startPainting() {
if (paintCycle == null) {
createBufferStrategy(2);
painting.set(true);
paintCycle = new PaintCycle();
Thread t = new Thread(paintCycle);
t.setDaemon(true);
t.start();
}
}
public void stopPainting() {
if (paintCycle != null) {
painting.set(false);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
public class PaintCycle implements Runnable {
private BufferStrategy strategy;
private int xDelta = 2;
private int yDelta = 2;
#Override
public void run() {
System.out.println("Painting has started");
int x = (int) (Math.random() * (getWidth() - 40));
int y = (int) (Math.random() * (getHeight() - 40));
do {
xDelta = (int) (Math.random() * 8) - 4;
} while (xDelta == 0);
do {
yDelta = (int) (Math.random() * 8) - 4;
} while (yDelta == 0);
clickBounds = new Rectangle(x, y, 40, 40);
strategy = getBufferStrategy();
while (painting.get()) {
// Update the state of the model...
update();
// Paint the state of the model...
paint();
try {
// What ever calculations you want to use to maintain the framerate...
Thread.sleep(40);
} catch (InterruptedException ex) {
}
}
System.out.println("Painting has stopped");
}
protected void update() {
int x = clickBounds.x + xDelta;
int y = clickBounds.y + yDelta;
if (x + 40 > getWidth()) {
x = getWidth() - 40;
xDelta *= -1;
} else if (x < 0) {
x = 0;
xDelta *= -1;
}
if (y + 40 > getHeight()) {
y = getHeight() - 40;
yDelta *= -1;
} else if (y < 0) {
y = 0;
yDelta *= -1;
}
clickBounds.setLocation(x, y);
}
protected void paint() {
// Render single frame
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D graphics = (Graphics2D) strategy.getDrawGraphics();
// Render to graphics
// ...
graphics.setColor(Color.BLUE);
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.setColor(Color.RED);
graphics.fill(clickBounds);
// Dispose the graphics
graphics.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (strategy.contentsRestored());
// Display the buffer
strategy.show();
// Repeat the rendering if the drawing buffer was lost
} while (strategy.contentsLost());
}
}
}
}
You should also remember, Swing's been using either DirectX or OpenGL pipelines since about 1.4 (or maybe 1.5). The main reasons for using BufferStrategy are more direct access to the hardware (which Swing is pretty close to anyway) AND direct control over the painting process (which is now really the only reason to use it)
Continuing from a previous question, I keep searching for the optimal way to combine active rendering with textfields in Java. I tried several options, using BufferStrategy, VolatileImage or overriding update() and paint() in standard AWT, but I ended up using Swing.
I'm posting the current state of affairs here just in case someone happens to have new insights based on my code example, and perhaps others who are working on a similar app might benefit from my findings.
The target is to accomplish these three feats:
render an animating object on top of a background buffer that is updated only when necessary
use textfields on top of the rendered result
resize the window without problems
Below is the code of the demo application developed with the great help of stackoverflower trashgod.
Two notes:
1) Refreshing strictly the area that is invalidated by the previous step in the animation appears to be so much prone to visual errors that I gave up on it. This means I now redraw the entire background buffer every frame.
2) The efficiency of drawing a BufferedImage to screen is hugely dependent on the platform. The Mac implementation doesn't seem to support hardware acceleration properly, which makes repainting the background image to the output window a tedious task, depending of course on the size of the window.
I found the following results on my 2.93 GHz dualcore iMac:
Mac OS 10.5:
640 x 480: 0.9 ms, 8 - 9%
1920 x 1100: 5 ms, 35 - 40%
Windows XP:
640 x 480: 0.05 ms, 0%
1920 x 1100: 0.05 ms, 0%
Legend:
screen size: average time to draw a frame, CPU usage of the application.
As far as I can see, the code below is the most efficient way of accomplishing my goals. Any new insights, optimizations or test results are very welcome!
Regards,
Mattijs
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;
public class SwingTest extends JPanel implements
ActionListener,
Runnable
{
private static final long serialVersionUID = 1L;
private BufferedImage backgroundBuffer;
private boolean repaintbackground = true;
private static final int initWidth = 640;
private static final int initHeight = 480;
private static final int radius = 25;
private final Timer t = new Timer(20, this);
private final Rectangle rect = new Rectangle();
private long totalTime = 0;
private int frames = 0;
private long avgTime = 0;
public static void main(String[] args) {
EventQueue.invokeLater(new SwingTest());
}
public SwingTest() {
super(true);
this.setPreferredSize(new Dimension(initWidth, initHeight));
this.setLayout(null);
this.setOpaque(false);
this.addMouseListener(new MouseHandler());
}
#Override
public void run() {
JFrame f = new JFrame("SwingTest");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.addComponentListener(new ResizeHandler());
/* This extra Panel with GridLayout is necessary to make sure
our content panel is properly resized with the window.*/
JPanel p = new JPanel(new GridLayout());
p.add(this);
f.add(p);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
createBuffer();
t.start();
}
#Override
public void actionPerformed(ActionEvent e) {
this.repaint();
}
#Override
protected void paintComponent(Graphics g) {
long start = System.nanoTime();
super.paintComponent(g);
if (backgroundBuffer == null) createBuffer();
if (repaintbackground) {
/* Repainting the background may require complex rendering operations,
so we don't want to do this every frame.*/
repaintBackground(backgroundBuffer);
repaintbackground = false;
}
/* Repainting the pre-rendered background buffer every frame
seems unavoidable. Previous attempts to keep track of the
invalidated area and repaint only that part of the background buffer
image have failed. */
g.drawImage(backgroundBuffer, 0, 0, null);
repaintBall(g, backgroundBuffer, this.getWidth(), this.getHeight());
repaintDrawTime(g, System.nanoTime() - start);
}
void repaintBackground(BufferedImage buffer) {
Graphics2D g = buffer.createGraphics();
int width = buffer.getWidth();
int height = buffer.getHeight();
g.clearRect(0, 0, width, height);
for (int i = 0; i < 100; i++) {
g.setColor(new Color(0, 128, 0, 100));
g.drawLine(width, height, (int)(Math.random() * (width - 1)), (int)(Math.random() * (height - 1)));
}
}
void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);
g.setColor(Color.BLUE);
g.fillOval(rect.x, rect.y, rect.width, rect.height);
}
void repaintDrawTime(Graphics g, long frameTime) {
if (frames == 32) {avgTime = totalTime/32; totalTime = 0; frames = 0;}
else {totalTime += frameTime; ++frames; }
g.setColor(Color.white);
String s = String.valueOf(avgTime / 1000000d + " ms");
g.drawString(s, 5, 16);
}
void createBuffer() {
int width = this.getWidth();
int height = this.getHeight();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gs = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gs.getDefaultConfiguration();
backgroundBuffer = gc.createCompatibleImage(width, height, Transparency.OPAQUE);
repaintbackground = true;
}
private class MouseHandler extends MouseAdapter {
#Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
JTextField field = new JTextField("test");
Dimension d = field.getPreferredSize();
field.setBounds(e.getX(), e.getY(), d.width, d.height);
add(field);
}
}
private class ResizeHandler extends ComponentAdapter {
#Override
public void componentResized(ComponentEvent e) {
super.componentResized(e);
System.out.println("Resized to " + getWidth() + " x " + getHeight());
createBuffer();
}
}
}
I have a few observations:
Your improved repaintDrawTime() is very readable, but it is a micro-benchmark and subject to the vagaries of the host OS. I can't help wondering if the XP results are an artifact of that system's limited clock resolution. I see very different results on Windows 7 and Ubuntu 10.
If you don't use a null layout, you won't need the extra panel; the default layout for JPanel is FlowLayout, and f.add(this) simply adds it to the center of the frame's default BorderLayout.
Repeated constructor invocations can be time consuming.
Consider replacing
g.setColor(new Color(0, 128, 0, 100));
with
private static final Color color = new Color(0, 128, 0, 100);
...
g.setColor(color);
Alternatively, a simple color lookup table, may be useful, e.g.
private final Queue<Color> clut = new LinkedList<Color>();
I don't see a point for passing in the BufferedImage to this method:
void repaintBall(Graphics g, BufferedImage backBuffer, int width, int height) {
double time = 2* Math.PI * (System.currentTimeMillis() % 3300) / 3300.;
rect.setRect((int)(Math.sin(time) * width/3 + width/2 - radius), (int)(Math.cos(time) * height/3 + height/2) - radius, radius * 2, radius * 2);
g.setColor(Color.BLUE);
g.fillOval(rect.x, rect.y, rect.width, rect.height);
}
It doesn't seem like you use it ever in the method body.
I was able to transplant the graphics portion of this class into my own JPanel constructor class, and it improved the graphics of my game a lot, but I never needed to use a method like this where I pass in a BufferedImage as an argument but never use it.