Chapter 11. Making the Most of It: Optimizations

So you've finished your game. It's time to break out the champagne. But then you load your game onto your favorite Java device and get an out-of-memory message. What now?

Well, you basically have two choices: Only release the game on devices that have enough speed and memory, or rewrite, optimize, and strip your game down to make it fit.

A Limited World

Most devices that run MIDP are very limited in memory size, processor power, display resolution, and connectivity. Manufacturers have made sacrifices in the hopes of creating devices that are portable, battery-powered, and cheap. Initially, most handheld devices were intended for business users who wanted to check e-mail or do an Internet search out of the office. More and more, however, manufacturers are gearing these devices to a broader audience. An audience thirsting for good games. And even though we're beginning to see more color screens, better sound capabilities, and faster processors, handheld devices are always going to be pretty restricted compared to their big brother, the PC.

Because of these steep limitations, developers—especially game developers—must focus like mad on making applications smaller, faster, economical, and optimal.

Making Code Optimal

Making game code optimal is not an easy task. As a matter of fact, half the work in game development for small devices is in the optimization stage. But with experience, you will learn where typical problems are and how to solve them.

Code optimization can be divided into five categories:

  • Optimizations to reduce code size

  • Optimizations to speed up the code execution

  • Optimizations to decrease memory usage

  • Optimizations to increase device availability

  • Optimizations to increase network performance

Code Size Optimizations

When a game is installed onto a device via a synch cable or over the network, the JAR file is unpacked into the device's storage memory. Every time the game is executed, the execution byte code (Java classes) are copied into the working memory (heap memory) and processed. The larger the classes are, the less heap memory is left for the game. The less heap memory, the slower things run and the greater the chances that the game will run out of memory completely. So, given all that, making game classes as small as possible is of the utmost importance.

Making Code Faster

According to specification, devices that run J2ME CLDC have 16- or 32-bit processors with low frequencies, in the range between a few megahertz (smart phones), to a few hundred (Windows CE-based devices). The higher the clock speed is, the more power-hungry devices are. Unfortunately, all small devices are limited by their batteries or accumulators—and most devices aim to have at least 24 hours of continuous battery usage. As such, device manufacturers have purposely installed low-powered chipsets. Making code run fast, then, is another key goal of micro game development.

Decreasing Memory

Devices have different ranges of available working memory. Smart phones only have between 128KB and 256KB of heap memory. Developers who write games in native assembly code, or a low-level languages such as C++, can easily compact their code to be as efficient as possible. Java developers, however, must deal with a lot of overhead, such as memory allocation (object construction) and deallocation (garbage collection). A chunk of memory is also eaten up by the Java Virtual Machine (KVM), which runs the byte code. As you begin to create Java games and test them on real devices, you'll probably run across out-of-memory error messages.

Device Availability

Different actions, such as the background display light or vibrations, might consume extra power and force the device into off-line mode. With correct development, and by avoiding the overuse of various calls, your game can have a better impact on a device's lifetime.

Network Performance

If your game is multiplayer-capable, then you'll need to send and receive messages over the network. In most cases, this network is awfully slow, with high latency and limited bandwidth. As such, it is important to make sure that every packet your game deals with is as compressed as possible.

More information about network optimization can be found in Chapter 20, “Connecting Out: Wireless Networking.”

Code Size Reductions

Making the execution code or JAR file smaller has several positive impacts on devices and game players:

  • The smaller a JAR file is, the less time it takes to download it over the Internet. With fewer bytes to transfer, gamers will be charged less by their mobile operators, and be online and playing much faster.

  • The smaller execution code is, the less time it takes to install, verify, and execute a game. Users will not have to wait for delays between stages.

  • The less space classes take, the more heap memory is left for the game. Games usually allocate large tables (for example, for level design, a list of enemies, and so on) might run out of memory if there's not enough elbow room.

  • Some devices have explicit limits on the size of a JAR file. For example, i-mode applications may not be larger than 10 kilobytes.

Please note that code size optimizations are not always sufficient. Often times, game functionality must be stripped down or simplified.

There are three techniques used to perform code reduction:

  • Shorten the names of variables and methods in the code.

  • Avoid a pure object-oriented way of programming.

  • Make the image sizes smaller.

Obfuscators and Name-Shortening

When developing in an assembly language, every variable or method is represented by an address (16- or 32-bit in small device processors) that points to a memory cell. Whatever mnemonic a developer uses for a variable or method name, the size of the variable in memory stays the same (2 or 4 bytes). Needless to say, Java is not like assembly language. Instead, Java's focus is on making code easy to write and maintain. Typical variable and method names might be GoldMonster or LowLevelInterfaceControl. In the world of desktop computing, where machines have a few hundred extra megabytes to play around with, there's usually no need to give these names a second thought. But in the limited world of handhelds, every little byte counts:

  • Each letter in the name of the class adds an additional byte to the execution code.

  • Each letter in the name of the public method adds an additional byte to the execution code. Protected and private methods don't have any impact on the class size.

  • Each letter in the name of the public variable adds an additional byte to the execution code. Protected and private variables don't have any influence on the size of the class. Also, the name length of local variables and parameters don't change the size of the class.

  • Every letter in the name of the constructor adds an additional byte to the class file.

Does this mean that you should try to write a game using tiny method and variable names? Imagine changing all method names like moveEnemy(), drawScene(), and makeNextMove() into a(), b(), and c(). Not fun! The length of the class file would immediately be shortened, but the source code would be muddled and impossible to maintain.

There is a better solution. Instead of making the source code dirty and unreadable, developers can use a special application called an obfuscator.

An obfuscator's main job is to protect applications against illegal decompilation by making the code hard to read and difficult to unravel. The obfuscator is run only when the code is ready to be released. It takes normal Java files and outputs tight, special class files. Luckily for J2ME developers, most obfuscators will also drastically shorten class, variable, and method names. An obfuscator's output code is typically 5 to 20 percent smaller than original class files. The size of the reduction is based on the number of classes, methods, and variables.

There are many different obfuscators on the market, some available commercially and some free. Check them out:

For example, to run Jax on a class, you would use the following code fragment:

java jax TestClass.class

Jax will output the compressed class into a Zip file called TestClass_jax.zip.

NOTE

Note that you may not want to change the name of your main MIDlet class. Most obfuscators let you specify classes whose names should not be changed.

The Object-Oriented Dilemma

Java is an object-oriented language. Developers typically separate their code into many different objects, each represented by a separate class. Each class has number of public methods and variables available to other classes, and then has private methods for the class' internal usage.

With good object-oriented design, programmers can easily reuse pre-developed components and speed up the development cycle. Code is also more tightly written and separated by functionality. For example, a game may typically have a separate class for game logic and one for actually drawing visual graphics.

The following illustrates a typical object-oriented approach, in which two different classes will use the same method name to achieve different functionality. Class Ext1 sets the global variable to the same value as the parameter, and class Ext2 sets it to the square value of the parameter:

public abstract class Base
{
  protected int value;

  public abstract void setValue(int value);
}
public class Ext1 extends Base
{
  public void setValue(int value)
  {
    this.value = value;
  }
}

public class Ext2 extends Base
{
  public void setValue(int value)
  {
    this.value = value * value;
  }
}

This type of programming is very useful if you want to create an abstract concept that acts differently at different times. For example, you might have a RaceTrack interface that returns different values depending on whether the track is in the country or the city.

The result of the three classes listed above, however, is that they take up 654KB worth of storage space. Another option is to use a non-object-oriented approach, and put all three classes into one:

public class Base
{
  public static final int EXT1 = 0;
  public static final int EXT2 = 1;

  private int type;
  private int value;

  public Base(int type)
  {
    this.type = type;
  }

  public void setValue(int value)
  {
    switch (type)
    {
      case EXT1:
          this.value = value;
          break;
      case EXT2:
          this.value = value * value;
          break;
      default:
    }
  }
}

In this case, the execution code only takes up 431 bytes. The code size is reduced by one third!

Although it is not recommended that you abandon all object-oriented techniques altogether, you should be aware of not over-designing your game into too many classes. A good rule of thumb is to design and build your game in a way that is easiest for you, and when you are ready to release the game, combine classes that don't offer much extra functionality.

WARNING

Some devices, such as the Siemens Java phone, have a 16KB class size limit. In this case, it may make sense to split big classes into several objects.

Image Size Reduction

Graphics are the centerpiece of most modern games. Graphics are everywhere—in the introductory animation, in sprites, and in cutscenes. These image files can really add up.

Everything that a J2ME game needs is stuffed into one JAR file. Too many individual images can quickly inflate the JAR file beyond its suggested maximum size.

The PNG image format used with MIDP has a large amount of overhead. PNG files are 24-bit and include a complete palette in the PNG header. If you put twenty image files in your JAR file, you will also be putting in 20 copies of the same palette.

One idea is to squeeze images together by putting them into one image file, like a filmstrip. That way, one file can have multiple images, but only one palette. To grab the image from this filmstrip, your program must clip the screen appropriately. More information about clipping can be found in Chapter 14, “Low-Level Approach.”

Another means of reducing the JAR file size is to move images off the local JAR file and onto the network. This way, images are downloaded over HTTP from a Web server as they are needed. The images can even be stored in the device's local database. More information about downloading and storing images can be found in Chapter 16, “Managing Your Sprites.”

Speeding Up the Code

In the world of game development, animation and gameplay are often expressed as frames per second, or FPS. A typical 3D console game may animate millions of polygons at 24 FPS. The time it takes for a human eye to judge a series of still images as “moving” ranges from 20 to 30 frames per second. Any less than that and the animation or film begins to look choppy, like an old Charlie Chaplin movie.

On handheld devices, users are much more forgiving. As long as a game animates at 10 frames per second or better, things will appear relatively smooth.

However, running 10 FPS is far from easy. For example, if the animation consists of a UFO sprite flying across the screen, then every frame of the game must move the sprite, recalculate the position using complex physics, look for collisions, and so on. To achieve 10 FPS, all of this must be done in fewer than 100 milliseconds.

Here are some cold hard facts about mobile devices and processor power:

  • The processors are 16- or 32-bit.

  • The frequency of the processors is extremely low compared to PCs. For example, the Siemens SL45i runs at 13MHz, and the Motorola Accompli A008 runs at 33MHz.

  • To provide low power consumption, the processors have the smallest possible number of transistors. That means there's no floating-point support, no extra memory management units, and so on.

Given these parameters, a thoughtlessly developed J2ME game will run at only 1 or 2 frames per second. No game player in the world will stand for that.

The following optimizations can help speed up your game's execution:

  • Optimize the call for garbage collection.

  • Void constructing new objects.

  • Use static methods instead of object methods.

  • Speed up the screen repainting.

Dealing with the Garbage Collector

For many developers, Java's memory management is one of its most attractive features. A Java developer never has to deal directly with memory allocation and deallocation. When an object is created, Java automatically grabs the memory it needs. When the object is no longer needed, you simply set it to null. Java's garbage collector destroys the object and cleans up the memory after the destruction.

Garbage collection only reclaims memory when it determines that the object is unreachable from any part of the application. Since the Java language specifications don't provide any rules for how the garbage collection should be invoked, each JVM uses its own implementation. The garbage collector generally runs in its own thread and at its own pace.

J2ME MIDP devices are limited not only to the heap memory, but also to the memory needed for implementation of the device's Kilobyte virtual machine. In order to make the KVM small, some functionality has been stripped down or implemented in a more primitive way. As such, garbage collection is usually not very sophisticated on handheld devices. Sometimes creating too many objects can confuse the garbage collector and cause an out-of-memory situation.

To avoid application crash situations, you should call garbage collection manually whenever possible. The process can be invoked by calling

System.gc();

or

Runtime.getRuntime().gc();

In other words, the second you are done using any object and reach a good spot in your game to pause, set it to null and notify the garbage collector. A good time to call the garbage collector is after your screen paints. That way, the garbage collector will not kick up in the middle of animations, making sprites seem choppy or inconsistent.

The Constructorless Way

