Wednesday, September 16, 2009

Programming java applet games part 3 - Game loop

ma
Lets go to the next step by demonstrating the basic structure of every game, The game loop witch from a programming standpoint is the main component of any game that you have ever played. The game loop is the routine that manages control every game event, user input, graphics and sound output to produce the software that we all like. Most of the software that we use does not produce any output unless the user give a command. For example an image editor will not print not even a line unless you do not click on a canvas. Games does not follows this model, the show must go on even if the user will not press not even a key. The game loop handles to produce events and actions without user reaction. A typically game loop have to looks like
while( isGameOver() == false ) {
  checkUserInput();
  checkAI();
  updateGameAvatars();
  resolveAvatarCollisions();
  renderGraphics();
  playSounds();
} 
Every game is almost based on this idea, the difference between games is in the code that is needed for this methods. Off course today we have multitasking operating systems with multi-core CPUs and many games user threads to increase the performance by execute some of these methods in parallel. As you can imagine much of the code that must be run at AI is independent from the check for user input and we can implement them using threads. As CPUs technologies focus more and more to multiply the cores that exist on a CPU chipset the usage of parallel algorithms in computer programming will continue to be increased, These days even game consoles like PS 3 and XBOX 360 have multi-core CPUs.
Now lets see how we can implement the game loop in a Java applet.  Applets are event driven components. that means that by their nature needs user and browser input to interact. There are specific methods that are called by the browsers during the lifetime of an Applet.
init();  
This is where programmers put initialization code for the applets and they are called firts by the browser. You can see init as the default constructor of a class.
start();  
This method is called when starts the execution of an applet and after its initialization
stop(); 
Called by the browser or applet viewer to inform this applet that it should stop its execution. It is called when the Web page that contains this applet has been replaced by another page, and also just before the applet is to be destroyed. An applet should override this method if it has any operation that it wants to perform each time the Web page containing it is no longer visible.
destroy(); 
Called by the browser to inform this applet that it is being reclaimed and that it should destroy any resources that it has allocated. The stop method will always be called before destroy. It contains the clean up code of our applet.
update();
Is called when an area of the the applet screen needs to be redrawn 
paint();
Is called when you need to re-render the 100% of the applets area.  For extended reference of this methods you must read the javadocs

After all these, it is time to define our Applet

package games.applet.Asteroid;
import java.applet.Applet;
import java.awt.Graphics;
public class AsterpodsApplet extends Applet {
    public void init() {
    }
    public void start() {
    }
    public void stop() {
    }
    public void update( Graphics g ) {
        paint( g );
    }
    public void paint( Graphics g ) {
        // your code here;
    }
}

As i said Applets are event driven and so everything  are executed after  events (ie user just pressed a key, user moves the mouse etc ) and in a single thread. So to escape this fact and run  the game loop continuously we will create a separate thread.  It is not my goal to make a tutorials about threads so for now just accept the simple code that i post and you can read some thing aboyt java Threads at the javadocs

http://java.sun.com/j2se/1.3/docs/api/java/lang/Thread.html
 

For now just accept that every applet that needs to run a thread must implement the Runnable interface.  Ok this is not true but just accept it.  Runnable interface needs only one method to be implemented the

public void run();
This is where our main loop belongs
package games.applet.Asteroid;

import java.applet.Applet;
import java.awt.Graphics;

public class AsterpodsApplet extends Applet implements Runnable {

    private Thread mainLoop;
    private boolean gameOver = false;

    @Override
    public void init() {
        mainLoop = new Thread( this );
    }

    @Override
    public void start() {
        mainLoop.start();
    }

    @Override
    public void stop() {
        gameOver = true;
    }

    @Override
    public void update( Graphics g ) {
        paint( g );
    }

    @Override
    public void paint( Graphics g ) {
        // your code here;
    }

    public void run() {
        while ( gameOver == false ) {
            checkUserInput();
            runAI();
            updateUser();
            updateEnemies();
            repaint(); //rapaints the applet
            Thread.yield();
        }
    }

