July 23, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

J2ME Game Optimization Secrets

  • July 14, 2003
  • By Mike Shivas
  • Send Email »
  • More Articles »

This article describes the role that code optimization plays in writing fast games for mobile devices. Using examples I will show how, when and why to optimize your code to squeeze every last drop of performance out of MIDP-compliant handsets. We will discuss why optimization is necessary and why it is often best NOT to optimize. I explain the difference between high-level and low-level optimization and we will see how to use the Profiler utility that ships with the J2ME Wireless Toolkit to discover where to optimize your code. Finally this article reveals lots of techniques for making your MIDlets move.

Why Optimize?

It is possible to divide video games into two broad categories; real-time and input-driven. Input-driven games display the current state of game play and wait indefinitely for user input before moving on. Card games fall into this category, as do most puzzle games, strategy games and text adventures. Real-time games, sometimes referred to as Skill and Action games do not wait for the player - they continue until the game is over.

Skill and Action games are often characterized by a great deal of on-screen movement (think of Galaga or Robotron). Refresh rates must be at least 10fps (frames per second) and there has to be enough action to keep gamers challenged. They require fast reflexes and good hand-eye co-ordination from the player, so compelling S&A games must also be extemely responsive to user input. Providing all that graphical activity at high framerates while responding quickly to key-presses is why code for real-time games has to be fast. The challenges are even greater when developing for J2ME.

Java 2 Micro Edition (J2ME) is a stripped-down version of Java, suitable for small devices with limited capabilities, such as cell phones and PDAs. J2ME devices have:

  • limited input capabilities (no keyboard!)
  • small display sizes
  • restricted storage memory and heap sizes
  • slow CPUs

Writing fast games for the J2ME platform further challenges developers to write code that will perform on CPUs far slower than those found on desktop computers.

When not to Optimize

If you're not writing a Skill and Action game, there's probably no need to optimize. If the player has pondered her next move for several seconds or minutes, she will probably not mind if your game's response to her action takes more than a couple hundred milliseconds. An exception to this rule is if the game needs to perform a great deal of processing in order to determine its next move, such as searching through a million possible combinations of chess pieces. In this case, you might want to optimize your code so that the computer's next move can be calculated in a few seconds, not minutes.

Even if you are writing this type of game, optimization can be perilous. Many of these techniques come with a price tag - they fly in the face of conventional notions of "Good" programming and can make your code harder to read. Some are a trade-off, and require the developer to significantly increase the size of the app in order to gain just a minor improvement in performance. J2ME developers are all too familiar with the challenges of keeping their JAR as small as possible. Here are a few more reasons not to optimize:

  • optimization is a good way to introduce bugs
  • some techniques decrease the portability of your code
  • you can expend a lot of effort for little or no results
  • optimization is hard

That last point needs some illumination. Optimization is a moving target, only more so on the Java platform, and even more so with J2ME because the execution environments vary so greatly. Your optimized code might run faster on an emulator, but slower on the actual device, or vice versa. Optimizing for one handset might actually decrease performance on another.

But there is hope. There are two passes you can make at optimization, a high-level and a low-level. The first is likely to increase execution performance on every platform, and even improve the overall quality of your code. The second pass is the one more likely to cause you headaches, but these low-level techniques are quite simple to introduce, and even simpler to omit if you don't want to use them. At the very least, they're very interesting to look at.

We will also use the System timer to profile your code on the actual device, which can help you to gauge exactly how effective ( or utterly ineffective ) these techniques can be on the hardware you're targeting for deployment.

And one last bullet-point:

  • optimizing is fun

A Bad Example

Let's take a look at a simple application that consists of two classes. First, the MIDlet...

import javax.microedition.midlet.*;import javax.microedition.lcdui.*;public class OptimizeMe extends MIDlet implements CommandListener {  private static final boolean debug = false;  private Display display;  private OCanvas oCanvas;  private Form form;  private StringItem timeItem = new StringItem( "Time: ", "Unknown" );  private StringItem resultItem =                             new StringItem( "Result: ", "No results" );  private Command cmdStart = new Command( "Start", Command.SCREEN, 1 );  private Command cmdExit = new Command( "Exit", Command.EXIT, 2 );  public boolean running = true;  public OptimizeMe() {    display = Display.getDisplay(this);    form = new Form( "Optimize" );    form.append( timeItem );    form.append( resultItem );    form.addCommand( cmdStart );    form.addCommand( cmdExit );    form.setCommandListener( this );    oCanvas = new OCanvas( this );  }  public void startApp() throws MIDletStateChangeException {    running = true;    display.setCurrent( form );  }  public void pauseApp() {    running = false;  }  public void exitCanvas(int status) {    debug( "exitCanvas - status = " + status );    switch (status) {      case OCanvas.USER_EXIT:        timeItem.setText( "Aborted" );        resultItem.setText( "Unknown" );      break;      case OCanvas.EXIT_DONE:        timeItem.setText( oCanvas.elapsed+"ms" );        resultItem.setText( String.valueOf( oCanvas.result ) );      break;    }    display.setCurrent( form );  }  public void destroyApp(boolean unconditional)                           throws MIDletStateChangeException {    oCanvas = null;    display.setCurrent ( null );    display = null;  }  public void commandAction(Command c, Displayable d) {    if ( c == cmdExit ) {      oCanvas = null;      display.setCurrent ( null );      display = null;      notifyDestroyed();    }    else {      running = true;      display.setCurrent( oCanvas );      oCanvas.start();    }  }  public static final void debug( String s ) {    if (debug) System.out.println( s );  }}

