Dungeon game
/* Title: J2ME Games With MIDP2 Authors: Carol Hamer Publisher: Apress ISBN: 1590593820 */ import java.io.*; import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; import javax.microedition.midlet.*; import javax.microedition.rms.*; /** * This is the main class of the dungeon game. * * @author Carol Hamer */ public class Dungeon extends MIDlet implements CommandListener { //----------------------------------------------------- // game object fields /** * The canvas that the dungeon is drawn on. */ private DungeonCanvas myCanvas; /** * the thread that advances the game clock. */ private GameThread myGameThread; //----------------------------------------------------- // command fields /** * The button to exit the game. */ private Command myExitCommand = new Command("Exit", Command.EXIT, 99); /** * The command to save the game in progress. */ private Command mySaveCommand = new Command("Save Game", Command.SCREEN, 2); /** * The command to restore a previously saved game. */ private Command myRestoreCommand = new Command("Restore Game", Command.SCREEN, 2); /** * the command to start moving when the game is paused. */ private Command myGoCommand = new Command("Go", Command.SCREEN, 1); /** * the command to pause the game. */ private Command myPauseCommand = new Command("Pause", Command.SCREEN, 1); /** * the command to start a new game. */ private Command myNewCommand = new Command("Next Board", Command.SCREEN, 1); //----------------------------------------------------- // initialization and game state changes /** * Initialize the canvas and the commands. */ public Dungeon() { try { // create the canvas and set up the commands: myCanvas = new DungeonCanvas(this); myCanvas.addCommand(myExitCommand); myCanvas.addCommand(mySaveCommand); myCanvas.addCommand(myRestoreCommand); myCanvas.addCommand(myPauseCommand); myCanvas.setCommandListener(this); } catch (Exception e) { // if there's an error during creation, display it as an alert. errorMsg(e); } } /** * Switch the command to the play again command. (removing other commands * that are no longer relevant) */ void setNewCommand() { myCanvas.removeCommand(myPauseCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myNewCommand); } /** * Switch the command to the go command. (removing other commands that are * no longer relevant) */ void setGoCommand() { myCanvas.removeCommand(myPauseCommand); myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myGoCommand); } /** * Switch the command to the pause command. (removing other commands that * are no longer relevant) */ void setPauseCommand() { myCanvas.removeCommand(myNewCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); } //---------------------------------------------------------------- // implementation of MIDlet // these methods may be called by the application management // software at any time, so we always check fields for null // before calling methods on them. /** * Start the application. */ public void startApp() throws MIDletStateChangeException { if (myCanvas != null) { if (myGameThread == null) { // create the thread and start the game: myGameThread = new GameThread(myCanvas); myCanvas.start(); myGameThread.start(); } else { // in case this gets called again after // the application has been started once: myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } } } /** * Stop the threads and throw out the garbage. */ public void destroyApp(boolean unconditional) throws MIDletStateChangeException { myCanvas = null; if (myGameThread != null) { myGameThread.requestStop(); } myGameThread = null; System.gc(); } /** * Pause the game. */ public void pauseApp() { if (myCanvas != null) { setGoCommand(); } if (myGameThread != null) { myGameThread.pause(); } } //---------------------------------------------------------------- // implementation of CommandListener /* * Respond to a command issued on the Canvas. (reset, exit, or change size * prefs). */ public void commandAction(Command c, Displayable s) { try { if (c == myGoCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } else if (c == myPauseCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myPauseCommand); myCanvas.addCommand(myGoCommand); myGameThread.pause(); } else if (c == myNewCommand) { myCanvas.setNeedsRepaint(); // go to the next board and restart the game myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myPauseCommand); myCanvas.reset(); myGameThread.resumeGame(); /*} else if (c == Alert.DISMISS_COMMAND) { // if there was a serious enough error to // cause an alert, then we end the game // when the user is done reading the alert: // (Alert.DISMISS_COMMAND is the default // command that is placed on an Alert // whose timeout is FOREVER) destroyApp(false); notifyDestroyed();*/ } else if (c == mySaveCommand) { myCanvas.setNeedsRepaint(); myCanvas.saveGame(); } else if (c == myRestoreCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myNewCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.revertToSaved(); } else if (c == myExitCommand) { destroyApp(false); notifyDestroyed(); } } catch (Exception e) { errorMsg(e); } } //------------------------------------------------------- // error methods /** * Converts an exception to a message and displays the message.. */ void errorMsg(Exception e) { if (e.getMessage() == null) { errorMsg(e.getClass().getName()); } else { errorMsg(e.getClass().getName() + ":" + e.getMessage()); } } /** * Displays an error message alert if something goes wrong. */ void errorMsg(String msg) { Alert errorAlert = new Alert("error", msg, null, AlertType.ERROR); errorAlert.setCommandListener(this); errorAlert.setTimeout(Alert.FOREVER); Display.getDisplay(this).setCurrent(errorAlert); } } /** * This class represents doors and keys. * * @author Carol Hamer */ class DoorKey extends Sprite { //--------------------------------------------------------- // fields /** * The image file shared by all doors and keys. */ public static Image myImage; /** * A code int that indicates the door or key's color. */ private int myColor; //--------------------------------------------------------- // get/set data /** * @return the door or key's color. */ public int getColor() { return (myColor); } //--------------------------------------------------------- // constructor and initializer static { try { myImage = Image.createImage("/images/keys.png"); } catch (Exception e) { throw (new RuntimeException( "DoorKey.<init>-->failed to load image, caught " + e.getClass() + ": " + e.getMessage())); } } /** * Standard constructor sets the image to the correct frame (according to * whether this is a door or a key and what color it should be) and then * puts it in the correct location. */ public DoorKey(int color, boolean isKey, int[] gridCoordinates) { super(myImage, DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_WIDTH); myColor = color; int imageIndex = color * 2; if (isKey) { imageIndex++; } setFrame(imageIndex); setPosition(gridCoordinates[0] * DungeonManager.SQUARE_WIDTH, gridCoordinates[1] * DungeonManager.SQUARE_WIDTH); } } /** * This class is a set of simple utility functions that can be used to convert * standard data types to bytes and back again. It is used especially for data * storage, but also for sending and receiving data. * * @author Carol Hamer */ class DataConverter { //-------------------------------------------------------- // utilities to encode small, compactly-stored small ints. /** * Encodes a coordinate pair into a byte. * * @param coordPair * a pair of integers to be compacted into a single byte for * storage. WARNING: each of the two values MUST BE between 0 and * 15 (inclusive). This method does not verify the length of the * array (which must be 2!) nor does it verify that the ints are * of the right size. */ public static byte encodeCoords(int[] coordPair) { // get the byte value of the first coordinate: byte retVal = (new Integer(coordPair[0])).byteValue(); // move the first coordinate's value up to the top // half of the storage byte: retVal = (new Integer(retVal << 4)).byteValue(); // store the second coordinate in the lower half // of the byte: retVal += (new Integer(coordPair[1])).byteValue(); return (retVal); } /** * Encodes eight ints into a byte. This could be easily modified to encode * eight booleans. * * @param eight * an array of at least eight ints. WARNING: all values must be 0 * or 1! This method does not verify that the values are in the * correct range nor does it verify that the array is long * enough. * @param offset * the index in the array eight to start reading data from. * (should usually be 0) */ public static byte encode8(int[] eight, int offset) { // get the byte value of the first int: byte retVal = (new Integer(eight[offset])).byteValue(); // progressively move the data up one bit in the // storage byte and then record the next int in // the lowest spot in the storage byte: for (int i = offset + 1; i < 8 + offset; i++) { retVal = (new Integer(retVal << 1)).byteValue(); retVal += (new Integer(eight[i])).byteValue(); } return (retVal); } //-------------------------------------------------------- // utilities to decode small, compactly-stored small ints. /** * Turns a byte into a pair of coordinates. */ public static int[] decodeCoords(byte coordByte) { int[] retArray = new int[2]; // we perform a bitwise and with the value 15 // in order to just get the bits of the lower // half of the byte: retArray[1] = coordByte & 15; // To get the bits of the upper half of the // byte, we perform a shift to move them down: retArray[0] = coordByte >> 4; // bytes in Java are generally assumed to be // signed, but in this coding algorithm we // would like to treat them as unsigned: if (retArray[0] < 0) { retArray[0] += 16; } return (retArray); } /** * Turns a byte into eight ints. */ public static int[] decode8(byte data) { int[] retArray = new int[8]; // The flag allows us to look at each bit individually // to determine if it is 1 or 0. The number 128 // corresponds to the highest bit of a byte, so we // start with that one. int flag = 128; // We use a loop that checks // the data bit by bit by performing a bitwise // and (&) between the data byte and a flag: for (int i = 0; i < 8; i++) { if ((flag & data) != 0) { retArray[i] = 1; } else { retArray[i] = 0; } // move the flag down one bit so that we can // check the next bit of data on the next pass // through the loop: flag = flag >> 1; } return (retArray); } //-------------------------------------------------------- // standard integer interpretation /** * Uses an input stream to convert an array of bytes to an int. */ public static int parseInt(byte[] data) throws IOException { DataInputStream stream = new DataInputStream(new ByteArrayInputStream( data)); int retVal = stream.readInt(); stream.close(); return (retVal); } /** * Uses an output stream to convert an int to four bytes. */ public static byte[] intToFourBytes(int i) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(4); DataOutputStream dos = new DataOutputStream(baos); dos.writeInt(i); baos.close(); dos.close(); byte[] retArray = baos.toByteArray(); return (retArray); } //-------------------------------------------------------- // integer interpretation illustrated /** * Java appears to treat a byte as being signed when returning it as an * int--this function converts from the signed value to the corresponding * unsigned value. This method is used by nostreamParseInt. */ public static int unsign(int signed) { int retVal = signed; if (retVal < 0) { retVal += 256; } return (retVal); } /** * Takes an array of bytes and returns an int. This version will return the * same value as the method parseInt above. This version is included in * order to illustrate how Java encodes int values in terms of bytes. * * @param data * an array of 1, 2, or 4 bytes. */ public static int nostreamParseInt(byte[] data) { // byte 0 is the high byte which is assumed // to be signed. As we add the lower bytes // one by one, we unsign them because because // a single byte alone is interpreted as signed, // but in an int only the top byte should be signed. // (note that the high byte is the first one in the array) int retVal = data[0]; for (int i = 1; i < data.length; i++) { retVal = retVal << 8; retVal += unsign(data[i]); } return (retVal); } /** * Takes an arbitrary int and returns an array of four bytes. This version * will return the same byte array as the method intToFourBytes above. This * version is included in order to illustrate how Java encodes int values in * terms of bytes. */ public static byte[] nostreamIntToFourBytes(int i) { byte[] fourBytes = new byte[4]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get all four bytes by taking the lowest // byte four times and moving the whole int // down by one byte between each one. // (note that the high byte is the first one in the array) fourBytes[3] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[2] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[0] = (new Integer(i)).byteValue(); return (fourBytes); } /** * Takes an int between -32768 and 32767 and returns an array of two bytes. * This does not verify that the argument is of the right size. If the * absolute value of i is too high, it will not be encoded correctly. */ public static byte[] nostreamIntToTwoBytes(int i) { byte[] twoBytes = new byte[2]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get the lower two bytes by taking the lowest // byte twice and moving the whole int // down by one byte between each one. twoBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; twoBytes[0] = (new Integer(i)).byteValue(); return (twoBytes); } } /** * This class contains the data for the map of the dungeon.. * * @author Carol Hamer */ class BoardDecoder { //-------------------------------------------------------- // fields /** * The coordinates of where the player starts on the map in terms of the * array indices. */ private int[] myPlayerSquare; /** * The coordinates of the goal (crown). */ private int[] myGoalSquare; /** * The coordinates of the doors. the there should be two in a row of each * color, following the same sequence as the keys. */ private int[][] myDoors; /** * The coordinates of the Keys. the there should be of each color, following * the same sequence as the doors. */ private int[][] myKeys; /** * The coordinates of the stone walls of the maze, encoded bit by bit. */ private TiledLayer myLayer; /** * The data in bytes that gives the various boards. This was created using * EncodingUtils... This is a two-dimensional array: Each of the four main * sections corresponds to one of the four possible boards. */ private static byte[][] myData = { { 0, 0, -108, -100, -24, 65, 21, 58, 53, -54, -116, -58, -56, -84, 115, -118, -1, -1, -128, 1, -103, -15, -128, 25, -97, -127, -128, 79, -14, 1, -126, 121, -122, 1, -113, -49, -116, 1, -100, -3, -124, 5, -25, -27, -128, 1, -1, -1 }, { 0, 1, 122, 90, -62, 34, -43, 72, -59, -29, 56, -55, 98, 126, -79, 61, -1, -1, -125, 1, -128, 17, -26, 29, -31, 57, -72, 1, -128, -51, -100, 65, -124, 57, -2, 1, -126, 13, -113, 1, -97, 25, -127, -99, -8, 1, -1, -1 }, { 0, 2, 108, -24, 18, -26, 102, 30, -58, 46, -28, -88, 34, -98, 97, -41, -1, -1, -96, 1, -126, 57, -9, 97, -127, 69, -119, 73, -127, 1, -109, 59, -126, 1, -26, 103, -127, 65, -103, 115, -127, 65, -25, 73, -128, 1, -1, -1 }, { 0, 3, -114, 18, -34, 27, -39, -60, -76, -50, 118, 90, 82, -88, 34, -74, -1, -1, -66, 1, -128, 121, -26, 125, -128, -123, -103, 29, -112, 1, -109, 49, -112, 1, -116, -31, -128, 5, -122, 5, -32, 13, -127, -51, -125, 1, -1, -1 }, }; //-------------------------------------------------------- // initialization /** * Constructor fills data fields by interpreting the data bytes. */ public BoardDecoder(int boardNum) throws Exception { // we start by selecting the two dimensional // array corresponding to the desired board: byte[] data = myData[boardNum]; // The first two bytes give the version number and // the board number, but we ignore them because // they are assumed to be correct. // The third byte of the first array is the first one // we read: it gives the player's starting coordinates: myPlayerSquare = DataConverter.decodeCoords(data[2]); // the next byte gives the coordinates of the crown: myGoalSquare = DataConverter.decodeCoords(data[3]); // the next four bytes give the coordinates of the keys: myKeys = new int[4][]; for (int i = 0; i < myKeys.length; i++) { myKeys[i] = DataConverter.decodeCoords(data[i + 4]); } // the next eight bytes give the coordinates of the doors: myDoors = new int[8][]; for (int i = 0; i < myDoors.length; i++) { myDoors[i] = DataConverter.decodeCoords(data[i + 8]); } // now we create the TiledLayer object that is the // background dungeon map: myLayer = new TiledLayer(16, 16, Image.createImage("/images/stone.png"), DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_WIDTH); // now we call an internal utility that reads the array // of data that gives the positions of the blocks in the // walls of this dungeon: decodeDungeon(data, myLayer, 16); } //-------------------------------------------------------- // get/set data /** * @return the number of boards currently stored in this class. */ public static int getNumBoards() { return (myData.length); } /** * get the coordinates of where the player starts on the map in terms of the * array indices. */ public int[] getPlayerSquare() { return (myPlayerSquare); } /** * get the coordinates of the goal crown in terms of the array indices. */ public int[] getGoalSquare() { return (myGoalSquare); } /** * get the tiled layer that gives the map of the dungeon. */ public TiledLayer getLayer() { return (myLayer); } /** * Creates the array of door sprites. (call this only once to avoid creating * redundant sprites). */ DoorKey[] createDoors() { DoorKey[] retArray = new DoorKey[8]; for (int i = 0; i < 4; i++) { retArray[2 * i] = new DoorKey(i, false, myDoors[2 * i]); retArray[2 * i + 1] = new DoorKey(i, false, myDoors[2 * i + 1]); } return (retArray); } /** * Creates the array of key sprites. (call this only once to avoid creating * redundant sprites.) */ DoorKey[] createKeys() { DoorKey[] retArray = new DoorKey[4]; for (int i = 0; i < 4; i++) { retArray[i] = new DoorKey(i, true, myKeys[i]); } return (retArray); } //-------------------------------------------------------- // decoding utilities /** * Takes a dungeon given as a byte array and uses it to set the tiles of a * tiled layer. * * The TiledLayer in this case is a 16 x 16 grid in which each square can be * either blank (value of 0) or can be filled with a stone block (value of * 1). Therefore each square requires only one bit of information. Each byte * of data in the array called "data" records the frame indices of eight * squares in the grid. */ private static void decodeDungeon(byte[] data, TiledLayer dungeon, int offset) throws Exception { if (data.length + offset < 32) { throw (new Exception( "BoardDecoder.decodeDungeon-->not enough data!!!")); } // a frame index of zero indicates a blank square // (this is always true in a TiledLayer). // This TiledLayer has only one possible (non-blank) // frame, so a frame index of 1 indicates a stone block int frame = 0; // Each of the 32 bytes in the data array records // the frame indices of eight block in the 16 x 16 // grid. Two bytes give one row of the dungeon, // so we have the array index go from zero to 16 // to set the frame indices fro each of the 16 rows. for (int i = 0; i < 16; i++) { // The flag allows us to look at each bit individually // to determine if it is 1 or 0. The number 128 // corresponds to the highest bit of a byte, so we // start with that one. int flag = 128; // Here we check two bytes at the same time // (the two bytes together correspond to one row // of the dungeon). We use a loop that checks // the bytes bit by bit by performing a bitwise // and (&) between the data byte and a flag: for (int j = 0; j < 8; j++) { if ((data[offset + 2 * i] & flag) != 0) { frame = 1; } else { frame = 0; } dungeon.setCell(j, i, frame); if ((data[offset + 2 * i + 1] & flag) != 0) { frame = 1; } else { frame = 0; } dungeon.setCell(j + 8, i, frame); // move the flag down one bit so that we can // check the next bit of data on the next pass // through the loop: flag = flag >> 1; } } } } /** * This class contains the loop that keeps the game running. * * @author Carol Hamer */ class GameThread extends Thread { //--------------------------------------------------------- // fields /** * Whether or not the main thread would like this thread to pause. */ private boolean myShouldPause; /** * Whether or not the main thread would like this thread to stop. */ private static boolean myShouldStop; /** * A handle back to the graphical components. */ private DungeonCanvas myDungeonCanvas; /** * The System.time of the last screen refresh, used to regulate refresh * speed. */ private long myLastRefreshTime; //---------------------------------------------------------- // initialization /** * standard constructor. */ GameThread(DungeonCanvas canvas) { myDungeonCanvas = canvas; } //---------------------------------------------------------- // utilities /** * Get the amount of time to wait between screen refreshes. Normally we wait * only a single millisecond just to give the main thread a chance to update * the keystroke info, but this method ensures that the game will not * attempt to show too many frames per second. */ private long getWaitTime() { long retVal = 1; long difference = System.currentTimeMillis() - myLastRefreshTime; if (difference < 75) { retVal = 75 - difference; } return (retVal); } //---------------------------------------------------------- // actions /** * pause the game. */ void pause() { myShouldPause = true; } /** * restart the game after a pause. */ synchronized void resumeGame() { myShouldPause = false; notify(); } /** * stops the game. */ synchronized void requestStop() { myShouldStop = true; this.notify(); } /** * start the game.. */ public void run() { // flush any keystrokes that occurred before the // game started: myDungeonCanvas.flushKeys(); myShouldStop = false; myShouldPause = false; while (true) { myLastRefreshTime = System.currentTimeMillis(); if (myShouldStop) { break; } myDungeonCanvas.checkKeys(); myDungeonCanvas.updateScreen(); // we do a very short pause to allow the other thread // to update the information about which keys are pressed: synchronized (this) { try { wait(getWaitTime()); } catch (Exception e) { } } if (myShouldPause) { synchronized (this) { try { wait(); } catch (Exception e) { } } } } } } /** * This class contains the data for a game currently in progress. used to store * a game and to resume a stored game. * * @author Carol Hamer */ class GameInfo { //-------------------------------------------------------- // fields /** * The name of the datastore. */ public static final String STORE = "GameInfo"; /** * This is set to true if an attempt is made to read a game when no game has * been saved. */ private boolean myNoDataSaved; /** * The number that indicates which board the player is currently on. */ private int myBoardNum; /** * The amount of time that has passed. */ private int myTime; /** * The coordinates of where the player is on the board. coordinate values * must be between 0 and 15. */ private int[] myPlayerSquare; /** * The coordinates of where the keys are currently found. MUST BE four sets * of two integer coordinates. coordinate values must be between 0 and 15. */ private int[][] myKeyCoords; /** * The list of which doors are currently open. 0 = open 1 = closed WARNING: * this array MUST have length 8. */ private int[] myDoorsOpen; /** * The number of the key that is currently being held by the player. if no * key is held, then the value is -1. */ private int myHeldKey; //-------------------------------------------------------- // data gets/sets /** * @return true if no saved game records were found. */ boolean getIsEmpty() { return (myNoDataSaved); } /** * @return The number that indicates which board the player is currently on. */ int getBoardNum() { return (myBoardNum); } /** * @return The number of the key that is currently being held by the player. * if no key is held, then the value is -1. */ int getHeldKey() { return (myHeldKey); } /** * @return The amount of time that has passed. */ int getTime() { return (myTime); } /** * @return The coordinates of where the player is on the board. coordinate * values must be between 0 and 15. */ int[] getPlayerSquare() { return (myPlayerSquare); } /** * @return The coordinates of where the keys are currently found. MUST BE * four sets of two integer coordinates. coordinate values must be * between 0 and 15. */ int[][] getKeyCoords() { return (myKeyCoords); } /** * @return The list of which doors are currently open. 0 = open 1 = closed * WARNING: this array MUST have length 8. */ int[] getDoorsOpen() { return (myDoorsOpen); } //-------------------------------------------------------- // constructors /** * This constructor records the game info of a game currently in progress. */ GameInfo(int boardNum, int time, int[] playerSquare, int[][] keyCoords, int[] doorsOpen, int heldKey) throws Exception { myBoardNum = boardNum; myTime = time; myPlayerSquare = playerSquare; myKeyCoords = keyCoords; myDoorsOpen = doorsOpen; myHeldKey = heldKey; encodeInfo(); } /** * This constructor reads the game configuration from memory. This is used * to reconstruct a saved game. */ GameInfo() { RecordStore store = null; try { // if the record store does not yet exist, don't // create it store = RecordStore.openRecordStore(STORE, false); if ((store != null) && (store.getNumRecords() > 0)) { // the first record has id number 1 // it should also be the only record since this // particular game stores only one game. byte[] data = store.getRecord(1); myBoardNum = data[0]; myPlayerSquare = DataConverter.decodeCoords(data[1]); myKeyCoords = new int[4][]; myKeyCoords[0] = DataConverter.decodeCoords(data[2]); myKeyCoords[1] = DataConverter.decodeCoords(data[3]); myKeyCoords[2] = DataConverter.decodeCoords(data[4]); myKeyCoords[3] = DataConverter.decodeCoords(data[5]); myDoorsOpen = DataConverter.decode8(data[6]); myHeldKey = data[7]; byte[] fourBytes = new byte[4]; System.arraycopy(data, 8, fourBytes, 0, 4); myTime = DataConverter.parseInt(fourBytes); } else { myNoDataSaved = true; } } catch (Exception e) { // this throws when the record store doesn't exist. // for that or any error, we assume no data is saved: myNoDataSaved = true; } finally { try { if (store != null) { store.closeRecordStore(); } } catch (Exception e) { // if the record store is open this shouldn't throw. } } } //-------------------------------------------------------- // encoding method /** * Turn the data into a byte array and save it. */ private void encodeInfo() throws Exception { RecordStore store = null; try { byte[] data = new byte[12]; data[0] = (new Integer(myBoardNum)).byteValue(); data[1] = DataConverter.encodeCoords(myPlayerSquare); data[2] = DataConverter.encodeCoords(myKeyCoords[0]); data[3] = DataConverter.encodeCoords(myKeyCoords[1]); data[4] = DataConverter.encodeCoords(myKeyCoords[2]); data[5] = DataConverter.encodeCoords(myKeyCoords[3]); data[6] = DataConverter.encode8(myDoorsOpen, 0); data[7] = (new Integer(myHeldKey)).byteValue(); byte[] timeBytes = DataConverter.intToFourBytes(myTime); System.arraycopy(timeBytes, 0, data, 8, 4); // 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) { store.setRecord(1, data, 0, data.length); } else { store.addRecord(data, 0, data.length); } } catch (Exception e) { throw (e); } finally { try { if (store != null) { store.closeRecordStore(); } } catch (Exception e) { // if the record store is open this shouldn't throw. } } } } /** * This class contains the data for the map of the dungeon. This is a utility * class that allows a developer to write the data for a board in a simple * format, then this class encodes the data in a format that the game can use. * * note that the data that this class encodes is hard-coded. that is because * this class is intended to be used only a few times to encode the data. Once * the board data has been encoded, it never needs to be encoded again. The * encoding methods used in this class could be generalized to be used to create * a board editor which would allow a user to easily create new boards, but that * is an exercise for another day... * * @author Carol Hamer */ class EncodingUtils { //-------------------------------------------------------- // fields /** * data for which squares are filled and which are blank. 0 = empty 1 = * filled */ private int[][] mySquares = { { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 }, { 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1 }, { 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1 }, { 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1 }, { 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1 }, { 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1 }, { 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1 }, { 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1 }, { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, }; /** * The coordinates of where the player starts on the map in terms of the * array indices. */ private int[] myPlayerSquare = { 7, 10 }; /** * The coordinates of the goal (crown). */ private int[] myGoalSquare = { 5, 10 }; //-------------------------------------------------------- // get/set data /** * Creates the array of door sprites. (call this only once to avoid creating * redundant sprites). */ int[][] getDoorCoords() { int[][] retArray = new int[8][]; for (int i = 0; i < retArray.length; i++) { retArray[i] = new int[2]; } // red retArray[0][0] = 12; retArray[0][1] = 5; retArray[1][0] = 14; retArray[1][1] = 3; // green retArray[2][0] = 3; retArray[2][1] = 8; retArray[3][0] = 12; retArray[3][1] = 9; // blue retArray[4][0] = 6; retArray[4][1] = 2; retArray[5][0] = 7; retArray[5][1] = 14; // yellow retArray[6][0] = 11; retArray[6][1] = 1; retArray[7][0] = 3; retArray[7][1] = 13; return (retArray); } /** * Creates the array of key sprites. (call this only once to avoid creating * redundant sprites.) */ int[][] getKeyCoords() { int[][] retArray = new int[4][]; for (int i = 0; i < retArray.length; i++) { retArray[i] = new int[2]; } // red retArray[0][0] = 12; retArray[0][1] = 2; // green retArray[1][0] = 2; retArray[1][1] = 2; // blue retArray[2][0] = 13; retArray[2][1] = 5; // yellow retArray[3][0] = 4; retArray[3][1] = 8; return (retArray); } //-------------------------------------------------------- // encoding / decoding utilities /** * Encodes the entire dungeon. */ byte[][] encodeDungeon() { byte[][] retArray = new byte[2][]; retArray[0] = new byte[16]; // the first byte is the version number: retArray[0][0] = 0; // the second byte is the board number: retArray[0][1] = 0; // the player's start square: retArray[0][2] = DataConverter.encodeCoords(myPlayerSquare); // the goal (crown) square: retArray[0][3] = DataConverter.encodeCoords(myGoalSquare); //encode the keys: int[][] keyCoords = getKeyCoords(); for (int i = 0; i < keyCoords.length; i++) { retArray[0][i + 4] = DataConverter.encodeCoords(keyCoords[i]); } //encode the doors: int[][] doorCoords = getDoorCoords(); for (int i = 0; i < doorCoords.length; i++) { retArray[0][i + 8] = DataConverter.encodeCoords(doorCoords[i]); } //encode the maze: try { retArray[1] = encodeDungeon(mySquares); } catch (Exception e) { e.printStackTrace(); } return (retArray); } /** * Takes a dungeon given in terms of an array of 1s and 0s and turns it into * an array of bytes. WARNING: the array MUST BE 16 X 16. */ static byte[] encodeDungeon(int[][] dungeonMap) throws Exception { if ((dungeonMap.length != 16) || (dungeonMap[0].length != 16)) { throw (new Exception( "EncodingUtils.encodeDungeon-->must be 16x16!!!")); } byte[] retArray = new byte[32]; for (int i = 0; i < 16; i++) { retArray[2 * i] = DataConverter.encode8(dungeonMap[i], 0); retArray[2 * i + 1] = DataConverter.encode8(dungeonMap[i], 8); } return (retArray); } //-------------------------------------------------------- // main prints the bytes to standard out. // (note that this class is not intended to be run as a MIDlet) /** * Prints the byte version of the board to standard out. */ public static void main(String[] args) { try { EncodingUtils map = new EncodingUtils(); byte[][] data = map.encodeDungeon(); System.out.println("EncodingUtils.main-->dungeon encoded"); System.out.print("{\n " + data[0][0]); for (int i = 1; i < data[0].length; i++) { System.out.print(", " + data[0][i]); } for (int i = 1; i < data[1].length; i++) { System.out.print(", " + data[1][i]); } System.out.println("\n};"); } catch (Exception e) { e.printStackTrace(); } } } /** * This class handles the graphics objects. * * @author Carol Hamer */ class DungeonManager extends LayerManager { //--------------------------------------------------------- // dimension fields // (constant after initialization) /** * The x-coordinate of the place on the game canvas where the LayerManager * window should appear, in terms of the coordiantes of the game canvas. */ static int CANVAS_X; /** * The y-coordinate of the place on the game canvas where the LayerManager * window should appear, in terms of the coordiantes of the game canvas. */ static int CANVAS_Y; /** * The width of the display window. */ static int DISP_WIDTH; /** * The height of this object's visible region. */ static int DISP_HEIGHT; /** * the (right or left) distance the player goes in a single keystroke. */ static final int MOVE_LENGTH = 8; /** * The width of the square tiles that this game is divided into. This is the * width of the stone walls as well as the princess and the ghost. */ static final int SQUARE_WIDTH = 24; /** * The jump index that indicates that no jump is currently in progress.. */ static final int NO_JUMP = -6; /** * The maximum speed for the player's fall.. */ static final int MAX_FREE_FALL = 3; //--------------------------------------------------------- // game object fields /** * the handle back to the canvas. */ private DungeonCanvas myCanvas; /** * the background dungeon. */ private TiledLayer myBackground; /** * the player. */ private Sprite myPrincess; /** * the goal. */ private Sprite myCrown; /** * the doors. */ private DoorKey[] myDoors; /** * the keys. */ private DoorKey[] myKeys; /** * the key currently held by the player. */ private DoorKey myHeldKey; /** * The leftmost x-coordinate that should be visible on the screen in terms * of this objects internal coordinates. */ private int myViewWindowX; /** * The top y-coordinate that should be visible on the screen in terms of * this objects internal coordinates. */ private int myViewWindowY; /** * Where the princess is in the jump sequence. */ private int myIsJumping = NO_JUMP; /** * Whether or not the screen needs to be repainted. */ private boolean myModifiedSinceLastPaint = true; /** * Which board we're playing on. */ private int myCurrentBoardNum = 0; //----------------------------------------------------- // gets/sets /** * Tell the layer manager that it needs to repaint. */ public void setNeedsRepaint() { myModifiedSinceLastPaint = true; } //----------------------------------------------------- // initialization // set up or save game data. /** * Constructor merely sets the data. * * @param x * The x-coordinate of the place on the game canvas where the * LayerManager window should appear, in terms of the coordiantes * of the game canvas. * @param y * The y-coordinate of the place on the game canvas where the * LayerManager window should appear, in terms of the coordiantes * of the game canvas. * @param width * the width of the region that is to be occupied by the * LayoutManager. * @param height * the height of the region that is to be occupied by the * LayoutManager. * @param canvas * the DungeonCanvas that this LayerManager should appear on. */ public DungeonManager(int x, int y, int width, int height, DungeonCanvas canvas) throws Exception { myCanvas = canvas; CANVAS_X = x; CANVAS_Y = y; DISP_WIDTH = width; DISP_HEIGHT = height; // create a decoder object that creates the dungeon and // its associated Sprites from data. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // starts on. int[] playerCoords = decoder.getPlayerSquare(); // create the player sprite myPrincess = new Sprite(Image.createImage("/images/princess.png"), SQUARE_WIDTH, SQUARE_WIDTH); myPrincess.setFrame(1); // we define the reference pixel to be in the middle // of the princess image so that when the princess turns // from right to left (and vice versa) she does not // appear to move to a different location. myPrincess.defineReferencePixel(SQUARE_WIDTH / 2, 0); // the dungeon is a 16x16 grid, so the array playerCoords // gives the player's location in terms of the grid, and // then we multiply those coordinates by the SQUARE_WIDTH // to get the precise pixel where the player should be // placed (in terms of the LayerManager's coordinate system) myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH * playerCoords[1]); // we append all of the Layers (TiledLayer and Sprite) // so that this LayerManager will paint them when // flushGraphics is called. append(myPrincess); // get the coordinates of the square where the crown // should be placed. int[] goalCoords = decoder.getGoalSquare(); myCrown = new Sprite(Image.createImage("/images/crown.png")); myCrown.setPosition( (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); append(myCrown); // The decoder creates the door and key sprites and places // them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); } for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); } // append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null; System.gc(); } /** * sets all variables back to their initial positions. */ void reset() throws Exception { // first get rid of the old board: for (int i = 0; i < myDoors.length; i++) { remove(myDoors[i]); } myHeldKey = null; for (int i = 0; i < myKeys.length; i++) { remove(myKeys[i]); } remove(myBackground); // now create the new board: myCurrentBoardNum++; // in this version we go back to the beginning if // all boards have been completed. if (myCurrentBoardNum == BoardDecoder.getNumBoards()) { myCurrentBoardNum = 0; } // we create a new decoder object to read and interpret // all of the data for the current board. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // starts on. int[] playerCoords = decoder.getPlayerSquare(); // the dungeon is a 16x16 grid, so the array playerCoords // gives the player's location in terms of the grid, and // then we multiply those coordinates by the SQUARE_WIDTH // to get the precise pixel where the player should be // placed (in terms of the LayerManager's coordinate system) myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH * playerCoords[1]); myPrincess.setFrame(1); // get the coordinates of the square where the crown // should be placed. int[] goalCoords = decoder.getGoalSquare(); myCrown.setPosition( (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); // The decoder creates the door and key sprites and places // them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); } for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); } // append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null; System.gc(); } /** * sets all variables back to the position in the saved game. * * @return the time on the clock of the saved game. */ int revertToSaved() throws Exception { int retVal = 0; // first get rid of the old board: for (int i = 0; i < myDoors.length; i++) { remove(myDoors[i]); } myHeldKey = null; for (int i = 0; i < myKeys.length; i++) { remove(myKeys[i]); } remove(myBackground); // now get the info of the saved game // only one game is saved at a time, and the GameInfo object // will read the saved game's data from memory. GameInfo info = new GameInfo(); if (info.getIsEmpty()) { // if no game has been saved, we start from the beginning. myCurrentBoardNum = 0; reset(); } else { // get the time on the clock of the saved game. retVal = info.getTime(); // get the number of the board the saved game was on. myCurrentBoardNum = info.getBoardNum(); // create the BoradDecoder that gives the data for the // desired board. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // was on in the saved game. int[] playerCoords = info.getPlayerSquare(); myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH * playerCoords[1]); myPrincess.setFrame(1); // get the coordinates of the square where the crown // should be placed (this is given by the BoardDecoder // and not from the data of the saved game because the // crown does not move during the game. int[] goalCoords = decoder.getGoalSquare(); myCrown.setPosition((SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); // The decoder creates the door and key sprites and places // them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); // get an array of ints that lists whether each door is // open or closed in the saved game int[] openDoors = info.getDoorsOpen(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); if (openDoors[i] == 0) { // if the door was open, make it invisible myDoors[i].setVisible(false); } } // the keys can be moved by the player, so we get their // coordinates from the GameInfo saved data. int[][] keyCoords = info.getKeyCoords(); for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); myKeys[i].setPosition(SQUARE_WIDTH * keyCoords[i][0], SQUARE_WIDTH * keyCoords[i][1]); } // if the player was holding a key in the saved game, // we have the player hold that key and set it to invisible. int heldKey = info.getHeldKey(); if (heldKey != -1) { myHeldKey = myKeys[heldKey]; myHeldKey.setVisible(false); } // append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null; System.gc(); } return (retVal); } /** * save the current game in progress. */ void saveGame(int gameTicks) throws Exception { int[] playerSquare = new int[2]; // the coordinates of the player are given in terms of // the 16 x 16 dungeon grid. We divide the player's // pixel coordinates to ge the right grid square. // If the player was not precisely alligned with a // grid square when the game was saved, the difference // will be shaved off. playerSquare[0] = myPrincess.getX() / SQUARE_WIDTH; playerSquare[1] = myPrincess.getY() / SQUARE_WIDTH; // save the coordinates of the current locations of // the keys, and if a key is currently held by the // player, we save the info of which one it was. int[][] keyCoords = new int[4][]; int heldKey = -1; for (int i = 0; i < myKeys.length; i++) { keyCoords[i] = new int[2]; keyCoords[i][0] = myKeys[i].getX() / SQUARE_WIDTH; keyCoords[i][1] = myKeys[i].getY() / SQUARE_WIDTH; if ((myHeldKey != null) && (myKeys[i] == myHeldKey)) { heldKey = i; } } // save the information of which doors were open. int[] doorsOpen = new int[8]; for (int i = 0; i < myDoors.length; i++) { if (myDoors[i].isVisible()) { doorsOpen[i] = 1; } } // take all of the information we've gathered and // create a GameInfo object that will save the info // in the device's memory. GameInfo info = new GameInfo(myCurrentBoardNum, gameTicks, playerSquare, keyCoords, doorsOpen, heldKey); } //------------------------------------------------------- // graphics methods /** * paint the game graphic on the screen. */ public void paint(Graphics g) throws Exception { // only repaint if something has changed: if (myModifiedSinceLastPaint) { g.setColor(DungeonCanvas.WHITE); // paint the background white to cover old game objects // that have changed position since last paint. // here coordinates are given // with respect to the graphics (canvas) origin: g.fillRect(0, 0, DISP_WIDTH, DISP_HEIGHT); // here coordinates are given // with respect to the LayerManager origin: setViewWindow(myViewWindowX, myViewWindowY, DISP_WIDTH, DISP_HEIGHT); // call the paint funstion of the superclass LayerManager // to paint all of the Layers paint(g, CANVAS_X, CANVAS_Y); // don't paint again until something changes: myModifiedSinceLastPaint = false; } } //------------------------------------------------------- // game movements /** * respond to keystrokes by deciding where to move and then moving the * pieces and the view window correspondingly. */ void requestMove(int horizontal, int vertical) { if (horizontal != 0) { // see how far the princess can move in the desired // horizontal direction (if not blocked by a wall // or closed door) horizontal = requestHorizontal(horizontal); } // vertical < 0 indicates that the user has // pressed the UP button and would like to jump. // therefore, if we're not currently jumping, // we begin the jump. if ((myIsJumping == NO_JUMP) && (vertical < 0)) { myIsJumping++; } else if (myIsJumping == NO_JUMP) { // if we're not jumping at all, we need to check // if the princess should be falling: // we (temporarily) move the princess down and see if that // causes a collision with the floor: myPrincess.move(0, MOVE_LENGTH); // if the princess can move down without colliding // with the floor, then we set the princess to // be falling. The variable myIsJumping starts // negative while the princess is jumping up and // is zero or positive when the princess is coming // back down. We therefore set myIsJumping to // zero to indicate that the princess should start // falling. if (!checkCollision()) { myIsJumping = 0; } // we move the princess Sprite back to the correct // position she was at before we (temporarily) moved // her down to see if she would fall. myPrincess.move(0, -MOVE_LENGTH); } // if the princess is currently jumping or falling, // we calculate the vertical distance she should move // (taking into account the horizontal distance that // she is also moving). if (myIsJumping != NO_JUMP) { vertical = jumpOrFall(horizontal); } // now that we've calculated how far the princess // should move, we move her. (this is a call to // another internal method of this method // suite, it is not a built-in LayerManager method): move(horizontal, vertical); } /** * Internal to requestMove. Calculates what the real horizontal distance * moved should be after taking obstacles into account. * * @return the horizontal distance that the player can move. */ private int requestHorizontal(int horizontal) { // we (temporarily) move her to the right or left // and see if she hits a wall or a door: myPrincess.move(horizontal * MOVE_LENGTH, 0); if (checkCollision()) { // if she hits something, then she's not allowed // to go in that direction, so we set the horizontal // move distance to zero and then move the princess // back to where she was. myPrincess.move(-horizontal * MOVE_LENGTH, 0); horizontal = 0; } else { // if she doesn't hit anything then the move request // succeeds, but we still move her back to the // earlier position because this was just the checking // phase. myPrincess.move(-horizontal * MOVE_LENGTH, 0); horizontal *= MOVE_LENGTH; } return (horizontal); } /** * Internal to requestMove. Calculates the vertical change in the player's * position if jumping or falling. this method should only be called if the * player is currently jumping or falling. * * @return the vertical distance that the player should move this turn. * (negative moves up, positive moves down) */ private int jumpOrFall(int horizontal) { // by default we do not move vertically int vertical = 0; // The speed of rise or descent is computed using // the int myIsJumping. Since we are in a jump or // fall, we advance the jump by one (which simulates // the downward pull of gravity by slowing the rise // or accellerating the fall) unless the player is // already falling at maximum speed. (a maximum // free fall speed is necessary because otherwise // it is possible for the player to fall right through // the bottom of the maze...) if (myIsJumping <= MAX_FREE_FALL) { myIsJumping++; } if (myIsJumping < 0) { // if myIsJumping is negative, that means that // the princess is rising. We calculate the // number of pixels to go up by raising 2 to // the power myIsJumping (absolute value). // note that we make the result negative because // the up and down coordinates in Java are the // reverse of the vertical coordinates we learned // in math class: as you go up, the coordinate // values go down, and as you go down the screen, // the coordinate numbers go up. vertical = -(2 << (-myIsJumping)); } else { // if myIsJumping is positive, the princess is falling. // we calculate the distance to fall by raising two // to the power of the absolute value of myIsJumping. vertical = (2 << (myIsJumping)); } // now we temporarily move the princess the desired // vertical distance (with the corresponding horizontal // distance also thrown in), and see if she hits anything: myPrincess.move(horizontal, vertical); if (checkCollision()) { // here we're in the case where she did hit something. // we move her back into position and then see what // to do about it. myPrincess.move(-horizontal, -vertical); if (vertical > 0) { // in this case the player is falling. // so we need to determine precisely how // far she can fall before she hit the bottom vertical = 0; // we temporarily move her the desired horizontal // distance while calculating the corresponding // vertical distance. myPrincess.move(horizontal, 0); while (!checkCollision()) { vertical++; myPrincess.move(0, 1); } // now that we've calculated how far she can fall, // we move her back to her earlier position myPrincess.move(-horizontal, -vertical); // we subtract 1 pixel from the distance calculated // because once she has actually collided with the // floor, she's gone one pixel too far... vertical--; // now that she's hit the floor, she's not jumping // anymore. myIsJumping = NO_JUMP; } else { // in this case we're going up, so she // must have hit her head. // This next if is checking for a special // case where there's room to jump up exactly // one square. In that case we increase the // value of myIsJumping in order to make the // princess not rise as high. The details // of the calculation in this case were found // through trial and error: if (myIsJumping == NO_JUMP + 2) { myIsJumping++; vertical = -(2 << (-myIsJumping)); // now we see if the special shortened jump // still makes her hit her head: // (as usual, temporarily move her to test // for collisions) myPrincess.move(horizontal, vertical); if (checkCollision()) { // if she still hits her head even // with this special shortened jump, // then she was not meant to jump... myPrincess.move(-horizontal, -vertical); vertical = 0; myIsJumping = NO_JUMP; } else { // now that we've chhecked for collisions, // we move the player back to her earlier // position: myPrincess.move(-horizontal, -vertical); } } else { // if she hit her head, then she should not // jump up. vertical = 0; myIsJumping = NO_JUMP; } } } else { // since she didn't hit anything when we moved // her, then all we have to do is move her back. myPrincess.move(-horizontal, -vertical); } return (vertical); } /** * Internal to requestMove. Once the moves have been determined, actually * perform the move. */ private void move(int horizontal, int vertical) { // repaint only if we actually change something: if ((horizontal != 0) || (vertical != 0)) { myModifiedSinceLastPaint = true; } // if the princess is moving left or right, we set // her image to be facing the right direction: if (horizontal > 0) { myPrincess.setTransform(Sprite.TRANS_NONE); } else if (horizontal < 0) { myPrincess.setTransform(Sprite.TRANS_MIRROR); } // if she's jumping or falling, we set the image to // the frame where the skirt is inflated: if (vertical != 0) { myPrincess.setFrame(0); // if she's just running, we alternate between the // two frames: } else if (horizontal != 0) { if (myPrincess.getFrame() == 1) { myPrincess.setFrame(0); } else { myPrincess.setFrame(1); } } // move the position of the view window so that // the player stays in the center: myViewWindowX += horizontal; myViewWindowY += vertical; // after all that work, we finally move the // princess for real!!! myPrincess.move(horizontal, vertical); } //------------------------------------------------------- // sprite interactions /** * Drops the currently held key and picks up another. */ void putDownPickUp() { // we do not want to allow the player to put // down the key in the air, so we verify that // we're not jumping or falling first: if ((myIsJumping == NO_JUMP) && (myPrincess.getY() % SQUARE_WIDTH == 0)) { // since we're picking something up or putting // something down, the display changes and needs // to be repainted: setNeedsRepaint(); // if the thing we're picking up is the crown, // we're done, the player has won: if (myPrincess.collidesWith(myCrown, true)) { myCanvas.setGameOver(); return; } // keep track of the key we're putting down in // order to place it correctly: DoorKey oldHeld = myHeldKey; myHeldKey = null; // if the princess is on top of another key, // that one becomes the held key and is hence // made invisible: for (int i = 0; i < myKeys.length; i++) { // we check myHeldKey for null because we don't // want to accidentally pick up two keys. if ((myPrincess.collidesWith(myKeys[i], true)) && (myHeldKey == null)) { myHeldKey = myKeys[i]; myHeldKey.setVisible(false); } } if (oldHeld != null) { // place the key we're putting down in the Princess's // current position and make it visible: oldHeld.setPosition(myPrincess.getX(), myPrincess.getY()); oldHeld.setVisible(true); } } } /** * Checks of the player hits a stone wall or a door. */ boolean checkCollision() { boolean retVal = false; // the "true" arg meand to check for a pixel-level // collision (so merely an overlap in image // squares does not register as a collision) if (myPrincess.collidesWith(myBackground, true)) { retVal = true; } else { // Note: it is not necessary to synchronize // this block because the thread that calls this // method is the same as the one that puts down the // keys, so there's no danger of the key being put down // between the moment we check for the key and // the moment we open the door: for (int i = 0; i < myDoors.length; i++) { // if she's holding the right key, then open the door // otherwise bounce off if (myPrincess.collidesWith(myDoors[i], true)) { if ((myHeldKey != null) && (myDoors[i].getColor() == myHeldKey.getColor())) { setNeedsRepaint(); myDoors[i].setVisible(false); } else { // if she's not holding the right key, then // she has collided with the door just the same // as if she had collided with a wall: retVal = true; } } } } return (retVal); } } /** * This class is the display of the game. * * @author Carol Hamer */ class DungeonCanvas extends GameCanvas { //--------------------------------------------------------- // dimension fields // (constant after initialization) /** * the height of the black region below the play area. */ static int TIMER_HEIGHT = 32; /** * the top corner x coordinate according to this object's coordinate * system:. */ static final int CORNER_X = 0; /** * the top corner y coordinate according to this object's coordinate * system:. */ static final int CORNER_Y = 0; /** * the width of the portion of the screen that this canvas can use. */ static int DISP_WIDTH; /** * the height of the portion of the screen that this canvas can use. */ static int DISP_HEIGHT; /** * the height of the font used for this game. */ static int FONT_HEIGHT; /** * the font used for this game. */ static Font FONT; /** * color constant */ public static final int BLACK = 0; /** * color constant */ public static final int WHITE = 0xffffff; //--------------------------------------------------------- // game object fields /** * a handle to the display. */ private Display myDisplay; /** * a handle to the MIDlet object (to keep track of buttons). */ private Dungeon myDungeon; /** * the LayerManager that handles the game graphics. */ private DungeonManager myManager; /** * whether or not the game has ended. */ private static boolean myGameOver; /** * The number of ticks on the clock the last time the time display was * updated. This is saved to determine if the time string needs to be * recomputed. */ private int myOldGameTicks = 0; /** * the number of game ticks that have passed since the beginning of the * game. */ private int myGameTicks = myOldGameTicks; /** * we save the time string to avoid recreating it unnecessarily. */ private static String myInitialString = "0:00"; /** * we save the time string to avoid recreating it unnecessarily. */ private String myTimeString = myInitialString; //----------------------------------------------------- // gets/sets /** * This is called when the game ends. */ void setGameOver() { myGameOver = true; myDungeon.pauseApp(); } /** * Find out if the game has ended. */ static boolean getGameOver() { return (myGameOver); } /** * Tell the layer manager that it needs to repaint. */ public void setNeedsRepaint() { myManager.setNeedsRepaint(); } //----------------------------------------------------- // initialization and game state changes /** * Constructor sets the data, performs dimension calculations, and creates * the graphical objects. */ public DungeonCanvas(Dungeon midlet) throws Exception { super(false); myDisplay = Display.getDisplay(midlet); myDungeon = midlet; // calculate the dimensions DISP_WIDTH = getWidth(); DISP_HEIGHT = getHeight(); if ((!myDisplay.isColor()) || (myDisplay.numColors() < 256)) { throw (new Exception("game requires full-color screen")); } if ((DISP_WIDTH < 150) || (DISP_HEIGHT < 170)) { throw (new Exception("Screen too small")); } if ((DISP_WIDTH > 250) || (DISP_HEIGHT > 250)) { throw (new Exception("Screen too large")); } // since the time is painted in white on black, // it shows up better if the font is bold: FONT = Font .getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM); // calculate the height of the black region that the // timer is painted on: FONT_HEIGHT = FONT.getHeight(); TIMER_HEIGHT = FONT_HEIGHT + 8; // create the LayerManager (where all of the interesting // graphics go!) and give it the dimensions of the // region it is supposed to paint: if (myManager == null) { myManager = new DungeonManager(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT - TIMER_HEIGHT, this); } } /** * This is called as soon as the application begins. */ void start() { myGameOver = false; myDisplay.setCurrent(this); setNeedsRepaint(); } /** * sets all variables back to their initial positions. */ void reset() throws Exception { // most of the variables that need to be reset // are held by the LayerManager: myManager.reset(); myGameOver = false; setNeedsRepaint(); } /** * sets all variables back to the positions from a previously saved game. */ void revertToSaved() throws Exception { // most of the variables that need to be reset // are held by the LayerManager, so we // prompt the LayerManager to get the // saved data: myGameTicks = myManager.revertToSaved(); myGameOver = false; myOldGameTicks = myGameTicks; myTimeString = formatTime(); setNeedsRepaint(); } /** * save the current game in progress. */ void saveGame() throws Exception { myManager.saveGame(myGameTicks); } /** * clears the key states. */ void flushKeys() { getKeyStates(); } /** * If the game is hidden by another app (or a menu) ignore it since not much * happens in this game when the user is not actively interacting with it. * (we could pause the timer, but it's not important enough to bother with * when the user is just pulling up a menu for a few seconds) */ protected void hideNotify() { } /** * When it comes back into view, just make sure the manager knows that it * needs to repaint. */ protected void showNotify() { setNeedsRepaint(); } //------------------------------------------------------- // graphics methods /** * paint the game graphics on the screen. */ public void paint(Graphics g) { // color the bottom segment of the screen black g.setColor(BLACK); g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT - TIMER_HEIGHT, DISP_WIDTH, TIMER_HEIGHT); // paint the LayerManager (which paints // all of the interesting graphics): try { myManager.paint(g); } catch (Exception e) { myDungeon.errorMsg(e); } // draw the time g.setColor(WHITE); g.setFont(FONT); g.drawString("Time: " + formatTime(), DISP_WIDTH / 2, CORNER_Y + DISP_HEIGHT - 4, g.BOTTOM | g.HCENTER); // write "Dungeon Completed" when the user finishes a board: if (myGameOver) { myDungeon.setNewCommand(); // clear the top region: g.setColor(WHITE); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT * 2 + 1); int goWidth = FONT.stringWidth("Dungeon Completed"); g.setColor(BLACK); g.setFont(FONT); g.drawString("Dungeon Completed", (DISP_WIDTH - goWidth) / 2, CORNER_Y + FONT_HEIGHT, g.TOP | g.LEFT); } } /** * a simple utility to make the number of ticks look like a time... */ public String formatTime() { if ((myGameTicks / 16) != myOldGameTicks) { myTimeString = ""; myOldGameTicks = (myGameTicks / 16) + 1; int smallPart = myOldGameTicks % 60; int bigPart = myOldGameTicks / 60; myTimeString += bigPart + ":"; if (smallPart / 10 < 1) { myTimeString += "0"; } myTimeString += smallPart; } return (myTimeString); } //------------------------------------------------------- // game movements /** * update the display. */ void updateScreen() { myGameTicks++; // paint the display try { paint(getGraphics()); flushGraphics(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT); } catch (Exception e) { myDungeon.errorMsg(e); } } /** * Respond to keystrokes. */ public void checkKeys() { if (!myGameOver) { int vertical = 0; int horizontal = 0; // determine which moves the user would like to make: int keyState = getKeyStates(); if ((keyState & LEFT_PRESSED) != 0) { horizontal = -1; } if ((keyState & RIGHT_PRESSED) != 0) { horizontal = 1; } if ((keyState & UP_PRESSED) != 0) { vertical = -1; } if ((keyState & DOWN_PRESSED) != 0) { // if the user presses the down key, // we put down or pick up a key object // or pick up the crown: myManager.putDownPickUp(); } // tell the manager to move the player // accordingly if possible: myManager.requestMove(horizontal, vertical); } } }
1. | Maze game | ||
2. | Checkers game | ||
3. | Game Action Example | ||
4. | Game Key Event | ||
5. | Sweep Game | ||
6. | Sweep | ||
7. | Tumbleweed game | ||
8. | Canvas for processing game actions |