    private void checkUserInput() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void runAI() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void updateUser() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void updateEnemies() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }
}
So the start and stop methods start and stop the game loop. One important thing in game loop is to produce accurate timing. Every time that a game loop is executed consumes different CPU time. I will not explain why this happens but trust my it happens. To avoid the time hazards we need to  specify a variant time time window for the mainLoop thread to sleep to balance the time lose. There are two ways to consume time
a) Pause the game every time by calling currentThread.sleep(int)
b) Calling System.currentTimeMillis() to keep track of the time changes.
I will use both of them :-)
So lets say that we want to have maximum 20 frame per second, we need our game loop to last about 50 milliseconds. We can do this by calculating the time that a loop has consumed and then by sleeping 50 - loopTIme
So the run method changes to
 public void run() {
        long lastTime; 
        long lostTime;
        while ( gameOver == false ) {
            try {
                lastTime = System.currentTimeMillis();
                checkUserInput();
                runAI();
                updateUser();
                updateEnemies();
                repaint(); //rapaints the applet
                lastTime = System.currentTimeMillis() - lastTime;
                Thread.sleep( GAME_LOOP_MAX_TIME - lastTime );
            } catch ( InterruptedException ex ) {
                Logger.getLogger( AsterpodsApplet.class.getName() ).log( Level.SEVERE, null, ex );
            }
        }
    }
The last words for today, i will try to explain how to get input from keyboard. One way to accept keyboard input is to implement the KeyListener interface and this can be done by implement the methods
public void keyTyped( KeyEvent e ) ;
is called  when a key is typed
public void keyPressed( KeyEvent e );
is called  when a key is pressed
public void keyReleased( KeyEvent e );
is called  when a key is released

These methods as you can see the parameter e is an instance of the class KeyEvent. This is a class that contains information about the key that is typed/pressed/released. As always there is extended information about this class at the javadoc
http://java.sun.com/j2se/1.3/docs/api/java/awt/event/KeyEvent.html

in our game we need to track the arrow keys, the space key and the z. So the code for keyPressed and keyReleased switch on and off boolean variables variables. So the applet finaly is 


package games.applet.Asteroid;

import java.applet.Applet;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author alexm
 */
public class AsterpodsApplet extends Applet implements Runnable, KeyListener {

    private static final int GAME_LOOP_MAX_TIME = 50;
    private Thread mainLoop;
    private boolean gameOver = false;
    private boolean up;
    private boolean down;
    private boolean left;
    private boolean right;
    private boolean space;
    private boolean z;

    @Override
    public void init() {
        mainLoop = new Thread( this );
        up = false;
        down = false;
        left = false;
        right = false;
        space = false;
        z = false;
    }

    @Override
    public void start() {
        mainLoop.start();
    }

    @Override
    public void stop() {
        gameOver = true;
    }

    @Override
    public void update( Graphics g ) {
        paint( g );
    }

    @Override
    public void paint( Graphics g ) {       
    }

    public void run() {
        long lastTime;
        long lostTime;
        while ( gameOver == false ) {
            try {
                lastTime = System.currentTimeMillis();
                checkUserInput();
                runAI();
                updateUser();
                updateEnemies();
                repaint(); //rapaints the applet
                lastTime = System.currentTimeMillis() - lastTime;
                Thread.sleep( GAME_LOOP_MAX_TIME - lastTime );
            } catch ( InterruptedException ex ) {
                Logger.getLogger( AsterpodsApplet.class.getName() ).log( Level.SEVERE, null, ex );
            }
        }
    }

    private void checkUserInput() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void runAI() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void updateUser() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    private void updateEnemies() {
        throw new UnsupportedOperationException( "Not yet implemented" );
    }

    public void keyTyped( KeyEvent e ) {
    }

    public void keyPressed( KeyEvent e ) {
        if ( e.getKeyCode() == KeyEvent.VK_UP ) {
            up = true;
        }
        if ( e.getKeyCode() == KeyEvent.VK_DOWN ) {
            down = true;
        }
        if ( e.getKeyCode() == KeyEvent.VK_LEFT ) {
            left = true;
        }
        if ( e.getKeyCode() == KeyEvent.VK_RIGHT ) {
            right = true;
        }
        if ( e.getKeyCode() == KeyEvent.VK_SPACE ) {
            space = true;
        }
        if ( e.getKeyCode() == KeyEvent.VK_Z ) {
            z = true;
        }
    }

    public void keyReleased( KeyEvent e ) {
        if ( e.getKeyCode() == KeyEvent.VK_UP ) {
            up = false;
        }
        if ( e.getKeyCode() == KeyEvent.VK_DOWN ) {
            down = false;
        }
        if ( e.getKeyCode() == KeyEvent.VK_LEFT ) {
            left = false;
        }
        if ( e.getKeyCode() == KeyEvent.VK_RIGHT ) {
            right = false;
        }
        if ( e.getKeyCode() == KeyEvent.VK_SPACE ) {
            space = false;
        }
        if ( e.getKeyCode() == KeyEvent.VK_Z ) {
            z = false;
        }
    }
}


So until next time, have a nice coding time my friends

No comments:

Post a Comment