Table of ContentsChapter 14.            The Device PortsBuild Systems

Nokia Customization

The Nokia UI provides a host of cool features that can really enhance the quality of any J2ME game. For example, using the FullCanvas class you'll be able to utilize much more of the screen for your game. Likewise, you can use the imaging tools to reduce the size of graphics and add vibration support for a nice death effect.

In the next few sections, you'll modify Star Assault to take advantage of these new features. After you see what changes you need to make, I'll show you how to use build scripts and pre-processing to maintain a single code base for all the device ports you'll carry out in the future.

FullCanvas

Nokia's FullCanvas class is more or less the same as the MIDP Canvas class, just without the title bar and the mandatory LCDUI command line. As you can see in Figure 14.1, you gain quite a bit of screen real estate as a result. Adding support for FullCanvas is therefore quite worthwhile.

Figure 14.1. Taking advantage of the Nokia UI's full screen canvas means you gain extra space for games (left) than the normal LCDUI canvas (right).

graphic/14fig01.gif


Switching over to use FullCanvas is pretty easy. There are only two changes required. First, you need to derive the GameScreen class from FullCanvas,rather than Canvas.So instead of:

public class GameScreen extends Canvas implements Runnable, CommandListener

You'll use:

public class GameScreen extends FullCanvas implements Runnable

