How can I make this simple game loop better?

I took your mainloop and made a testrun, its very simple left-to-right animation. I have tried everything(?) to make it smooth as silk in a windowed Java application still not eating CPU100%. I think its close to impossible or I just can’t.

I can cleary see occasional jumps and small tearing.


//http://www.java-gaming.org/index.php/topic,19971.0.html

import java.util.*;

import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferStrategy;

import java.awt.DisplayMode; // for full-screen mode

public class GameLoop1 implements KeyListener {
    Frame mainFrame;

    private static final long NANO_IN_MILLI = 1000000L;	
    long desiredFPS = 60;
    long desiredDeltaLoop = (1000*1000*1000)/desiredFPS;    

    long beginLoopTime;
    long endLoopTime;
    long currentPhysicTime;
    long lastPhysicTime;

    long fps;
    long frameCounter;
    long lastFpsTime;
    
    Rectangle2D rect;
    
    public GameLoop1(Map<String,String> args, GraphicsDevice device) {
        try {
            // Setup the frame
            GraphicsConfiguration gc = device.getDefaultConfiguration();
            
            mainFrame = new Frame(gc);
            mainFrame.setUndecorated(true);
            mainFrame.setIgnoreRepaint(true);
            mainFrame.setVisible(true);
            mainFrame.setSize(640, 480);
            //mainFrame.setLocationRelativeTo();
            mainFrame.setLocation(100,100);
            mainFrame.createBufferStrategy(2);
            mainFrame.addKeyListener(this);

            if ("true".equalsIgnoreCase(args.get("fullscreen"))) {
              device.setFullScreenWindow(mainFrame);
              device.setDisplayMode(new DisplayMode(640, 480, 8, DisplayMode.REFRESH_RATE_UNKNOWN));
            }
          
            // Cache the buffer strategy and create a rectangle to move
            BufferStrategy bufferStrategy = mainFrame.getBufferStrategy();
            rect = new Rectangle2D.Float(0,100,64,64);
            
            // loop initialization
            currentPhysicTime = System.nanoTime();
            lastFpsTime = currentPhysicTime;

            // Main loop
            while(true) {
                beginLoopTime = System.nanoTime();

                // Synchronise with the display hardware. Note that on
                // Windows Vista this method may cause your screen to flash.
                // If that bothers you, just comment it out.
                //Toolkit.getDefaultToolkit().sync();
				
                // **1) execute drawing
                Graphics g = bufferStrategy.getDrawGraphics();
                drawScreen(g);
                g.dispose();

                // Flip the buffer
                if( !bufferStrategy.contentsLost() )
                    bufferStrategy.show();
                
                // **2) execute physics
                lastPhysicTime = currentPhysicTime;
                currentPhysicTime = System.nanoTime();
                updateWorld(currentPhysicTime - lastPhysicTime);					
				
                calculateFramesPerSecond();
				
                // **3) execute logic
                // executeLogic(); // handle keyboard and other inputs
                endLoopTime = System.nanoTime();
                adjustSpeed();				
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            device.setFullScreenWindow(null);
        }
    }
	
    private void adjustSpeed() {
        long deltaLoop = endLoopTime - beginLoopTime;
        
        if(deltaLoop > desiredDeltaLoop) {
            // do nothing. We are alreadyLate
            System.out.println("Late, do not sleep");
        } else {
            try {
                Thread.sleep((desiredDeltaLoop - deltaLoop) / NANO_IN_MILLI);
            } catch (InterruptedException ex) { }
        }
    }	

    private void updateWorld(long elapsedTime) {
        // speed: 150 pixels per second
        double xMov = (140f/(NANO_IN_MILLI*1000)) * elapsedTime;
        //double xMov = 2.0;
        rect.setRect(rect.getX() + xMov, 100, 64, 64);
        
        if( rect.getX() > mainFrame.getWidth() )
            rect.setRect(-rect.getWidth(), 100, 64, 64);
    }
    
    private void drawScreen(Graphics g) {
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, mainFrame.getWidth(), mainFrame.getHeight());
        g.setColor(Color.WHITE);
        g.drawString("FPS: " + fps, 0, 17);
        
        g.setColor(Color.RED);
        g.fillRect((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight());
    }
    
    private void calculateFramesPerSecond() {
        if( currentPhysicTime - lastFpsTime >= NANO_IN_MILLI*1000 ) {
            fps = frameCounter;
            frameCounter = 0;
            lastFpsTime = currentPhysicTime;
        }
        frameCounter++;
    }
	
    public void keyPressed(KeyEvent e) {
        if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
            System.exit(0);
        }
    }
	