A constructor is called every time a class is created. When the constructor is called, two steps are involved during class instantiation:

  1. Memory space needed for the object is allocated.

  2. Any additional code in the constructor method is called. The constructor may also call a constructor of the parent class.

Both steps usually take quite a bit of time. Creating too many classes within your game loop will cause needless delays. Instead, you should create any objects you need before the main game loop actually begins. Of course, you must be careful here: Too many created objects might cause an out-of-memory situation.

One good design pattern is to use a pool of created objects that are available to the application as necessary. When the object is needed, it can be borrowed from the pool. After using it, the application returns the object back to the pool for later reuse.

Math Classes

Custom-created mathematical classes in particular can cause lots of constructor problems. For example, many MIDP programs will have a class simulating floating-point numbers. Often, it is tempting to create each number as a separate object:

Float number1 = new Float(100);

Making each number its own object is convenient. You can easily call various methods on the number to add it, subtract it, and so forth. The big problem with this approach, however, is that each new mathematical operation takes extra time and memory for object construction and garbage collection. If a lot of small objects are created during the game execution, the memory might become fragmented. If memory becomes too fragmented, a new large object can't be created even if there is plenty of memory left, because there is not enough clean, consecutive memory space.

A better way of implementing a mathematical library is to create one singleton class that is always accessible. This class can have static methods that allow mathematical functions to be called at any time.

Static Methods

Static methods belong to entire classes, not to individual objects. A program can call a static method without constructing a new object. As such, using lots of static methods can speed up your game's execution time and increase the size of available heap memory.

Of course, there is also a downside: Static methods can only call other static methods, and only access static variables. This limits their usability.

A good example of static methods usage can be found in a the Cache class detailed in Chapter 9, “Creating A MIDlet.” This class holds all game-wide information, such as the language, screen resolution, list of sprites, and so on. You should strive to put any commonly used variables or other info as static variables within static classes.

The Fast-Draw

A game is a series of actions that constantly repeat. A typical game animation loop will usually perform the following actions:

  • Read in user input

  • Calculate new position of sprites

  • Animate the background

  • Check for collisions

  • Draw the complete scene

Calculating sprite position can take a nice chunk of available cycle time. The more complex a game is, the more intricate the game's artificial intelligence (AI) is. One helpful technique is to place the game's AI in a separate thread, so that animation isn't waiting on game logic.

Another blocking point is collision detection. The more sprites you have, the more collision detection routines you will need to run. Chapter 16 contains several techniques for speeding up collision detection.

Obviously, you want to strive to get through the game loop as quickly as possible. The faster you can get all the calculations done with, the faster your frame rate will be. However, some frames may finish much more quickly than others. This means that your animations may appear a bit herky-jerky, with sprites sometimes racing across the screen and sometimes moping.

Some devices support double buffering. These devices accept all paint calls and execute them on an offscreen image located in device memory. When the paint() method is complete, the device will automatically flush the offscreen image onto the device's display. If double buffering is not supported, you will need to implement it yourself to avoid screen-flickering. Creating this additional offscreen image can take some more free memory, and the extra drawing routine can take additional more time.

When double buffering, try not to clear and redraw every sprite on your entire game screen each and every frame. Instead, you can merely delete pieces of the scene where the sprite was located before the new movement occurred. You can then draw the sprite at its new location.

Another good idea is to keep track of your frame rate by checking the system clock. You may even want to slow down extra-fast frames using the Thread.sleep() method.

More information about these and other animation techniques can be found in Chapter 17, “Sprite Movement.”

In the end, though, the main graphical bottleneck is the device's graphics driver. The driver's main job is to connect an application paint call with the device's actual display. Unfortunately, the driver is not optimized like the ones found in personal computers, and doesn't have any additional accelerators for fast painting. In fact, many micro devices take more than 100 milliseconds to paint a typical screen.

Micro Java game developers will have to separate all Java-enabled devices into two groups: Those that are fast enough and those that are hopeless. For example, smart phones such as Motorola's i85 and Siemens' SL45i are faster than PDA-like phones with big screens such as the Motorola Accompli A008.

Using Less Memory

When a game executes, it usually needs to allocate lots and lots of memory. Every sprite, image, game state, and other piece of the virtual world will require a bit of memory. Since devices have a limited amount of memory, games should strive to create each level or scene only as needed.