Notice I've also dropped the CommandListener interface. FullCanvas doesn't allow you to add commands (there's nowhere to draw them), so there's no point in having this interface.

Because you no longer have support for commands, you need to remove the code you use in the constructor to add the Menu command and set the listener. Here's the code you used to have in the constructor, now commented out:

public GameScreen(StarAssault midlet)
{
    ...
    // Command code
    // menu = new Command("Menu", Command.SCREEN, 1);
    // addCommand(menu);
    // setCommandListener(this);

You also need to remove the commandAction handler method.

Because you no longer have any commands, you might wonder how the player will exit to the menu. To support this, Nokia added two extra keys (FullCanvas.SOFT_KEY1 and FullCanvas.SOFT_KEY2). These are the two keys you normally use to select commands on Nokia handsets; you can use them to bring up an in-game function or exit from a full-screen game. To support this, you simply check for these keys in the keyPressed method.

protected void keyPressed(int keyCode)
{
    ...
    if (keyCode == FullCanvas.KEY_SOFTKEY1 || keyCode == FullCanvas.KEY_SOFTKEY2)
    {
        pause();
        theMidlet.activateMenu();
    }
}

Because your game uses the getWidth and getHeight methods to dynamically set the size of the display, the game doesn't need any changes to adjust to the new screen size.

public GameScreen(StarAssault midlet)
{
    ...

    screenWidth = getWidth();
    screenHeight = getHeight();

That's it. Star Assault will now run in full-screen mode! Next you can look at adding Nokia vibration.

Vibration

Adding vibration to Star Assault is a good effect as well; however, you need to be careful not to overuse it. To implement this effect with the Nokia library, you simply call DeviceControl.startVibra with a limited time by adding a utility method to the Tools class.

public final static void vibrate(int strength, int time)
{
    if (StarAssault.getApp().isOptionVibrate())
    {
        DeviceControl.startVibra(strength, time);
    }
}

Using Reflection and Rotation

Probably the best of the Nokia UI features are in the imaging area, specifically the support for rotating and reflecting images. As you saw in Chapter 6, you can use the DirectGraphics class to draw an image reflected (either horizontally or vertically) or rotated by 90, 180, or 270 degrees.

For Star Assault, many of the graphics (especially the fighters and turrets) are simply reflections or rotations of other images. Take another look at the ship graphics in Figure 14.2 and you can see what I mean.

Figure 14.2. The original ship graphics

graphic/14fig02.gif


If you look down the first column of yellow fighters, you can see that as you move down, each image is simply a 90-degree reflection of the previous one. This applies as you move across the columns too. If you take advantage of the Nokia tools, you can dispense with any image that is a 90-degree reflection of any other, so you can achieve the same result using the reduced set shown in Figure 14.3.

Figure 14.3. Using reflection you need far fewer frames to represent all 16 directions.

graphic/14fig03.gif


This version is less than half the number of bytes of the original, so it's well worth the trouble. You can add some code to generate the rest of the images on the fly.

NOTE

Note

You'll notice something else different about the images you use for Nokia devices. That's right transparency! Nokia supports this by default in PNG files.

Using reflection and rotation to simulate the different directions a sprite can move is a common trick, so I have some reasonably sophisticated methods to handle most situations.

First, you wrap up the reflection and rotation capabilities into a simpler method. The logical home for this is in the ImageSet class.

public final static Image getReflectedImage(Image source, boolean flipHorizontal,
boolean flipVertical)
{    // Create an image to draw onto and return.
    Image result = DirectUtils.createImage(source.getWidth(), source.getHeight(),
0x00000000);
    // Grab a reference to the Nokia UI DirectGraphics context for the result
    // image.
    DirectGraphics rdg = DirectUtils.getDirectGraphics(result.getGraphics());
    // Select the reflection required and execute it.
    int flipType = 0;
    if (flipHorizontal) flipType |= DirectGraphics.FLIP_HORIZONTAL;
    if (flipVertical) flipType |= DirectGraphics.FLIP_VERTICAL;
    rdg.drawImage(source, 0, 0, Tools.GRAPHICS_TOP_LEFT, flipType);
    // Return the newly reflect image back to the requestor.
    return result;
}

You can use this method to simplify reflection of an image horizontally or vertically (or both). For example, if you want to create another version of the enemy drone (the floating mine), using horizontally-reflected graphics you could just do:

Image droneImage = ImageSet.loadClippedImage("/mine.png", 0, 0);
droneSet = new ImageSet(1);
Image[] droneImages = ImageSet.extractFrames(droneImage, 0, 0, 3, 2, 16, 16);

// replace each image with a horizontally-reflected version
for (int i=0; i < droneImages.length; i++)
    droneImages[i] = ImageSet.getReflectedImage(droneImages[i], true, false);

droneSet.addState(droneImages, 800);

Nothing too difficult about that, but consider doing this for the ship (with its 16 directions). No really, I mean itthink about it for a second. As you can see in the revised graphics for your ships, you need to reflect all the angles in the first quadrant to the other three. Doing this using the previously mentioned method can quickly turn into a confusing mess. How about you write some code to handle all this?

NOTE

Note

This method of reflection creates another copy of each image. While this is very fast, it also uses a lot of extra image memory. The built-in Nokia UI reflection code is actually so fast on most Nokia MIDs that it's possible to do this type of reflection in real-time, without having to create another copy of each reflection of the image. To do this I use an expanded version of the ImageSet class which adds an option for a particular state to be a reflection of another. You can see a complete example of this on the CD under the Chapter 14 source code directory.

Before you look at the code to do this, I'd better explain what I'm doing a little more. Depending on the game and the sprites you're working with, you need to do different forms of reflection. The number and type you do really come down to how many directions the sprite can face. A man walking in a 2D side-scrolling game only needs two directions, left and right. For this you only need one source image (and one reflection). For a monster in a top-down dungeon crawl, you would need as many as eight directions. For the more sophisticated hero graphics in a fantasy game (or in this case, the fighter in Star Assault), you use 16 directions. If you think back to your reflection tools, you can use copies of any image that is a 90-degree reflection of another. Figure 14.4 shows some of these typical cases; the colored fighters are the original source images.

Figure 14.4. Common reflection cases for sprite graphics

graphic/14fig04.gif


As you can see in Figure 14.4, if you have a sprite with two directions you can simply mirror the first direction to get the other one. For four directions or more, you mirror all the source images both vertically and horizontally. In essence, you're reflecting each source image on its opposing axis. Take a look at a method to automate this process for any number of directions.

public final static Image[] getAxisReflections(Image[] source)
{
    int n = source.length;
    if (n == 0) return null;

First, the method takes an array of source images, and then, based on the number, it returns the axis reflections of these. To start, you need to figure out how many images are in the resulting array. For cases other than one or two, this is the number of source images minus one, multiplied by four (the maximum number of reflections any image can have). You can check this by looking at the diagram and counting the number of resulting images for the one with three source directions. The number of images in the result is equal to three minus one, which is two; multiply that by four, and you get a total of eight. If you did this for four source images, the result would be 12; for five images (the number in Star Assault), the result would be 16.

int total = (n - 1) * 4;
// special case for simple reflections
if (n < 3) total = n * 2;

Once you have the number of result images, you simply create an array and then copy in the source images.

Image[] result = new Image[total];
// copy the original images to the result
for (int i = 0; i < n; i++)
    result[i] = source[i];

Next you take care of the two easy cases, mirroring horizontally and vertically. Because there has to be at least one image, you always mirror source image zero vertically (feel free to default this to horizontal if you want) and place the resulting image halfway into the result. If the number of source images is greater than one, you also mirror the horizontal image into the result in a similar fashion.

// mirror the vertical (0 to 180)
result[total / 2] = getReflectedImage(source[0], true, false);

// mirror the horizontal (90 to 270)
if (n > 1)
{
    // you can think of total/4 as 90 degrees and total/2 as 180
    // keep in mind we're starting at 0 for array access
    result[total / 2 + (total / 4)] = getReflectedImage(source[n - 1],
                                                false, true);
}

The remaining code handles cases in which you have more than two source images. For each of these images, you reflect them into the other three quadrants of the circle. There's no rocket science to this; you just loop through those images and use vertical or horizontal reflection (or both) to mirror the images into the appropriate position in the result.

// mirror everything between 0 and 90 to the three other quadrants
if (n > 2)
{
    // now this gets a little messy; we need to mirror everything
    // in between 0 and 90 to the other quadrants. Since N > 2 we know we
        // have at least 1 image we need to reflect. First let's figure out how
        // many there are in the first quadrant, minus the two axes we already
        // took care of.
        int f = (n - 2);

        // now we mirror these to their opposing sides for the other 3
        // quadrants
        for (int i = 1; i <= f; i++)
        {
            result[total / 2 - i] = getReflectedImage(source[i], true, false);
            result[total / 2 + i] = getReflectedImage(source[i], true, true);
            result[total - i] = getReflectedImage(source[i], false, true);
        }
    }
    return result;
}

Now take a look at this code in a little more detail.

To use this code with your new fighter graphics, you adjust the image loading to suit the smaller PNG file, and then use the extractFrames method to create the rest.

yellowDirImages = ImageSet.extractFrames(shipImage, 0, 0, 5, 1, 16, 16);
yellowDirImages = ImageSet.getAxisReflections(yellowDirImages);

You can then use the image array in the same way you would if you loaded it directly from the image file. And you can use this code to reflect any number of source images.

That's where I'll stop with Nokia customization (although there are still plenty of things you could add).

All these code changes have created a problem, though. Your game no longer works with any MID other than a Nokia! At this point you could simply create different versions of the class files, but this quickly becomes hard to manage. What you'll look at next is how to use a build system to manage multiple versions of the same class.

    Table of ContentsChapter 14.            The Device PortsBuild Systems