    public void keyReleased(KeyEvent e) { }
    public void keyTyped(KeyEvent e) { }

    public static void main(String[] args) {
        try {
	    Map<String,String> mapArgs = parseArguments(args);

            GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice device = env.getDefaultScreenDevice();
            new GameLoop1(mapArgs, device);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


	/**
	 * Parse commandline arguments, each parameter is a name-value pair.
	 * Example: java.exe MyApp "key1=value1" "key2=value2"
	 */
	private static Map<String,String> parseArguments(String[] args) {
		Map<String,String> mapArgs = new HashMap<String,String>();

		for(int idx=0; idx < args.length; idx++) {
			String val = args[idx];
			int delimIdx = val.indexOf('=');
			if (delimIdx < 0) {
				mapArgs.put(val, null);
			} else if (delimIdx == 0) {
				mapArgs.put("", val.substring(1));
			} else {
				mapArgs.put(
					val.substring(0, delimIdx).trim(),
					val.substring(delimIdx+1)
				);
			}
		}
		
		return mapArgs;
	}

}

edit: another issue, my FPS counter clearly does not work. target fps is 60 but my debug prints +90fps all the time.

Hello Whome.

Thx for making this testrun. To tell the truth when I post it I was hoping someone find a flaw in my design so I could improve it. (It is nearly my first gameloop).

I tried you testrun but I couldn’t reproduce the jump unless I go to like 2000 fps (that eating nearly all my CPU). But, I see some tearing. I not sure to fully understand what cause tearing in a game but I find this with some research. http://www.gamedev.net/community/forums/topic.asp?topic_id=372033. Looks like you can’t remove tearing unless you go fullscreen mode and enable vsync.

btw, what is you debug? Your fps counter print around 61 fps for me but you said it doesn’t work.

Upper-left corner FPS counter was my debug value. When I posted my example code it printed run +90 fps but now I run it again. Its a steady 60-63 fps value. Go figure what was wrong last time, strange. So we come to a conclusion fps calc works after all :slight_smile:

I know and have done few native D3D apps they run fine windowed vsync lock. Its so nice a windowed non-tearing rendering loop, triple buffered flips and low cpu% usage.

Here is an another game loop borrowed from a forum post. Loop animation does not rely on a delta time but each updateWorld() call is one animation frame to be simulated. Slower machines drop renderFPS but loop tries to keep up to a updateFPS. I think it is a bit more consistent fps, but tearing is still visible. About “Toolkit.getDefaultToolkit().sync()” method I think we can forget it.


// java GameLoop2 "fullscreen=true" "fps=60" "vsync=false"
//http://www.java-gaming.org/index.php/topic,19971.0.html

import java.util.*;

import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferStrategy;

import java.awt.DisplayMode; // for full-screen mode

// java GameLoop2 "fullscreen=true" "fps=60" "vsync=false"
public class GameLoop2 implements KeyListener {
    Frame mainFrame;

    private static final long NANO_IN_MILLI = 1000000L;	

    // num of iterations with a sleep delay of 0ms before
    // game loop yields to other threads.
    private static final int NO_DELAYS_PER_YIELD = 16;

    // max num of renderings that can be skipped in one game loop,
    // game's internal state is updated but not rendered on screen.
    private static int MAX_RENDER_SKIPS = 5;

    private static int TARGET_FPS = 60;
	
    //private long prevStatsTime;
    private long gameStartTime;
    private long curRenderTime;
    private long rendersSkipped = 0L;
    private long period; // period between rendering in nanosecs

    long fps;
    long frameCounter;
    long lastFpsTime;
    
    Rectangle2D rect;
    
    public GameLoop2(Map<String,String> args, GraphicsDevice device) {
        try {
            if (args.containsKey("fps"))
              TARGET_FPS = Integer.parseInt(args.get("fps"));


            // Setup the frame
            GraphicsConfiguration gc = device.getDefaultConfiguration();
            
            mainFrame = new Frame(gc);
            mainFrame.setUndecorated(true);
            mainFrame.setIgnoreRepaint(true);
            mainFrame.setVisible(true);
            mainFrame.setSize(640, 480);
            //mainFrame.setLocationRelativeTo();
            mainFrame.setLocation(100,100);
            mainFrame.createBufferStrategy(2);
            mainFrame.addKeyListener(this);

            if ("true".equalsIgnoreCase(args.get("fullscreen"))) {
              device.setFullScreenWindow(mainFrame);
              device.setDisplayMode(new DisplayMode(640, 480, 8, DisplayMode.REFRESH_RATE_UNKNOWN));
            }
          
            final boolean VSYNC = "true".equalsIgnoreCase(args.get("vsync"));

            // Cache the buffer strategy and create a rectangle to move
            BufferStrategy bufferStrategy = mainFrame.getBufferStrategy();
            rect = new Rectangle2D.Float(0,100,64,64);

            // loop initialization
            long beforeTime, afterTime, timeDiff, sleepTime;
            long overSleepTime = 0L;
            int noDelays = 0;
            long excess = 0L;
            gameStartTime = System.nanoTime();
            //prevStatsTime = gameStartTime;
            beforeTime = gameStartTime;
		
            period = (1000L*NANO_IN_MILLI)/TARGET_FPS;  // rendering FPS (nanosecs/targetFPS)
            System.out.println("FPS: " + TARGET_FPS + ", vsync=" + VSYNC);
            System.out.println("FPS period: " + period);
            

            // Main loop
            while(true) {
               // **2) execute physics
               updateWorld(0);					

               // **1) execute drawing
               Graphics g = bufferStrategy.getDrawGraphics();
               drawScreen(g);
               g.dispose();

               // Synchronise with the display hardware. Note that on
               // Windows Vista this method may cause your screen to flash.
               // If that bothers you, just comment it out.
               if (VSYNC) Toolkit.getDefaultToolkit().sync();

               // Flip the buffer
               if( !bufferStrategy.contentsLost() )
                   bufferStrategy.show();

               afterTime = System.nanoTime();
               curRenderTime = afterTime;
               calculateFramesPerSecond();

               timeDiff = afterTime - beforeTime;
               sleepTime = (period-timeDiff) - overSleepTime;
               if (sleepTime > 0) { // time left in cycle
                  //System.out.println("sleepTime: " + (sleepTime/NANO_IN_MILLI));
                  try {
                     Thread.sleep(sleepTime/NANO_IN_MILLI);//nano->ms
                  } catch(InterruptedException ex){}
                  overSleepTime = (System.nanoTime()-afterTime) - sleepTime;
               } else { // sleepTime <= 0;
                  System.out.println("Rendering too slow");
                  // this cycle took longer than period
                  excess -= sleepTime;
                  // store excess time value
                  overSleepTime = 0L;
                  if (++noDelays >= NO_DELAYS_PER_YIELD) {
                     Thread.yield();
                     // give another thread a chance to run
                     noDelays = 0;
                  }
               }
            
               beforeTime = System.nanoTime();
			
               //If the rendering is taking too long, then
               //update the game state without rendering
               //it, to get the UPS nearer to the required frame rate. 
               int skips = 0;
               while((excess > period) && (skips < MAX_RENDER_SKIPS)) {
                  // update state but don’t render
                  System.out.println("Skip renderFPS, run updateFPS");
                  excess -= period;
                  updateWorld(0);
                  skips++;
               }
               rendersSkipped += skips;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            device.setFullScreenWindow(null);
        }
    }

    private void updateWorld(long elapsedTime) {
        // speed: 150 pixels per second
        //double xMov = (140f/(NANO_IN_MILLI*1000)) * elapsedTime;
        double xMov = 140f / (TARGET_FPS);
        rect.setRect(rect.getX()+xMov, 100, 64, 64);        
        if( rect.getX() > mainFrame.getWidth() )
            rect.setRect(-rect.getWidth(), 100, 64, 64);
    }
    
    private void drawScreen(Graphics g) {
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, mainFrame.getWidth(), mainFrame.getHeight());
        g.setColor(Color.WHITE);
        g.drawString("FPS: " + fps, 0, 17);
        
        g.setColor(Color.RED);
        g.fillRect((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight());
    }
    
    private void calculateFramesPerSecond() {
        if( curRenderTime - lastFpsTime >= NANO_IN_MILLI*1000 ) {
            fps = frameCounter;
            frameCounter = 0;
            lastFpsTime = curRenderTime;
        }
        frameCounter++;
    }
	
    public void keyPressed(KeyEvent e) {
        if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
            System.exit(0);
        }
    }
	
    public void keyReleased(KeyEvent e) { }
    public void keyTyped(KeyEvent e) { }

    public static void main(String[] args) {
        try {
	    Map<String,String> mapArgs = parseArguments(args);

            GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice device = env.getDefaultScreenDevice();
            new GameLoop2(mapArgs, device);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


	/**
	 * Parse commandline arguments, each parameter is a name-value pair.
	 * Example: java.exe MyApp "key1=value1" "key2=value2"
	 */
	private static Map<String,String> parseArguments(String[] args) {
		Map<String,String> mapArgs = new HashMap<String,String>();

		for(int idx=0; idx < args.length; idx++) {
			String val = args[idx];
			int delimIdx = val.indexOf('=');
			if (delimIdx < 0) {
				mapArgs.put(val, null);
			} else if (delimIdx == 0) {
				mapArgs.put("", val.substring(1));
			} else {
				mapArgs.put(
					val.substring(0, delimIdx).trim(),
					val.substring(delimIdx+1)
				);
			}
		}
		
		return mapArgs;
	}

}

Hiya,

I highly recommend looking at Kevin Glass’s tutorial. I too use Java2D. Here my loop, set up to run in an applet.



public void run()    {

        // Timer and FPS
        long loopTime = System.currentTimeMillis();
        FPS fps = new FPS();

       
        //Main game loop
        while(bRunning)
        {
            // timer for smoothing graphics
            long timeLapse = System.currentTimeMillis() - loopTime;
            loopTime = System.currentTimeMillis();
            
            // fps counter
            fps.update();


             Graphics2D g2d = (Graphics2D) bufferStrategy.getDrawGraphics();

            // Wipe screen to black
            g2d.setColor(Color.black);
            g2d.fillRect(0, 0, CANVAS_X, CANVAS_Y);

            // Splash screen
            if (state == GameState.SPLASH){
                splash.draw(g2d, highScore);
                lives = NUMBER_OF_LIVES;
            }
            

                g2d.drawString("fps: "+fps.getFPS(), 5, 30);
                drawAndSleep(g2d, loopTime);
            }
           }

 private void drawAndSleep(Graphics2D g2d, long loopTime){


        // clear and flip graphics
        g2d.dispose();

        //Show bufferStrategy
        if(!bufferStrategy.contentsLost())
            bufferStrategy.show();

        long sleep = loopTime+31-System.currentTimeMillis();
        // System.out.println(sleep);   // check to make sure never negative
        try { Thread.sleep(sleep); } catch (Exception e) {}

    }



Ok I took the 2 examples that you gave me and I remove all the things not related to timing so we could see the differences easily.

The one of Whome

private static final long NANO_IN_MILLI = 1000000L;
	private static int MAX_RENDER_SKIPS = 5;
	private static int TARGET_FPS = 60;
	
	boolean running = true;
	long period = (1000L*NANO_IN_MILLI)/TARGET_FPS;
	
	public void run(){

		long beforeTime = System.nanoTime();
		long afterTime, timeDiff, sleepTime;
		long excess = 0L;

		while(running){
			update();
			
			render();
			
			afterTime = System.nanoTime();
			timeDiff = afterTime - beforeTime;
			sleepTime = (period-timeDiff) - excess;
			if (sleepTime > 0) { // time left in cycle
				//System.out.println("sleepTime: " + (sleepTime/NANO_IN_MILLI));
				try {
					Thread.sleep(sleepTime/NANO_IN_MILLI);//nano->ms
				} catch(InterruptedException ex){}
				excess = (System.nanoTime()-afterTime) - sleepTime;
			} else { // sleepTime <= 0;
				System.out.println("Rendering too slow");
				// this cycle took longer than period
				excess -= sleepTime;
			}

			beforeTime = System.nanoTime();

			/* If the rendering is taking too long, then
                update the game state without rendering
                it, to get the UPS nearer to the
                required frame rate. */
			int skips = 0;
			while((excess > period) && (skips < MAX_RENDER_SKIPS)) {
				// update state but don’t render
				System.out.println("Skip renderFPS, run updateFPS");
				excess -= period;
				update();
				skips++;
			}
		}
	}
	
	private void render(){
		//Not implemented
	}
	
	private void update(){
		//Not implemented
	}

And the one of Darrin

public class ExampleLoopNew {
	
	boolean running = true;
	
	public void run(){
		
		long loopBeginTime = System.currentTimeMillis();
		long loopTimeLapse;
		
		while(running){
			
			loopTimeLapse = System.currentTimeMillis() - loopBeginTime;
			loopBeginTime = System.currentTimeMillis();
			
			render();
			
			long sleep = (loopBeginTime+31) - System.currentTimeMillis();
			// System.out.println(sleep);   // check to make sure never negative
			try { Thread.sleep(sleep); } catch (Exception e) {}
		}
	}
	
	private void render(){
		//Not implement
	}

}

and now mine (with somes modification)

public class ExampleLoopMine {
	
	long desiredFPS = 60;
    long desiredDeltaLoop = (1000*1000*1000)/desiredFPS;
    
	boolean running = true;
	
	public void run(){
		
		long beginLoopTime;
		long endLoopTime;
		long currentUpdateTime = System.nanoTime();
		long lastUpdateTime;
		long deltaLoop;
		
		while(running){
			beginLoopTime = System.nanoTime();
			
			render();
			
			lastUpdateTime = currentUpdateTime;
			currentUpdateTime = System.nanoTime();
			update(currentUpdateTime - lastUpdateTime);
			
			endLoopTime = System.nanoTime();
			deltaLoop = endLoopTime - beginLoopTime;
	        
	        if(deltaLoop > desiredDeltaLoop){
	            //Do nothing. We are already late
	        }else{
	            try{
	                Thread.sleep((desiredDeltaLoop - deltaLoop)/(1000*1000));
	            }catch(InterruptedException e){
	                //Do nothing
	            }
	        }
		}
	}
	
	private void render(){
		//Not implemented
	}
	
	private void update(long deltaTime){
		//Not implemented
	}

}

Anyone can tell what make one better than the other?

I can’t understand what is the use of this code in the loop Whome show

if (++noDelays >= NO_DELAYS_PER_YIELD) {
                   Thread.yield();
                   // give another thread a chance to run
                   noDelays = 0;
                }

In the comment it said “give another thread a chance to run” but other thread still run no matter this thread is running right? And if I’m right Thread.yield() doesn’t free up CPU time.

EDIT : I answered myself http://www.java-forums.org/new-java/12990-difference-between-thread-yield-thread-sleep-methods.html

a lot of familiar code,
finally the studying started to pay off

(though i should stop and start writing some code, otherwise what is the point??)

(hint, Andrew Davison, Killer Game Programming in Java, Chapter 2 )

(not really sure about the chapter)