Java Forum / GUI / March 2007
Requesting tips, comments for an EDT thread-safe game architecture
Oliver Wong - 21 Mar 2007 17:40 GMT I recently saw a thread about the Swing EDT in the CLJP, and it made me wonder whether my general game architecture was thread safe or not. EDT, and threading in general, are one of my weaker points in Java. Is this general design okay? Are there things which I've put into the EDT which I shouldn't have? Are there things which are outside of the EDT that should be in it?
<SSCCE> import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.AffineTransform; import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities;
public class Test {
private static final int DEFAULT_RENDERING_WIDTH = 640; private static final int DEFAULT_RENDERING_HEIGHT = 480; private static JFrame mainWindow; /* * the mainPanel is where most of the drawing happens. I draw onto a panel * instead of directly onto the JFrame to avoid messing around with * getInsets(). */ private static JPanel mainPanel; /* * Returns true if the game is in the process of shutting down and quitting. * If it's in the middle of a game-loop iteration, it'll finish that * iteration (potentially drawing 1 more frame of animation, and then quit. */ private static boolean timeToQuit = false; /* * True if the player pressed the "action" key since the last iteration * through the game loop, false otherwise. In a real game, I'd probably have * many of these booleans (e.g. an "up" key, a "down" key, etc., and move it * into a seperate class for organization purposes. */ private static boolean pressedActionKey = false;
public static void main(String[] args) throws InterruptedException, InvocationTargetException { /* * This first invokeAndWait initializes all the GUI components, and * registers the appropriate listeners to hook into the main game * engine. */ EventQueue.invokeAndWait(new Runnable() { @Override public void run() { mainWindow = new JFrame("My game"); mainWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); mainWindow.addWindowListener(new WindowListener() { @Override public void windowActivated(WindowEvent e) { // Does nothing. }
@Override public void windowClosed(WindowEvent e) { // Does nothing }
@Override public void windowClosing(WindowEvent e) { timeToQuit = true; EventQueue.invokeLater(new Runnable() { @Override public void run() { synchronized (mainWindow) { mainWindow.setVisible(false); mainWindow.dispose(); } } }); }
@Override public void windowDeactivated(WindowEvent e) { // Does nothing. }
@Override public void windowDeiconified(WindowEvent e) { // Does nothing. }
@Override public void windowIconified(WindowEvent e) { // Does nothing. }
@Override public void windowOpened(WindowEvent e) { // Does nothing. } }); mainWindow.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { pressedActionKey = true; } }
@Override public void keyReleased(KeyEvent e) { // does nothing. }
@Override public void keyTyped(KeyEvent e) { // does nothing. }
}); mainPanel = new JPanel(); mainPanel.setPreferredSize(new Dimension( DEFAULT_RENDERING_WIDTH, DEFAULT_RENDERING_HEIGHT)); mainWindow.add(mainPanel); mainWindow.pack(); mainWindow.setVisible(true);
} }); /* * This while loop is the main game loop. It basically iterates through * 3 stages forever: getting the player input, reacting to it, and * drawing the results on screen. */ while (!timeToQuit) { getPlayerInput(); processGameLogic(); EventQueue.invokeAndWait(new Runnable() { @Override public void run() { synchronized (mainWindow) { Graphics2D g = (Graphics2D) mainPanel.getGraphics(); g.setTransform(AffineTransform.getScaleInstance( (double) mainPanel.getWidth() / (double) DEFAULT_RENDERING_WIDTH, (double) mainPanel.getHeight() / (double) DEFAULT_RENDERING_HEIGHT)); updateScreen(g); } } }); Thread.sleep(1); } }
public static void processGameLogic() { /* * Check if pacman touched a ghost, stuff like that. Doesn't touch any * Swing components. */ }
public static void getPlayerInput() { /* * Doesn't touch any Swing components. */ if (pressedActionKey) { /* make mario jump */ System.out.println("Mario jumps."); pressedActionKey = false; } }
public static void updateScreen(Graphics2D g) { assert SwingUtilities.isEventDispatchThread() : "Don't call updateScreen from outside the EDT."; g.setColor(Color.BLACK); g.fillRect(0, 0, 640, 480); /* * TODO: Draw all sorts of amazing eye-candy. */ } } </SSCCE>
- Oliver
Knute Johnson - 22 Mar 2007 02:14 GMT > I recently saw a thread about the Swing EDT in the CLJP, and it made > me wonder whether my general game architecture was thread safe or not. [quoted text clipped - 16 lines] > }); > } Oliver:
Window events are already processed on the EDT you don't need to specifically call them with EventQueue.invokeLater().
I'm not sure why you have the calls to set then main window visible and dispose it synchronized. They are already being called on the EDT and if you don't make any Swing method calls except on the EDT they will already be synchronized by default.
Having played a lot with one form of animation, I would use a completely different tack. Use a Window or JWindow and do active rendering. This avoids the EDT altogether for any drawing. You still may have to synchronize some parts of your code but the more you can avoid that the better. Synchronizing can have a rather significant performance hit.
 Signature Knute Johnson email s/nospam/knute/
Daniel Pitts - 22 Mar 2007 03:29 GMT On Mar 21, 6:14 pm, Knute Johnson <nos...@rabbitbrush.frazmtn.com> wrote:
> > I recently saw a thread about the Swing EDT in the CLJP, and it made > > me wonder whether my general game architecture was thread safe or not. [quoted text clipped - 12 lines] > Knute Johnson > email s/nospam/knute/ And incorrectly avoid Synchronization can have an extreme correctness "hit".
Generally, synchronization isn't the "bad thing" that people have stigmatized it to be. Used incorrectly, it can lead to problems, yes... But those generally come from a lack of understanding. If you tell people "Avoid, Avoid, Avoid", they'll possible never learn when/ where they MUST use it.
I suggest Java Concurrency in Practice <http://jcip.net/> for anyone who wants to know the correct way to deal with multithreaded applications. I already understood SOME of it, but that book clarified a lot of concepts, and solidified my understanding of multi- threaded programming.
On Mar 21, 9:41 am, "Oliver Wong" <o...@castortech.com> wrote:
> <SSCCE> [snip]
> while (!timeToQuit) { > getPlayerInput(); [quoted text clipped - 16 lines] > } > } [snip]
> </SSCCE> > > - Oliver Oliver: I personally think its a "Bad Thing" to have a busy wait -- a while loop with a Thread.sleep(). Swing is design around an Event model, you can probably refactor your code to avoid running on the main thread, and use a javax.swing.Timer instead.
That way you don't need to have a "getPlayerInput()", "processGameLogic()", and the strange call to invokeAndWait(). It can all be event driven.
BTW, you should avoid "synchronize" in the EDT all together. If you have a thread that synchronizes on an object, and then calls "invokeAndWait" with a runnable that syncs on the same object, you have a deadlock. And this is only the first order example.
Hope this helps, Daniel.
P.S. Again, I suggest Java Concurrency in Practice <http://jcip.net/>
Oliver Wong - 22 Mar 2007 15:37 GMT > On Mar 21, 6:14 pm, Knute Johnson <nos...@rabbitbrush.frazmtn.com> > wrote: [quoted text clipped - 4 lines] >> synchronize some parts of your code but the more you can avoid that the >> better. Synchronizing can have a rather significant performance hit. I thought I *was* using active rendering. I thought active rendering was simply having your own rendering loop instead of waiting for AWT to sent paint events (which is how my code is structured). As a quick check, I skimmed through http://java.sun.com/docs/books/tutorial/extra/fullscreen/rendering.html and now I'm a bit confused. The page mentions setIgnoreRepaint, which I've forgotten to do. Okay, fine. But the page also implies (but does not explicitly state) that active rendering is only possible in full screen mode.
So:
(1) What's the difference between what I'm doing, and active rendering? Are you thinking mainly of BufferStrategies? (I'll address that later on in this post) (2) I assume active rendering possible in windowed-mode. Is this assumption correct? (3) And finally, why do you recommend JWindow over JFrame (or do you)?
[...]
> I suggest Java Concurrency in Practice <http://jcip.net/> for anyone > who wants to know the correct way to deal with multithreaded > applications. I already understood SOME of it, but that book > clarified a lot of concepts, and solidified my understanding of multi- > threaded programming. Thanks, I'll take a look at that book.
> On Mar 21, 9:41 am, "Oliver Wong" <o...@castortech.com> wrote: >> <SSCCE> [quoted text clipped - 30 lines] > you can probably refactor your code to avoid running on the main > thread, and use a javax.swing.Timer instead. Perhaps for a "normal" application, but I'm pretty sure the common wisdom for games is to have your own rendering loop. Sun themselves recommends a rendering loop in their active rendering tutorial: http://java.sun.com/docs/books/tutorial/extra/fullscreen/rendering.html
<quote> public void myRenderingLoop() { while (!done) { Graphics myGraphics = getPaintGraphics(); // Draw as appropriate using myGraphics myGraphics.dispose(); } } </quote>
> I'm not sure why you have the calls to set then main window visible and > dispose it synchronized. They are already being called on the EDT and > if you don't make any Swing method calls except on the EDT they will > already be synchronized by default. [...]
> BTW, you should avoid "synchronize" in the EDT all together. If you > have a thread that synchronizes on an object, and then calls > "invokeAndWait" with a runnable that syncs on the same object, you > have a deadlock. And this is only the first order example. I was getting some NPEs from mainPanel.getGraphics() returning null, and I had figured it was due to the window getting disposed while the rendering loop was still occurring or something along those lines. But after having read a bit more about the event model (e.g. that it is implemented as a queue and is single-threaded) I see now that my synchronizations didn't really make sense.
Anyway, here's my new code, taken into account your advices. The two main changes are:
(1) Used BufferStrategy to do hardware accelerated page flipping/double buffering. I normally do this in my games, but I thought it wasn't really relevant to the threading questions, so I left it out for this example. Now that I've put it in, I can't really use the JPanel (since you can't get a strategy on the JPanel), so this also means there's a bit of extra code to fiddle with insets on the Window.
(2) Replaced the synchronize() keywords with an if statement checking that mainWindow is not null.
<SSCCE> import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.AffineTransform; import java.awt.image.BufferStrategy; import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame; import javax.swing.SwingUtilities;
public class Test {
private static final int DEFAULT_RENDERING_WIDTH = 640; private static final int DEFAULT_RENDERING_HEIGHT = 480; private static JFrame mainWindow; /** * Returns true if the game is in the process of shutting down and quitting. * If it's in the middle of a game-loop iteration, it'll finish that * iteration (potentially drawing 1 more frame of animation, and then quit. */ private static boolean timeToQuit = false; /** * True if the player pressed the "action" key since the last iteration * through the game loop, false otherwise. In a real game, I'd probably have * many of these booleans (e.g. an "up" key, a "down" key, etc., and move it * into a seperate class for organization purposes. */ private static boolean pressedActionKey = false;
public static void main(String[] args) throws InterruptedException, InvocationTargetException { /* * This first invokeAndWait initializes all the GUI components, and * registers the appropriate listeners to hook into the main game * engine. */ EventQueue.invokeAndWait(new Runnable() { @Override public void run() { mainWindow = new JFrame("My game"); mainWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); mainWindow.setIgnoreRepaint(true); mainWindow.addWindowListener(new WindowListener() { @Override public void windowActivated(WindowEvent e) { // Does nothing. }
@Override public void windowClosed(WindowEvent e) { // Does nothing }
@Override public void windowClosing(WindowEvent e) { timeToQuit = true; EventQueue.invokeLater(new Runnable() { @Override public void run() { mainWindow.dispose(); mainWindow = null; } }); }
@Override public void windowDeactivated(WindowEvent e) { // Does nothing. }
@Override public void windowDeiconified(WindowEvent e) { // Does nothing. }
@Override public void windowIconified(WindowEvent e) { // Does nothing. }
@Override public void windowOpened(WindowEvent e) { // Does nothing. } }); mainWindow.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { pressedActionKey = true; } }
@Override public void keyReleased(KeyEvent e) { // does nothing. }
@Override public void keyTyped(KeyEvent e) { // does nothing. }
}); mainWindow.setPreferredSize(new Dimension( DEFAULT_RENDERING_WIDTH, DEFAULT_RENDERING_HEIGHT)); mainWindow.pack(); mainWindow.setVisible(true); mainWindow.createBufferStrategy(2); } }); /* * This while loop is the main game loop. It basically iterates through * 3 stages forever: getting the player input, reacting to it, and * drawing the results on screen. */ while (!timeToQuit) { getPlayerInput(); processGameLogic(); EventQueue.invokeAndWait(new Runnable() { @Override public void run() { if (mainWindow != null) { BufferStrategy strategy = mainWindow .getBufferStrategy(); Graphics2D g = (Graphics2D) strategy.getDrawGraphics(); Insets insets = mainWindow.getInsets(); g.translate(insets.left, insets.top); g.setTransform(AffineTransform .getScaleInstance( (double) (mainWindow.getWidth() - insets.right) / (double) DEFAULT_RENDERING_WIDTH, (double) (mainWindow .getHeight() - insets.bottom) / (double) DEFAULT_RENDERING_HEIGHT)); updateScreen(g); strategy.show(); } } }); Thread.sleep(1); } }
public static void processGameLogic() { /* * Check if pacman touched a ghost, stuff like that. Doesn't touch any * Swing components. */ }
public static void getPlayerInput() { /* * Doesn't touch any Swing components. */ if (pressedActionKey) { /* make mario jump */ System.out.println("Mario jumps."); pressedActionKey = false; } }
public static void updateScreen(Graphics2D g) { assert SwingUtilities.isEventDispatchThread() : "Don't call updateScreen from outside the EDT."; g.setColor(Color.BLACK); g.fillRect(0, 0, 640, 480); /* * TODO: Draw all sorts of amazing eye-candy. */ } } </SSCCE>
- Oliver
Oliver Wong - 22 Mar 2007 17:03 GMT [most of the code snipped]
> /* > * This while loop is the main game loop. It basically iterates through [quoted text clipped - 21 lines] > / (double) DEFAULT_RENDERING_HEIGHT)); > updateScreen(g); Oops, I should probably call g.dispose() here.
> strategy.show(); > } > } > }); > Thread.sleep(1); > } - Oliver
Knute Johnson - 22 Mar 2007 19:40 GMT > [most of the code snipped] >> /* [quoted text clipped - 33 lines] > > - Oliver Oliver:
For active rendering I would take your rendering out of the EDT altogether. The problem that you are going to run into is that eventually too much will be happening on the EDT and you won't be able to do any user input processing or that it will delay your rendering. You only need to create the BufferStrategy once not every time in the rendering loop. I like to use java.util.Timer to drive the loop but you can use Thread.sleep(). As Daniel said I would do my user input event driven. You can use a Frame or JFrame but you have to deal with the insets and title bar your self. If you use an undecorated frame you might as well use a window.
Below is how I would do it. See if you like any of it.
import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.util.*; import javax.swing.*;
public class test4 extends JWindow { java.util.Timer timer; BufferStrategy bs; int x,y,deltaX=3,deltaY=3;
public test4() { // you want to do all drawing setIgnoreRepaint(true);
addWindowListener(new WindowAdapter() { public void windowOpened(WindowEvent we) { // don't create the buffer strategy // until window is visible test4.this.createBufferStrategy(2); bs = getBufferStrategy(); // schedule animation timer.schedule(new AnimationTask(),0,30); } });
timer = new java.util.Timer();
setSize(400,300); setVisible(true); }
class AnimationTask extends TimerTask { public void run() { x += deltaX; y += deltaY; if (x > getWidth() - 30 || x < 0) deltaX = -deltaX; if (y > getHeight() - 30 || y < 0) deltaY = -deltaY; render(); } }
void render() { do { do { // get new draw graphics every time Graphics g = bs.getDrawGraphics(); // draw g.setColor(Color.WHITE); g.fillRect(0,0,getWidth(),getHeight()); g.setColor(Color.BLUE); g.fillOval(x,y,30,30); // dispose of the draw graphics g.dispose(); } while (bs.contentsRestored()) ; // blit/flip buffers bs.show(); } while (bs.contentsLost()) ; }
public static void main(String[] args) { Runnable r = new Runnable() { public void run() { test4 t4 = new test4(); t4.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent me) { System.exit(0); } }); } }; EventQueue.invokeLater(r); } }
 Signature Knute Johnson email s/nospam/knute/
Oliver Wong - 22 Mar 2007 23:01 GMT > For active rendering I would take your rendering out of the EDT > altogether. The problem that you are going to run into is that > eventually too much will be happening on the EDT and you won't be able > to do any user input processing or that it will delay your rendering. This key piece of information is pretty central to my (mis?)understanding. I thought it's written that whenever you touch Swing components, you should do so from within the EDT. If that's the case, shouldn't you perform painting of JFrames and other widgets within the EDT?
> You only need to create the BufferStrategy once not every time in the > rendering loop. Right: in the code I posted, I actually create the BufferStrategy once, but call getBufferStrategy() repeatedly from within the loop (as opposed to, in your code, calling getBufferStrategy once and storing its return value somewhere). From peering into the source code, it looks like getBufferStrategy() is just a plain getter anyway.
> I like to use java.util.Timer to drive the loop but you can use > Thread.sleep(). The Timer class seems to have its own internal "task queue". From the javadocs: http://java.sun.com/javase/6/docs/api/java/util/Timer.html <quote> Timer tasks should complete quickly. If a timer task takes excessive time to complete, it "hogs" the timer's task execution thread. This can, in turn, delay the execution of subsequent tasks, which may "bunch up" and execute in rapid succession when (and if) the offending task finally completes. </quote>
I haven't spent too much time analyzing the impact of this (presumably there will only be one if I use Timer.schedule() at multiple locations in my code), but it sounds like I can avoid worrying about it entirely with a tight loop and Thread.sleep(1).
I realize that the javadocs themselves claim that your design is appropriate for animations (It says "Fixed-delay execution is appropriate for recurring activities that require 'smoothness.' In other words, it is appropriate for activities where it is more important to keep the frequency accurate in the short run than in the long run. This includes most animation tasks"), but this fixed-delay behaviour sounds like it's incompatible with the way *I* want rendering to work in my engine.
It's not shown in my code, but in a "real" engine, I usually calculate the amount of time that has passed between each frame of animation (I've got this library which tries to use native C code on platforms it recognizes, and falls back on System.currentTimeMillis() on those it doesn't), and then I use that elapsed time to calculate whether to skip ahead a couple of frames (on computers that are a bit slow for this game) or to interpolate between two frames of animations (on computers that are a bit fast for this game).
For example, I might have a scripting language which says "It should take the character 2 seconds to walk from point A to point B", and the engine will smoothly animate a sprite moving from point A to point B at the highest frame rate that the computer can handle.
Long story short, the "Fixed-delay execution" system seems like it'll work counter to my goal there.
> As Daniel said I would do my user input event driven. Really? I find a polling-based input system much more natural to program for, in the context of making a game. For example, if the player presses the "Right" cursor key, I want Sonic the Hedgehog to start running towards the right. And if in the next frame, he's still holding down the "right" cursor I want Sonic to keep running (otherwise, he should start to brake, and slow down). And the longer the button is held down, the more Sonic accelerates (up to a certain terminal velocity).
With a polling system, the logic would look something like:
{ if (rightButtonIsDown()) { if (sonic.xVel < MAX_XVEL) { sonic.xVel += 1.0; } } else if (leftButtonIsDown()) { if (sonic.xVel > -MAX_XVEL) { sonic.xVel -= 1.0; } } else { sonic.xVel /= 2; /*Quickly approaches zero.*/ } }
The only way I could think of implementing this with Swing's event model would be to set a flag when a keypress occurs, and then unset a flag when the key is released... which is pretty much emulating a polling system ontop of Swing's event system... which is pretty much what the code I posted does.
> You can use a Frame or JFrame but you have to deal with the insets and > title bar your self. If you use an undecorated frame you might as well > use a window. Right, the insets are a bit of a pain, and I sometimes wish JFrame had a setSizeWithoutInsets() method in addition to its standard setSize() method. But I *do* want a decorate frame (at least when the game is not running in full screen mode), so that the player can move the window around and resize it. Of course, if the player switches the game to fullscreen mode, then at that point, the window should be undecorated.
> Below is how I would do it. See if you like any of it. [informative code snipped]
Thanks. You introduced me to the WindowAdapter class, and it looks like I'll have to re-read the javadocs for BufferStrategy (since it's not clear to me what you're trying to do with your calls to contentsLost() and contentsRestored()) It looks like it has something to do with losing image data due to the use of VolatileImage, but I'm wondering whether it's necessary for my game: In most of my games, so much is going on screen at any one point that I typically assume that the whole image is always lost and redraw the whole frame from scratch. The game is implemented as a state machine (not illustrated in the simplified version of the code I've been posting), and any state which needs access to previous frame contents (e.g. for a motion-blurring effect) needs to implement storing previous image data itself.
Here's yet another iteration of my design, taking into account your advice of pulling the active-drawing code out of the EDT. I've had to reintroduce the synchronize() statements, because otherwise it's possible that the user might close the window (and thus invalidate the graphics object) while I'm still using the graphics object to render the next frame. I've added a Thread.sleep(1000) statement to increase the likelihood of this race condition occurring for illustrative purposes.
I've also used your WindowAdapter (and KeyAdapter) trick.
<SSCCE> import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferStrategy; import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame;
public class Test {
private static final int DEFAULT_RENDERING_WIDTH = 640; private static final int DEFAULT_RENDERING_HEIGHT = 480; private static JFrame mainWindow; /** * Returns true if the game is in the process of shutting down and quitting. * If it's in the middle of a game-loop iteration, it'll finish that * iteration (potentially drawing 1 more frame of animation, and then quit. */ private static boolean timeToQuit = false; /** * True if the player pressed the "action" key since the last iteration * through the game loop, false otherwise. In a real game, I'd probably have * many of these booleans (e.g. an "up" key, a "down" key, etc., and move it * into a seperate class for organization purposes. */ private static boolean pressedActionKey = false;
public static void main(String[] args) throws InterruptedException, InvocationTargetException { /* * This first invokeAndWait initializes all the GUI components, and * registers the appropriate listeners to hook into the main game * engine. */ EventQueue.invokeAndWait(new Runnable() { @Override public void run() { mainWindow = new JFrame("My game"); mainWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); mainWindow.setIgnoreRepaint(true); mainWindow.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { timeToQuit = true; EventQueue.invokeLater(new Runnable() { @Override public void run() { synchronized (mainWindow) { mainWindow.dispose(); mainWindow = null; } } }); } }); mainWindow.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { pressedActionKey = true; } } }); mainWindow.setPreferredSize(new Dimension( DEFAULT_RENDERING_WIDTH, DEFAULT_RENDERING_HEIGHT)); mainWindow.pack(); mainWindow.setVisible(true); mainWindow.createBufferStrategy(2); } }); /* * This while loop is the main game loop. It basically iterates through * 3 stages forever: getting the player input, reacting to it, and * drawing the results on screen. */ while (!timeToQuit) { getPlayerInput(); processGameLogic(); synchronized (mainWindow) { if (mainWindow != null) { Thread.sleep(1000); BufferStrategy strategy = mainWindow.getBufferStrategy(); Graphics2D g = (Graphics2D) strategy.getDrawGraphics(); Insets insets = mainWindow.getInsets(); g.translate(insets.left, insets.top); g.setTransform(AffineTransform.getScaleInstance( (double) (mainWindow.getWidth() - insets.right) / (double) DEFAULT_RENDERING_WIDTH, (double) (mainWindow.getHeight() - insets.bottom) / (double) DEFAULT_RENDERING_HEIGHT)); updateScreen(g); g.dispose(); strategy.show(); } } Thread.sleep(1); } }
public static void processGameLogic() { /* * Check if pacman touched a ghost, stuff like that. Doesn't touch any * Swing components. */ }
public static void getPlayerInput() { /* * Doesn't touch any Swing components. */ if (pressedActionKey) { /* make mario jump */ System.out.println("Mario jumps."); pressedActionKey = false; } }
public static void updateScreen(Graphics2D g) { g.setColor(Color.BLACK); g.fillRect(0, 0, 640, 480); /* * TODO: Draw all sorts of amazing eye-candy. */ } } </SSCCE>
- Oliver
Knute Johnson - 23 Mar 2007 02:54 GMT >> For active rendering I would take your rendering out of the EDT >> altogether. The problem that you are going to run into is that [quoted text clipped - 6 lines] > shouldn't you perform painting of JFrames and other widgets within the > EDT? Unless you are trying to do high performance active rendering. Then you do your rendering in another thread. The normal Swing component drawing is disabled and you won't interfere with it at all.
>> You only need to create the BufferStrategy once not every time in the >> rendering loop. [quoted text clipped - 4 lines] > return value somewhere). From peering into the source code, it looks like > getBufferStrategy() is just a plain getter anyway. Yes you are correct. In your case it is just one less method call in the render loop to do it the way I did.
>> I like to use java.util.Timer to drive the loop but you can use >> Thread.sleep(). [quoted text clipped - 9 lines] > completes. > </quote> Each Timer object creates a thread. If you only schedule one task it won't run into problems with queue delays. I did test this with something else I was working on and it is better to create a new Timer for each task.
> I haven't spent too much time analyzing the impact of this (presumably > there will only be one if I use Timer.schedule() at multiple locations in > my code), but it sounds like I can avoid worrying about it entirely with a > tight loop and Thread.sleep(1).
> I realize that the javadocs themselves claim that your design is > appropriate for animations (It says "Fixed-delay execution is appropriate [quoted text clipped - 3 lines] > most animation tasks"), but this fixed-delay behaviour sounds like it's > incompatible with the way *I* want rendering to work in my engine. I know but I can tell you after a lot of experimentation with adjusting timers and fixed rate Timers that it looks better with fixed delay.
> It's not shown in my code, but in a "real" engine, I usually calculate > the amount of time that has passed between each frame of animation (I've [quoted text clipped - 4 lines] > or to interpolate between two frames of animations (on computers that are > a bit fast for this game). I don't know how real game developers handle timing. I do know that with a fair amount of testing I have gone back to Timer.schedule(). It does not produce that accurate a result on Windows XP computers that I have tested it on. It is just better than the alternatives I have tried.
I don't have any real experience with XP timing but I would be very curious to know if it is possible to make a JNI method that could sleep very accurately. I would like to find something that could be accurate to 250 microseconds +- to try in my timing loop. The accuracy of Timer.schedule() is no where near that close. If you ran the test code I posted you can see the jitter caused by the timing errors.
> For example, I might have a scripting language which says "It should > take the character 2 seconds to walk from point A to point B", and the > engine will smoothly animate a sprite moving from point A to point B at > the highest frame rate that the computer can handle. I think what you will find with that method is that your frame rate is constantly changing as thread timing and system load changes. But I will be interested in your results.
> Long story short, the "Fixed-delay execution" system seems like it'll > work counter to my goal there. I'm not trying to talk you out of it just trying to give you what little I know and have tried.
>> As Daniel said I would do my user input event driven. > [quoted text clipped - 27 lines] > system ontop of Swing's event system... which is pretty much what the code > I posted does. I used the old event model for the Pong game I wrote. That works well and for that sort of input will probably be better. I was thinking JTextFields and Buttons before. Pong game and source is at www.knutejohnson.com. It does not use active rendering.
>> You can use a Frame or JFrame but you have to deal with the insets and >> title bar your self. If you use an undecorated frame you might as well [quoted text clipped - 6 lines] > around and resize it. Of course, if the player switches the game to > fullscreen mode, then at that point, the window should be undecorated. I haven't used full screen exclusive mode much because most of what I have been working on uses multiple monitors and still requires user input. That won't work with FSEM. I don't know if it is possible to remove the decoration on a window once it has been created or not.
>> Below is how I would do it. See if you like any of it. > [informative code snipped] [quoted text clipped - 11 lines] > (e.g. for a motion-blurring effect) needs to implement storing previous > image data itself. Even a blit buffer strategy can use a VolatileImage as the back buffer. If you read the docs for VolatileImage you will see what I'm up to there. That code is directly out of the docs.
> Here's yet another iteration of my design, taking into account your > advice of pulling the active-drawing code out of the EDT. I've had to [quoted text clipped - 62 lines] > public void windowClosing(WindowEvent e) { > timeToQuit = true; You are already in the EDT here, invokeLater is not needed.
> EventQueue.invokeLater(new Runnable() { > @Override [quoted text clipped - 80 lines] > > - Oliver Try it and see how it works. Post something that has some action!
If you are interested in writing some C timing code for me, let me know.
 Signature Knute Johnson email s/nospam/knute/
Oliver Wong - 23 Mar 2007 15:43 GMT >>> For active rendering I would take your rendering out of the EDT >>> altogether. The problem that you are going to run into is that [quoted text clipped - 10 lines] > do your rendering in another thread. The normal Swing component drawing > is disabled and you won't interfere with it at all. Okay, thanks.
[...]
> I don't have any real experience with XP timing but I would be very > curious to know if it is possible to make a JNI method that could sleep > very accurately. I would like to find something that could be accurate > to 250 microseconds +- to try in my timing loop. The accuracy of > Timer.schedule() is no where near that close. If you ran the test code > I posted you can see the jitter caused by the timing errors. The library I use returns number of millisecond since the epoch, so you can't get sub-millisecond precision with it. Probably there are libraries out there that *do* provide sub-millisecond precision (and hopefully a corresponding accuracy), but I don't know of any.
>> For example, I might have a scripting language which says "It >> should take the character 2 seconds to walk from point A to point B", [quoted text clipped - 4 lines] > constantly changing as thread timing and system load changes. But I > will be interested in your results. Yes, that's the case. The experience is "optimized" for the high end machines. With a fixed-delay animation, you have some frame rate cap (e.g. 30fps), which all high end machines will be able to consistently produce, and there's a certain cut-off point after which slower machines will start to play the animation "too slowly", but will render every single frame. So for example, things like syncing the animation to music or sound track may become problematic.
With my variable delay system, you have a frame rate bottom (e.g. 24fps). If a machine is unable to consistently meet that 24fps rate, then every frame of the "minimal" 10 fps animation will play, but at a slower than intended speed (and thus have syncing issues once again). However, any machine which can easily produce the 10fps animation with cycles to spare will instead display an animation with the highest fps it can muster, correctly timed (e.g. perhaps at 60fps).
[...]
> I haven't used full screen exclusive mode much because most of what I > have been working on uses multiple monitors and still requires user > input. That won't work with FSEM. I don't know if it is possible to > remove the decoration on a window once it has been created or not. I just tested it, and you cannot remove the decoration once you've added it. I haven't used FSEM much either, because it's easier to debug your game if you can still access the Eclipse workbench. Looks like I'll have to rethink that part of the engine.
[...]
>> mainWindow.addWindowListener(new WindowAdapter() { >> @Override [quoted text clipped - 14 lines] >> } >> }); Thanks, I forgot about that.
[...]
> Try it and see how it works. Post something that has some action! Okay, here's a version which animates a blue ball sort of like the code you posted:
<SSCCE> import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferStrategy; import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame;
public class Test {
private static final int DEFAULT_RENDERING_WIDTH = 640; private static final int DEFAULT_RENDERING_HEIGHT = 480; private static JFrame mainWindow; /** * Returns true if the game is in the process of shutting down and quitting. * If it's in the middle of a game-loop iteration, it'll finish that * iteration (potentially drawing 1 more frame of animation, and then quit. */ private static boolean timeToQuit = false; /** * True if the player pressed the "action" key since the last iteration * through the game loop, false otherwise. In a real game, I'd probably have * many of these booleans (e.g. an "up" key, a "down" key, etc., and move it * into a seperate class for organization purposes. */ private static boolean pressedActionKey = false; static double x; static double y; static double deltaX = 3; static double deltaY = 3;
public static void main(String[] args) throws InterruptedException, InvocationTargetException { /* * This first invokeAndWait initializes all the GUI components, and * registers the appropriate listeners to hook into the main game * engine. */ EventQueue.invokeAndWait(new Runnable() { @Override public void run() { mainWindow = new JFrame("My game"); mainWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); mainWindow.setIgnoreRepaint(true); mainWindow.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { timeToQuit = true; synchronized (mainWindow) { mainWindow.dispose(); mainWindow = null; } } }); mainWindow.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { pressedActionKey = true; } } }); mainWindow.setPreferredSize(new Dimension( DEFAULT_RENDERING_WIDTH, DEFAULT_RENDERING_HEIGHT)); mainWindow.pack(); mainWindow.setVisible(true); mainWindow.createBufferStrategy(2); } }); /* * This while loop is the main game loop. It basically iterates through * 3 stages forever: getting the player input, reacting to it, and * drawing the results on screen. */ long lastIteration = System.currentTimeMillis(); while (!timeToQuit) { getPlayerInput(); { long now = System.currentTimeMillis(); long elapsed = now - lastIteration; if (elapsed > 1000 / 24) { /* Don't do frame skipping when you drop below 24fps. */ elapsed = 1000 / 24; } processGameLogic(elapsed); lastIteration = now; } synchronized (mainWindow) { if (mainWindow != null) { BufferStrategy strategy = mainWindow.getBufferStrategy(); Graphics2D g = (Graphics2D) strategy.getDrawGraphics(); Insets insets = mainWindow.getInsets(); g.translate(insets.left - 1, insets.top - 1); g .translate((double) (mainWindow.getWidth() - insets.right - insets.left) / (double) DEFAULT_RENDERING_WIDTH, (double) (mainWindow.getHeight() - insets.bottom - insets.top) / (double) DEFAULT_RENDERING_HEIGHT); updateScreen(g); g.dispose(); strategy.show(); } } Thread.sleep(1); } }
public static void processGameLogic(long timeElapsed) { x += deltaX * timeElapsed / 10.0; y += deltaY * timeElapsed / 10.0; if (x > DEFAULT_RENDERING_WIDTH - 30) { deltaX = -Math.abs(deltaX); } if (x < 0) { deltaX = Math.abs(deltaX); } if (y > DEFAULT_RENDERING_HEIGHT - 30) { deltaY = -Math.abs(deltaY); } if (y < 0) { deltaY = Math.abs(deltaY); } }
public static void getPlayerInput() { /* * Doesn't touch any Swing components. */ if (pressedActionKey) { /* make mario jump */ System.out.println("Mario jumps."); pressedActionKey = false; } }
public static void updateScreen(Graphics2D g) { g.setColor(Color.BLACK); g.fillRect(0, 0, 640, 480); g.setColor(Color.BLUE); g.fillOval((int) x, (int) y, 30, 30); } } </SSCCE>
Unfortunately, to keep it as a simple SSCCE, I'm not able to include the JNI library I'm using, so the timer resolution is pretty bad, which means the animation isn't as smooth as it could be. I also realized I had made a bug in the way I implemented insets and Graphics2D translation, which is fixed in this latest iteration, though the blue ball mysteriously seems to over-estimate the position of the bottom border, which I can't really explain...
> If you are interested in writing some C timing code for me, let me know. I'm not. I'm not a C programmer. The library I use I downloaded off of the internet: http://www.javaworld.com/javaqa/2003-01/01-qa-0110-timing_p.html
- Oliver
Knute Johnson - 23 Mar 2007 21:32 GMT Oliver:
It looks pretty good. The one thing is the yo-yo effect you see as the ball moves. That is caused by the unstable timing. There are really two things that you see when you do animation, the yo-yo effect is slow about 2 or 3 times per second and the jitter which is the fast cycle jerkiness. The yo-yo effect can be eliminated by using the Timer. The jitter is more problematic. If you come up with any good solutions for the jitter, please post them.
 Signature Knute Johnson email s/nospam/knute/
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|