Second, the OCanvas class that does most of the work in this example...

import javax.microedition.midlet.*;import javax.microedition.lcdui.*;import java.util.Random;public class OCanvas extends Canvas implements Runnable {  public static final int USER_EXIT = 1;  public static final int EXIT_DONE = 2;  public static final int LOOP_COUNT = 100;  public static final int DRAW_COUNT = 16;  public static final int NUMBER_COUNT = 64;  public static final int DIVISOR_COUNT = 8;  public static final int WAIT_TIME = 50;  public static final int COLOR_BG = 0x00FFFFFF;  public static final int COLOR_FG = 0x00000000;  public long elapsed = 0l;  public int exitStatus;  public int result;  private Thread animationThread;  private OptimizeMe midlet;  private boolean finished;  private long started;  private long frameStarted;  private long frameTime;  private int[] numbers;  private int loopCounter;  private Random random = new Random( System.currentTimeMillis() );  public OCanvas( OptimizeMe _o ) {    midlet = _o;    numbers = new int[ NUMBER_COUNT ];    for ( int i = 0 ; i < numbers.length ; i++ ) {      numbers[i] = i+1;    }  }  public synchronized void start() {    started = frameStarted = System.currentTimeMillis();    loopCounter = result = 0;    finished = false;    exitStatus = EXIT_DONE;    animationThread = new Thread( this );    animationThread.start();  }  public void run() {    Thread currentThread = Thread.currentThread();    try {      while ( animationThread == currentThread && midlet.running                                                && !finished ) {        frameTime = System.currentTimeMillis() - frameStarted;        frameStarted = System.currentTimeMillis();        result += work( numbers );        repaint();        synchronized(this) {          wait( WAIT_TIME );        }        loopCounter++;        finished = ( loopCounter > LOOP_COUNT );      }    }    catch ( InterruptedException ie ) {      OptimizeMe.debug( "interrupted" );    }    elapsed = System.currentTimeMillis() - started;    midlet.exitCanvas( exitStatus );  }  public void paint(Graphics g) {    g.setColor( COLOR_BG );    g.fillRect( 0, 0, getWidth(), getHeight() );    g.setColor( COLOR_FG );    g.setFont( Font.getFont( Font.FACE_PROPORTIONAL,          Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ) );    for ( int i  = 0 ; i < DRAW_COUNT ; i ++ ) {      g.drawString( frameTime + " ms per frame",                     getRandom( getWidth() ),                     getRandom( getHeight() ),                     Graphics.TOP | Graphics.HCENTER );    }  }  private int divisor;  private int r;  public synchronized int work( int[] n ) {    r = 0;    for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {      for ( int i = 0 ; i < n.length ; i++ ) {        divisor = getDivisor(j);        r += workMore( n, i, divisor );      }    }    return r;  }  private int a;  public synchronized int getDivisor( int n ) {    if ( n == 0 ) return 1;    a = 1;    for ( int i = 0 ; i < n ; i++ ) {      a *= 2;    }    return a;  }  public synchronized int workMore( int[] n, int _i, int _d ) {    return n[_i] * n[_i] / _d + n[_i];  }  public void keyReleased(int keyCode) {    if ( System.currentTimeMillis() - started > 1000l ) {      exitStatus = USER_EXIT;      midlet.running = false;    }  }  private int getRandom( int bound )   {  // return a random, positive integer less than bound    return Math.abs( random.nextInt() % bound );  }}

This app is a MIDlet that simulates a simple game loop:

  • work
  • draw
  • poll for user input
  • repeat

For fast games, this loop has to be as tight and fast as possible. Our loop continues for a finite number of iterations (LOOP_COUNT = 100) and uses the System timer to calculate how long in milliseconds the whole exercise took, so we can measure and improve its performance. The time and the results of the work are displayed on a simple Form. Use the Start command to begin the test. Pressing any key will exit the loop prematurely. The Exit command exits the application.

In most games, the work phase of the main game loop involves updating the state of the game world - moving all the actors, testing for and reacting to collisions, updating scores, etc. In this example, we're not doing anything particularly useful. The code simply runs through an array of numbers and performs a few arithmetic operations on each, aggregating the results in a running total.

The run() method also calculates the amount of time it takes to execute each iteration of the loop. Every frame, the OCanvas.paint() method displays this value in milliseconds at 16 random locations on screen. Normally you would be drawing the graphical elements of your game in this method, but our code offers a reasonable facsimile of this process.

Regardless of how pointless this code may seem, it will allow us ample opportunity to improve its performance.



Page 1 of 5



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel