Table of ContentsFront End OverviewThe Menus

The Application Class

As you saw when you developed RoadRun, your game starts and ends its life through the MIDlet class. In Star Assault you'll follow the same path; however, with the more complicated front end you need to make things a little more flexible.

First, to accommodate your new splash screen, menu system, and game screen, you have to be able to display a wide variety of different MIDP screens. Doing this is pretty easy; you just use the Display class' setCurrent method. However, at times you'll need to know which screen is currently active, so you need to keep track of what's currently displayed as well.

Take a look at your new application class (including support for GameScreen).

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.rms.*;
import java.io.*;

/**
 * The application class for the game Star Assault.
 */

public class StarAssault extends MIDlet
{
    private static StarAssault theApp;
    private GameScreen gs;
    private boolean hasAGameRunning;
    private Displayable currentDisplay;
    public StarAssault()
    {
        theApp = this;

The code below refers to the Splash class; we'll get to this in the "Splash Screen" section later in the chapter.

currentDisplay = new Splash(this);

The new application class retains a reference to the GameScreen class to notify it of changing in the application state.

    // init the game screen
    gs = new GameScreen(this);
}
public static StarAssault getApp()
{
    return theApp;
}

When the application starts, you set the current display object (which will be the splash screen created in the constructor) to be the current displayable. If the game is being resumed (from a previously paused state), this code notifies the GameScreen it's time to get on with business.

public void startApp() throws MIDletStateChangeException
{
    activateDisplayable(currentDisplay);

    // if we are resuming the game then tell the game screen about it
    if (gs != null)
    {
        if (gs.getState() == GameScreen.PAUSED)
            gs.resume();
    }
}

Again, when the application is paused by the player for some reason (incoming phone call), this code lets the GameScreen know.

public void pauseApp()
{
    if (gs != null)
        gs.pause();
}

public void destroyApp(boolean unconditional) throws MIDletStateChangeException
{
}

public void close()
{
    try
    {
        destroyApp(true);
        notifyDestroyed();
    }
    catch (MIDletStateChangeException e)
    {
    }
}

public void createNewGame()
{
    gs.startNewGame();
    activateGameScreen();
}

public void loadGame()
{
    gs.loadGame();
    activateGameScreen();
}

The activateGameScreen method takes care of placing the GameScreen in a state ready to play. If no game is currently underway it also sets up everything by calling the createNewGame method.

public void activateGameScreen()
{
    if (gs == null || gs.getState() == GameScreen.GAME_DONE)
        createNewGame();

    hasAGameRunning = true;
    gs.resume();
    currentDisplay = gs;
    activateDisplayable(gs);
}

This method is called by the menu system to determine if a game is currently active, and thus the "Resume Game" menu item should be displayed.

public boolean hasAGameRunning()
{
    if (!hasAGameRunning) return false;

    if (gs.getState() != GameScreen.GAME_DONE)
        return true;
    else
        return false;
    }

    public static void activateDisplayable(Displayable s)
    {
        try
        {
            Display.getDisplay(getApp()).setCurrent(s);
        }
        catch (Exception e)
        {
            System.out.println("App exception: " + e);
            e.printStackTrace();
        }
    }
}

You'll notice added methods the menu system will use to load and save the current game as well as to handle pausing and resuming the game properly. You'll see those in action when you code up the menus.

Configuration Options

Like in any comprehensive game, you must let the player configure certain aspects of the application. Exactly what these options are depends on your game.

For Star Assault the first option is auto-fire. On some MIDs it's pretty difficult to hit a direction key and a fire key in an effective, coordinated way. This can be a result of input delays, key placement, or the lack of simultaneous key press support. When you turn on auto-fire, the ship will automatically fire its weapon as fast as possible.

NOTE

Note

If you want to give players the option of auto-fire, consider the impact this might have on your game design. Having this option on should not adversely affect the state of play. For example, you might not be able to constrain weapons fire (such as a limited number of bullets or energy) or link special functionality to the fire key (such as holding it down to fire bigger blasts).

The second option you'll add is the ability to disable vibration. Vibration often sounds like a good idea at the time (and it is a good effect), but it can be difficult to tell the real impact of using it on actual handsets. Vibration can cause severe application delays (sometimes resulting in the game freezing) and it can drain batteries, especially if overused. It can also be just plain annoying sometimes.

NOTE

Tip

Probably the best place to use vibration is when the player dies or the game is over. This isn't likely to be very often, and the player expects a delay (penalty) at this time anyway.

Another option (and this is more for demonstration purposes) is to let the player disable the background star field. Because this is an entirely optional component, players can elect to disable the field if it's causing confusion or slowing things down.

To store settings for these options, add the following fields to the StarAssault class:

private boolean optionAutoFire = false;
private boolean optionVibrate = true;
private boolean optionStarField = true;

The first three fields are boolean types representing whether an option is enabled or disabled. AutoFire defaults to off, while Vibrate and StarField default to on. Next, add access methods for the boolean fields.

public boolean isOptionAutoFire()
{
    return optionAutoFire;
}

public void setOptionAutoFire(boolean optionAutoFire)
{
    this.optionAutoFire = optionAutoFire;
}

public boolean isOptionStarField()
{
    return optionStarField;
}

public void setOptionStarField(boolean optionStarField)
{
    this.optionStarField = optionStarField;
}

public boolean isOptionVibrate()
{
    return optionVibrate;
}
public void setOptionVibrate(boolean optionVibrate)
{
    this.optionVibrate = optionVibrate;
}

To now use configuration options, add some code into the other game classes. For autofire you just change the Ship class init method to set firing to true if auto-fire is on.

public final void init(int typeArg, int x, int y)
{
    ...

    switch (getType())
    {
        case PLAYER_SHIP:

            ...

            firing = StarAssault.getApp().isOptionAutoFire();
            break;

To implement the optional star field you change the world render method to check before drawing.

public final void render(Graphics g)
{
    try
    {
        // draw the background stars
        if (StarAssault.getApp().isOptionStarField())
            Tools.drawStarField(g, viewX / 10, viewY / 10, viewWidth, viewHeight);
    ...

Configuring Keys

Another configuration option is the assignment of keys to actions in the game. You can let the player select which keypad numbers to use for left, right, and fire. Like the options you added previously, you start by adding fields to the StarAssault class.

private int leftKeyNum = 4;
private int rightKeyNum = 6;
private int fireKeyNum = 5;

To store the key settings you use an integer that corresponds to the actual keypad number you want to use for that function. Most MIDs use a phone-style keypad, hence the default numbers I used in this example.

Again, like for the other options, I've added access methods for the keys (you'll see the GameScreen class's setKeyBindings method later in this section).

public int getFireKeyNum()
{
    return fireKeyNum;
}

public void setFireKeyNum(int fireKeyNum)
{
    this.fireKeyNum = fireKeyNum;
    if (gs != null) gs.setKeyBindings();
}

public int getLeftKeyNum()
{
    return leftKeyNum;
}

public void setLeftKeyNum(int leftKeyNum)
{
    this.leftKeyNum = leftKeyNum;
    if (gs != null) gs.setKeyBindings();
}

public int getRightKeyNum()
{
    return rightKeyNum;
}

public void setRightKeyNum(int rightKeyNum)
{
    this.rightKeyNum = rightKeyNum;
    if (gs != null) gs.setKeyBindings();
}

Setting these values isn't much good if you don't actually use them. Because your input handling code uses Canvas key codes rather than integers, you need to add some support to map between these two types.

public static final int getKeyCodeFromNum(int n)
{
    switch (n)
    {
        case 0:
            return Canvas.KEY_NUM0;
        case 1:
            return Canvas.KEY_NUM1;
        case 2:
            return Canvas.KEY_NUM2;
        case 3:
            return Canvas.KEY_NUM3;
        case 4:
            return Canvas.KEY_NUM4;
        case 5:
            return Canvas.KEY_NUM5;
        case 6:
            return Canvas.KEY_NUM6;
        case 7:
            return Canvas.KEY_NUM7;
        case 8:
            return Canvas.KEY_NUM8;
        case 9:
            return Canvas.KEY_NUM9;
    }
    return Canvas.KEY_NUM0;
}

Now you're ready to add code to the GameScreen class to support configurable keys. For speed, cache the key codes.

public class GameScreen extends Canvas implements Runnable, CommandListener
{
    ...

    private int leftKeyCode;
    private int rightKeyCode;
    private int fireKeyCode;
    public GameScreen(StarAssault midlet)
    {
        ...

        setKeyBindings();
    }
    public void setKeyBindings()
    {
        leftKeyCode = StarAssault.getKeyCodeFromNum(theMidlet.getLeftKeyNum());
        rightKeyCode = StarAssault.getKeyCodeFromNum(theMidlet.getRightKeyNum());
        fireKeyCode = StarAssault.getKeyCodeFromNum(theMidlet.getFireKeyNum());
    }

Because you're caching these values, you call the setKeyBindings method whenever the StarAssault class method changes the keys.

To make this all work, you simply adjust the input handling methods keyPressed and keyReleased to use these new values.

protected void keyPressed(int keyCode)
{
    ...
    // If the autofire is on you ignore the up key.
    if (!StarAssault.getApp().isOptionAutoFire())
    {
        if (action == GAME_A || keyCode == fireKeyCode || action == UP)
            playerShip.setFiring(true);
    }
}

protected void keyReleased(int keyCode)
{

    ...

    // If the autofire is on you ignore the up key.
    if (!StarAssault.getApp().isOptionAutoFire())
    {
        if (action == GAME_A || keyCode == fireKeyCode || action == UP)
            playerShip.setFiring(false);
    }
}

Saving and Loading Settings

Now that you've given the player the option to change the configuration of the game, you need to save his preferences.

The code to save the settings is a lot like the code to save the world. You simply create a record store named "Settings" and write out the data. The following is a method to save the settings in the application class. Note that this is an entirely separate operation to saving and loading the game (in the World class). Saving and loading settings is done whenever they are changed, not when playing the game.

public boolean saveSettings()
{
    RecordStore settings = null;
    try
    {
        try
        {
            // Remove any previous settings save record.
            RecordStore.deleteRecordStore("Settings");
        }
        catch (RecordStoreNotFoundException rse)
        {
            // No need to worry if it wasn't found as it could be the
            // first time they are saving the settings.
        }

        // Construct a new record store for the settings.
        settings = RecordStore.openRecordStore("Settings", true);

        // Output all the settings using a data stream.
        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(byteOutputStream);
        dataOutputStream.writeBoolean(optionAutoFire);
        dataOutputStream.writeBoolean(optionVibrate);
        dataOutputStream.writeBoolean(optionStarField);
        dataOutputStream.writeInt(leftKeyNum);
        dataOutputStream.writeInt(rightKeyNum);
        dataOutputStream.writeInt(fireKeyNum);

        dataOutputStream.flush();

        // delete the old one and add the new record
        byte[] recordOut = byteOutputStream.toByteArray();
        // Write the new record to the RMS.
        try
            {
            settings.setRecord(1, recordOut, 0, recordOut.length);
        }
        catch (InvalidRecordIDException ir)
        {
            settings.addRecord(recordOut, 0, recordOut.length);
        }
        dataOutputStream.close();
        byteOutputStream.close();
        return true;
    }
    catch (IOException io)
    {
        System.out.println("IOException: " + io);
        return false;
    }
    catch (RecordStoreException rse)
    {
        System.out.println("RSException: " + rse);
        return false;
    }
    // In case there was any form of error you need to make sure the
    // record store is properly closed.
    finally
    {
        try
        {
        if (settings != null) settings.closeRecordStore();
        }
        catch (RecordStoreNotOpenException e)
        {
        }
        catch (RecordStoreException e)
        {
        }
    }
}

You'll see how and when to call the saveSettings method when you create the menus in the next section. Take a look at how to load these settings first.

public boolean loadSettings()
{
    RecordStore settings = null;
    try
    {
        settings = RecordStore.openRecordStore("Settings", true);

        ByteArrayInputStream byteInputStream = new
ByteArrayInputStream(settings.getRecord(1));
            DataInputStream dataInputStream = new DataInputStream(byteInputStream);

        setOptionAutoFire(dataInputStream.readBoolean());
        setOptionVibrate(dataInputStream.readBoolean());
        setOptionStarField(dataInputStream.readBoolean());
        setLeftKeyNum(dataInputStream.readInt());
        setRightKeyNum(dataInputStream.readInt());
        setFireKeyNum(dataInputStream.readInt());

        dataInputStream.close();
        byteInputStream.close();

        settings.closeRecordStore();
        return true;
    }

    catch (IOException io)
    {
        System.out.println("IOException: " + io);
        return false;
    }
    catch (RecordStoreException rse)
    {
        System.out.println("RSException: " + rse);
        return false;
    }

    // In case there was any form of error you need to make sure the
    // record store is properly closed.
    finally
    {
        try
        {
            if (settings != null) settings.closeRecordStore();
        }

        catch (RecordStoreNotOpenException e)
        {
        }

        catch (RecordStoreException e)
        {
        }
    }
}

To load the settings, you simply call this method from the MIDlet constructor.

public class StarAssault extends MIDlet
{

    public StarAssault()
    {
        ...
        loadSettings();
    }
}

    Table of ContentsFront End OverviewThe Menus