Memory usage can also be controlled by using some smart coding techniques. Two of the most frequent memory problems occur while creating text and using lists.

String Versus StringBuffer

After a String object is created, the contents of the object can't actually be changed. Whenever you modify a String, you are actually creating one or more new String objects. That means the concatenation of two Strings actually creates three separate objects. The same problem occurs with other String operations such as inserting characters, converting to uppercase, or parsing out a substring.

A much more efficient way to manipulate character arrays is the StringBuffer class. StringBuffer objects automatically expand as needed. For example, if you concatenate two StringBuffers, then a new array is created and the old arrays are copied within. StringBuffer operations are much faster than using Strings.

Here's an example of using a StringBuffer instead of a String:

public class StringLib
{
  private StringLib() {}

  public static String getTime(int time)
  {
    StringBuffer buf = new StringBuffer();
    buf.append("""Time: """);
    int seconds = time / 1000;
    int minutes = seconds / 60;
    int hours = minutes / 60;
    seconds = seconds - minutes * 60;
    minutes = minutes - hours * 60;
    buf.append(hours);
    buf.append(":");
    buf.append(format(minutes));
    buf.append(":");
    buf.append(format(seconds));
    return buf.toString();
  }

  private static String format(int value)
  {
    String str = String.valueOf(value);
    str = (str.length() == 1 ? "0" + str : str);
    return str;
  }
}

The getTime() method cuts the time (provided in milliseconds) into separate chunks representing hours, minutes, and seconds. Because the default length of a StringBuffer object is 16 characters, there is no need for the object to expand itself.

If you were to use String objects in the preceding code in place of a StringBuffer, the application would need to create six string constants and five Strings for concatenations.

Note, however, the format() method in the preceding code. There is only one concatenation at most. In this case, it makes more sense to use two small Strings rather than one large StringBuffer.

Arrays Versus Vector and Hashtable

Arrays in Java are a special kind of object. They are created by using either the new operator, or in a combined declaration, creation, and initialization statement. An array represents a 32-bit address that points to a list of indexed values. Values may be of primitive types or classes. The biggest problem in using arrays is that you need to set the size of your array in advance. If you need to enlarge the array, you must create a larger one and copy the values from the old array into the new one (by calling the System.arraycopy() method).

A much easier approach is to use the Vector class. The Vector object is used to store an array of objects, and it can grow automatically as needed. The class also offers a wide range of methods for adding, inserting, finding, and deleting objects. All methods are synchronized to guard against one thread changing the data currently being used by another thread.

The Hashtable class is similar to the Vector class, allowing you to store an unlimited number of objects keyed to a particular value.

Unfortunately, the convenience and safety of a Vector or Hashtable comes at a price. A lot of overhead memory is used to create a Vector or Hashtable, and locking and releasing the Vector and Hashtables reduces your program's execution speed.

When it comes to your own games, try to use arrays whenever possible.

Power Consumption

The last important optimization technique has to do with the lifetime of a device's battery. Some phones are not very good with using power for graphics and program execution. Some phones might run out of battery power if the user plays a game for an hour or two.

Some of the biggest power-eating problems in games are

  • Whenever a game key is pressed, the display light turns on for a moment. Some devices have the ability to turn the light off manually, but most users don't bother to use this feature.

  • During the game execution, your game may produce sounds. Sounds may be turned off, but then your gameplay suffers.

  • Some devices support vibrations, and games may use them for special effects.

Pure MIDP devices don't support backlight, advanced sounds, or vibrations. However, some devices, such as the Siemens SL45i and the i-mode series 503 have an additional game API that enables those effects. As tempting as it may be to use some of these neat features, you should do so sparingly to avoid totally sapping battery power.

Summary

As you can see, Micro Java game development is a tricky business. However, by focusing carefully on your game's file size, execution speed, memory bounds, battery life, and networking protocol, you can achieve some remarkable things.

One thing J2ME programming teaches you is to be resourceful. In subsequent chapters, we will go into more detail about creating optimized routines that balance functionality with speed and size.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.152.136