Storing and Retrieving Data
(Page 1 of 8 )
Storing bytes of data locally on a device equipped for the Mobile Internet Device Profile (MIDP) is easy. MIDP allows you to store arrays of bytes. But what if the data you need to store isn't in the form of bytes? And how can you make the data small enough so that storing it on a device with a relatively small amount of memory is not a problem? That's where this article comes in. It is excerpted from the book
J2ME Games with MIDP2, written by Carol Hamer (Apress, 2004; ISBN: 1590593820).
IT TURNS OUT THAT storing bytes of data locally on a device that’s equipped for the Mobile Internet Device Profile (MIDP) is easy. Therefore, this chapter starts with an extremely simple example: You’ll take the maze game example from Chapter 3 and store the user’s preferred size information. Then, each time the user restarts the game, the game will automatically create the maze with walls of the user’s chosen width rather than starting at the default width.
The hard part of data storage and retrieval is when you have more complicated data to store. MIDP allows you to store arrays of bytes only. But what if the data you want to store isn’t in the form of bytes? The simplest thing to do is to use the classes java.io.DataInputStream and java.io.DataOutputStream to convert other types of data to bytes. But since memory can be scarce on a small device, it’s a good idea to understand how you convert integers to bytes, and vice versa, so you can compact your data to store it more efficiently. Therefore, in this chapter, you’ll see a utility class that converts ints to bytes and back again, compacting the data appropriately if it falls within a certain size range. Then you’ll see a complete game example (using the utility class) in which the user can save a game that’s currently in play and start it again later from that point. This example illustrates how to store complex data in a real game situation.
Saving Simple Data The MIDP Record Management System (RMS) is simple. Its package javax.microedition.rms has only one class: RecordStore. A RecordStore is a collection of records, which are in fact just byte arrays. A RecordStore is identified by the property MIDlet-Vendor and the property MIDlet-Name in the jad file, as well as by the name given to the RecordStore by the MIDlet that created it. This means that within a MIDlet suite (a group of MIDlets in the same jar file), the MIDlets share the RecordStores they’ve created, but MIDlet suites don’t share RecordStores with other MIDlet suites (unless they’re explicitly given permission to do so; you’ll learn more about that in the sidebar “Using Secure Connections While Selling Your Game” in Chapter 7). A RecordStore is identified by the MIDlet-Vendor and the MIDlet-Name properties in addition to the store’s name, so you don’t have to start your RecordStore’s name with your own package name to keep it in a separate namespace from the RecordStores of other unrelated MIDlets. It’s a good thing, too, because the name of the RecordStore can be only 32 (case-sensitive) characters long, so you don’t want to waste too many of them.
To create a RecordStore, all you have to do is call the static method RecordStore.openRecordStore() with the name of the RecordStore you want to create and the value true as arguments. The second argument true answers the question of whether to create the RecordStore if it doesn’t already exist. Once you have a handle to a RecordStore (either by creating it or by opening an existing RecordStore), you can get or set the records. Note that a record isn’t a separate class; it’s merely a byte array. The RMS assigns each record an integer record ID that you use to get or replace the data array using getRecord() or setRecord(). The first record is assigned the ID of one, and the record IDs go up incrementally from there. If you don’t like hard-coding numerical constants into your code (a reasonable inhibition), you can call enumerateRecords to get a RecordEnumerator to help you. The RecordEnumerator won’t necessarily give you the records in the same order they’d appear in if you had gotten them by number using getRecord(). If you’d like to traverse the records in a particular order, you can create a RecordFilter and/or a RecordComparator, which allows you to define, respectively, which subset of the records will be returned and in what order to return them. Both RecordFilter and RecordComparator are interfaces you must implement yourself if you’d like to use them. These interfaces were obviously designed with address book–type applications in mind rather than games, but you may find a use for them.
In this first example, you’ll create the simplest possible RecordStore. It’ll contain only one record, and that record will contain only 1 byte. The example works as follows: You start with the maze game from Chapter 3. After the user selects the preferred width for the maze walls and presses Done, the game calls the new class (PrefsStorage) with the preferred size information, and the PrefsStorage class then saves that information in a RecordStore. The game also consults the PrefsStorage class when the user first opens the game to check for a stored size preference to use when building the maze. If no preferred size has been stored, the PrefsStorage returns the default value.
In addition to adding the PrefsStorage class listed next, you need to modify a few other classes a bit. In the class MazeCanvas, you need to replace the following line:
mySquareSize = 5;
with the following line:
mySquareSize = PrefsStorage.getSquareSize();
Next, in the class SelectScreen, you need to add the following line:
PrefsStorage.setSquareSize(myWidthGauge.getValue());
to the method commandAction, as follows:
public void commandAction(Command c, Displayable s) {
if(c == myExitCommand){
PrefsStorage.setSquareSize(myWidthGauge.getValue());
myCanvas.newMaze();
}
}
Aside from those changes, the code for this example is identical to the code of the maze example from Chapter 3.
Listing 5-1 shows the code for PrefsStorage.java.
Listing 5-1.PrefsStorage.java
package net.frog_parrot.maze;
import javax.microedition.rms.*;
/**
* This class helps to store and retrieve the data about
* the maze size preferences.
*
* This is a utility class that does not contain instance data,
* so to simplify access, all the methods are static.
*
* @author Carol Hamer
*/
public class PrefsStorage {
//------------------------------------------------------- // static fields
/**
* The name of the datastore.
*/
public static final String STORE = "SizePrefs";
//-------------------------------------------------------- // business methods
/**
* This gets the preferred square size from the stored data.
*/
static int getSquareSize() {
// if data retrieval fails, the default value is 5
int retVal = 5;
RecordStore store = null;
try {
// if the record store does not yet exist, the second
// arg "true" tells it to create.
store = RecordStore.openRecordStore(STORE, true);
int numRecords = store.getNumRecords();
if(numRecords > 0) {
// the first record has id number 1
// (In fact this program stores only one record)
byte[] rec = store.getRecord(1);
retVal = rec[0];
}
} catch(Exception e) {
// data storage is not critical for this game and you're
// not creating a log, so if data retrieval fails, you
// just skip it and move on.
} finally {
try {
store.closeRecordStore();
} catch(Exception e) {
// if the record store is open, this shouldn't throw.
}
}
return(retVal);
}
/**
* This saves the preferred square size.
*/
static void setSquareSize(int size) {
RecordStore store = null;
try {
// since you're storing the int as a single byte,
// it's important that its value be less than
// 128. In fact, in real life the value would never
// get anywhere near this high, but I'm adding this
// little size check as a last line of defense against
// errors:
if(size > 127) {
size = 127;
}
// if the record store doesn't yet exist, the second
// arg "true" tells it to create.
store = RecordStore.openRecordStore(STORE, true);
byte[] record = new byte[1];
record[0] = (new Integer(size)).byteValue();
int numRecords = store.getNumRecords();
if(numRecords > 0) {
store.setRecord(1, record, 0, 1);
} else {
store.addRecord(record, 0, 1);
}
} catch(Exception e) {
// data storage isn't critical for this game and you're
// not creating a log, so if data storage fails, you
// just skip it and move on.
} finally {
try {
store.closeRecordStore();
} catch(Exception e) {
// if the record store is open, this shouldn't throw.
}
}
}
}
I need to mention one last point before you’re done with this little example—namely, the technique for converting back and forth between int values and byte values. The value I’m saving is an int, yet I save it in the form of a byte with little regard for the fact that an int and a byte in Java aren’t the same thing at all. (In particular, an int occupies 4 bytes of memory!) Yet you’ll notice that in the method setSquareSize() I get a byte value for the argument size merely by calling byteValue(), and in the other direction (in the method getSquareSize()) I convert the byte rec[0] to an int without any sort of conversion operation at all. What’s going on here? The answer is, I know the argument size is between -128 and 127 in value, so I know it can be stored as a single byte without losing any data. This simple conversion between ints and bytes is useful, but only if you’re 100 percent certain your int isn’t going to fall outside the appropriate range. If you’d like to store your int value with more precision, you can use alternate conversion techniques, discussed in the next section.
Next: Serializing More Complex Data Using Streams >>
More Java Articles
More By Apress Publishing
|
This article is taken from chapter five of the book J2ME Games with MIDP2, written by Carol Hamer (Apress, 2004; ISBN: 1590593820). Check it out at your favorite bookstore. Buy this book now.
|
|