Writing Cool Games for Cellular Devices Blog

Version 2

    {cs.r.title}



    One of the main usages of J2ME is game programming. In this article, I'll explain this by way of a standalone game that I developed. This application is a simple basketball game, in which the player plays against a computer opponent. Each game lasts exactly two minutes. The aim of the game is to shoot as many baskets as you can and to prevent the computer opponent from doing the same.

    Main Functions

    A typical game in J2ME environment might consist of the following:

    • A welcome screen that redirects automatically to the main menu.
    • The main menu.
    • The main screen. This is where the game is actually played.
    • Level menu (set the game difficulty, etc.).
    • Instruction screen.
    • About screen.

    The main screen and the welcome screen are executed by classes that are extends from the Canvas. This class is defined as being in the low-level API, meaning that it allows full control of the display at the pixel level. All the other screens are executed by high-level API classes that use J2ME standard controls.

    When we program apps for J2ME, we must also consider how to handle unique system events such as call interrupts. When we deal with this event, we might want to freeze the current game state or even be able to save this game state after we exit the application.

    In order to get started, we'll have to download the J2ME wireless toolkit. This is the most elementary SDK for this environment. The main objective of this toolkit is to compile the Java source files and to produce two deployment files: a JAR file that encapsulate all of the class files and a JAD (Java Application Descriptor) file. The JAD file provides data about the application such as vendor, JAR size, version number, etc. This SDK can be integrated with more advanced IDEs such as Eclipse or NetBeans. You can learn more about getting started with the wireless toolkit in the article " Wireless Development Tutorial Part I."

    Game Structure

    Figure 1 shows the class structure of the game.

    General structure of the game
    Figure 1. General structure of the game

    The game starts with an opening screen, and after five seconds redirects to a menu screen. From the menu screen, the player has the option to start the game, set the level, display an "about" screen, or display the instructions. Each screen is managed by its own class. For example, the welcome screen is managed by theSplash class, the main game screen is managed byMainScreen, and so forth.

    TheMidlet Class

    A very important class that is not displayed in the diagram above is the TestMidletMIDlet class. This class extends from the Midlet class, and its job is to initialize all of the other classes and controls. One major method of the Midlet class is the startApp()method. Normally, when a midlet is called, it is initially in apaused state. If everything runs normally and no exception occurs, then the midlet enters the active state. This is done by calling the startApp() method. Here is an example:

     
    public void startApp() { // some code here... display.setCurrent(splash); } 
    

    For this stage, the most important call isdisplay.setCurrent(splash). This method call sets the midlet's Displayable; in other words, it determines which of our screens is to be displayed on the device. In this example it will be the Splash screen, as seen in Figure 2.

    Splash screen
    Figure 2. Splash screen

    Splash Screen

    The splash screen is the first screen that appears when the application starts up. It is extends the Canvasclass. The Canvas class, as the so-called low-level API, allows full control of the display at the pixel level. In order to paint something, we have to override thepaint() method. For example:

     
    public void paint(Graphics g) { // sets background color g.setColor(255, 255, 255); // fill the screen with the color //defined previously g.fillRect(0, 0, this.getWidth(), this.getHeight()); // draw some image g.drawImage(screenshot, (this.getWidth() - 128) / 2, (this.getHeight() - 128) / 2, Graphics.TOP|Graphics.LEFT); } 
    

    After a period of five seconds, or by pressing any key, our game changes the display to the main menu screen. In order to handle key presses we have to implement the CommandListenerinterface. All key press events will be handled by the functioncommandAction().

     
    public void commandAction(Command command, Displayable displayable) { // stops timer operation timer.cancel(); /* switch the display to menu. parent is the Midlet object which actually does the job. */ parent.setCurrent("MainMenu2"); } 
    

    Timerand TimerTask are two classes that allow us to execute a function after a period of predefined time. We use these classes to automatically switch the display to the next screen.

     
    public void startTimer() { // TimerTask defines the task or subroutine // to be executed. TimerTask gotoMenu = new TimerTask() { public void run() { timer.cancel(); parent.setCurrent("MainMenu2"); } }; // Timer class creates and manages threads on // which the tasks are executed timer = new Timer(); timer.schedule(gotoMenu, 5000); } 
    

    TheMainMenu Class

    The MainMenu class, shown in Figure 3, shows the main menu of the game.

    Main Menu Screen
    Figure 3. Main menu screen

    The main menu has five elements:
    • Continue: Continues a game that has been previously stopped.
    • Start Game: Starts a new game.
    • Choose Level: Choose the speed of the computer player.
    • Instructions: Shows an instructions screen.
    • About: Shows an about screen.

    In J2ME, the class Listis responsible for displaying lists or menus, which is whyMainMenu subclasses it. As in the Splashclass, we have to implement the CommandListenerinterface in order to handle key presses.

    TheMainScreen Class

    MainScreen, shown in Figure 4, is the class that displays the actual basketball game being played. TheMainScreen class is extended from theCanvas class.

    Figure 4
    Figure 4. Main game screen

    Rules of the Game

    As mentioned before, this is a standalone game in which the player plays against the computer. There are two players on the screen. The human player controls the blue player, who tries to put the ball in the basket on the right. The computer controls the red player. Each game lasts two minutes, and the player who shoots the most baskets wins. The human player controls the red player by the arrow keys or by the number keys: 2 for up,8 for down, 6 for right, 4for left, and 5 to shoot the ball.

    Elements in the Main Screen

    Several graphic elements compose this screen. These include the background, the players, the ball, etc. Each element must be a PNG graphic file; these are illustrated in Figures 5 through 8.

    Screen background
    Figure 5. Screen background

    Human player icon
    Figure 6. Human player icon

    Computer player icon
    Figure 7. Computer player icon

    Ball icon
    Figure 8. Ball icon

    When the class MainScreen initializes, we must get each graphic element from the JAR file and store it in anImage field. This can be done in the constructor.

     
    private Image screenShot; public MainScreen(TestMidletMIDlet parent) { // some code... try { screenShot = Image.createImage ("/Images/screenShot.png"); } catch(IOException e) {} // we do this for all the other images } 
    
    Handling Key Presses

    As in previous classes, we again implement theCommandListener interface. Here, we only useCommandListener to pause the game and return to the main menu. In order to do this, we have to initialize a Commandobject first. In our example we label this object asPause. The label can be seen in Figure 4 on the bottom left side of the screen.

     
    // this is the Command object, labeled as "Pause" private Command pauseCommand = new Command("Pause", Command.STOP, 1); 
    

    All of the CommandListener events are handled bycommandAction().

     
    /** * Handles command actions */ public void commandAction (Command c, Displayable d) { // verified that the pause button was pressed if (c == pauseCommand) { // do something } } 
    

    In the Canvas class, we can also handle key presses with the keyPressed(int keyCode) andkeyReleased(int keyCode) methods. Every key press invokes keyPressed(), and every key release invokeskeyReleased(). In our game, we use these functions to handle all of the arrow and select keys. These are the buttons that move the human's player icon.

     
    private keyStatus = 0; /** * Called when a key is pressed. */ protected void keyPressed(int keyCode) { if (this.getGameAction(keyCode) == UP) { keyStatus = KEY_NUM2; } else if(this.getGameAction(keyCode) == DOWN){ keyStatus = KEY_NUM8; } else if(this.getGameAction(keyCode) == LEFT){ keyStatus = KEY_NUM4; } else if(this.getGameAction(keyCode) == RIGHT){ keyStatus = KEY_NUM6; } else if(this.getGameAction(keyCode) == FIRE){ keyStatus = KEY_NUM5; } else { keyStatus = keyCode; } } /** * Called when a key is released. */ protected void keyReleased(int keyCode) { keyStatus = 0; } 
    

    In general, this method updates the value of the fieldkeyStatus. If no key is being pressed at the moment, then the value of keyStatus is 0; otherwise, it is some integer value. The actual movement of the player will be explained later.

    Algorithm of the Game

    This class initializes an internal TimerTask class. The actual initialization is done by a Timer, which invokes the TimerTask periodically (by default, every 50 milliseconds, but this is adjustable by the human player on the Level screen). The TimerTask class executes the function myMove2().

     
    private Timer timer; public void startTimer() { // this class is being executed periodically. TimerTask mover = new TimerTask() { public void run() { myMove2(); } }; timer = new Timer(); // invokes the mover class. try { // if anything is being set by the level // screen timer.schedule(mover, parent.getLevel(), parent.getLevel()); } catch (IllegalArgumentException e) { timer.schedule(mover, 50, 50); } } 
    

    The myMove2() method has two roles:

    1. It checks the value of keyStatus (which is set by the keyPressed() and keyReleased()methods) and moves the human player icon accordingly.
    2. it moves the computer player icon according to the situation evolving in the game.

    As previously explained, each key press (on the arrow and select keys) sets the value of the keyStatus field. According to that value, we calculate the coordinates of the human player icon.

     
    // coordinates of human player icon private int meX, meY; // coordinates of the ball private int xBall, freeBallY, yBall; // coordinates of the field's corner private int x1, x2, x3, x4, y1, y2, y3, y4; private void myMove2() { // some code // define me Coordinates switch(keyStatus) { // up case Canvas.KEY_NUM2: meY--; if (meY < y1) meY = y1; break; // down case Canvas.KEY_NUM8: meY++; if (meY > y4) meY = y4; break; // right case Canvas.KEY_NUM6: meX++; if ((x2 + x3) / 2 < meX) { meX = (x2 + x3) / 2; } if (ballOwner == 0 && compMode != 5) xBall = meX + 8; break; // left case Canvas.KEY_NUM4: meX--; if ((x1 + x4) / 2 > meX) { meX = (x1 + x4) / 2; } if (ballOwner == 0 && compMode != 5) xBall = meX + 8; break; // fire case Canvas.KEY_NUM5: if (compMode == 2) { originalY = freeBallY; compMode = 5; } break; default: ///// } // some code... } 
    

    As the game runs, the computer controls the opposing player's icon. The CPU player's actions vary according to the situations that evolve during the game--in different situations, the computer behaves differently. The int fieldcompMode stores a "situation code" for every given moment. The situations are:

    1. Jump ball: This situation occurs only at the beginning of the game. Both players' icons are at the middle of the field, and the ball icon is right in between them, falling down. We switch situation status when either player icon catches the ball.
    2. The human player icon has the ball: When the human player moves, we can see the ball being dribbled with the human player. The computer moves its player toward the human player, and tries to steal the ball when it's close enough.
    3. The computer player icon has the ball: Here, the computer player dribbles the ball. In this situation, the computer tries to move towards the human player's basket. If the human player icon is in front of him, it will try to bypass him. When the computer player is near enough to the human's basket, it will try to shoot a basket.
    4. Not in use.
    5. The human player shoots the ball: The computer switches to this situation when the player presses the select(or fire, on some devices) key. We can see the ball being thrown to the basket. If the shot is successful, then the two players are replaced on either side of the field and the computer switches the situation to 3(compMode=3).
    6. The computer player shoots the ball: When the computer player has the ball and the computer player icon is near enough to the human player's basket, it will automatically try to shoot a basket. If the shot is successful, the two players are replaced on either side of the field and the computer switches the situation to2 (compMode=2).
     
    // coordinates of human player icon private int meX, meY; // coordinates of the ball private int xBall, freeBallY, yBall; // coordinates of the field's corner private int x1, x2, x3, x4, y1, y2, y3, y4; // computer player situation state private int compMode; private void myMove2() { // some code... // switch by situation switch (compMode) { /* *free ball jumps */ case 1: // this method controls the jump // movement of the ball ballJump(); // decides who gets the ball // computer gets the ball if (Math.abs(xBall - compX) <= 21 && Math.abs(freeBallY - compY) <= 5) { case3Mode = 1; compMode = 3; delay = 0; } // me kidnaps the ball if (Math.abs(meX - xBall) <= 21 && Math.abs(meY - freeBallY) <= 5) { compMode = 2; delay = 0; delay2 = 0; } // calculate comp moves if (compY > freeBallY) { compY--; } if (compY < freeBallY) { compY++; } if (compX > xBall) { compX--; } if (compX < xBall) { compX++; } compCheckBorders(); break; /* *the ball is at me player */ case 2: xBall = meX + 6; freeBallY = meY; ballOwner = 0; delay2++; ballJump(); // computer steals the ball if (Math.abs(meX - compX) <= 21 && Math.abs(meY - compY) <= 5 && delay >= 10) { case3Mode = 1; compMode = 3; delay = 0; delay2 = 0; } if (delay2 > 30) { if (Math.abs(meX + 20 - compX) > Math.abs(meY - compY)) { if (compX > meX + 20) compX--; else compX++; } else { if (compY > meY) compY--; else compY++; } // check borders for comp players compCheckBorders(); } break; /* *the ball is at computer player */ case 3: xBall = compX - 3; freeBallY = compY; ballOwner = 1; ballJump(); // me kidnaps the ball if (Math.abs(meX - compX) <= 21 && Math.abs(meY - compY) <= 5 && delay >= 5) { compMode = 2; delay = 0; delay2 = 0; } /* here we compute how the computer player icon will move */ switch (case3Mode) { // go back from player case 1: compX++; if (compX > meX + 29) { case3Mode = 2; } break; // go side from player case 2: if (compY < myHeight * 3 / 4) { compY++; compYDirection = 1; case3Mode = 3; } compY--; compYDirection = 0; case3Mode = 3; break; // continue go side case 3: if (compYDirection == 1 && compY <= meY + 15) { compY++; if (compY > y4) compY = y4; } else if (compYDirection == 0 && compY >= meY - 15) { compY--; if (compY < y1) compY = y1; } else { case3Mode = 4; } break; // go forward case 4: if (compX > myWidth * 11 / 32) { compX--; } else if (compX < myWidth * 1 / 4 - 3) { compX++; } else { originalY = freeBallY; compMode = 6; } break; // finally throw the ball case 5: case3Mode = 1; originalY = freeBallY; compMode = 6; yBall = 9; deltaX = 1; break; default: // } if (compX - meX <= 15 && Math.abs(compY - meY) <= 10) { case3Mode = 1; } // check borders for comp player compCheckBorders(); break; /* *not in use */ case 4: break; /* *me throws the ball */ case 5: freeBallY = originalY - deltaY[deltaX]; deltaX++; xBall++; // checks if the ball hits the basket if (deltaX >= 29 && (xBall >= (x2 + x3) / 2 - 10 && xBall <= (x2 + x3) / 2 + 10)) { myScore += 2; oldY = 0; compMode = 3; // reset player and the balls meX = myWidth * 3 / 16; meY = myHeight * 3 / 4; compX = myWidth * 13 / 16; compY = myHeight * 3 / 4; xBall = compX - 8; yBall = 3; deltaX = 0; } // if the ball reaches the floor else if (deltaX >= 29 && (xBall < (x2 + x3) / 2 - 10 || xBall > (x2 + x3) / 2 + 10)) { freeBallY = meY; compMode = 1; oldY = 0; deltaX = 0; } else { oldY = freeBallY; } break; /* *comp throws the ball */ case 6: freeBallY = originalY - deltaY[deltaX]; deltaX++; xBall--; // checkes if the ball hits the basket if (deltaX >= 29) { compScore += 2; oldY = 0; compMode = 2; // reset player and the balls meX = myWidth * 3 / 16; meY = myHeight * 3 / 4; compX = myWidth * 13 / 16; compY = myHeight * 3 / 4; xBall = meX + 8; yBall= 3; deltaX = 0; } else { oldY = freeBallY; } break; default: //sdfgsdfgsdgf } /* after the coordinates of the human player icon, the computer player icon and the ball has been set we can go to the last stage, which is painting the screen. */ repaint(); } /* this method controls the jump movement of the ball */ private void ballJump() { if (ballDir == 0) { yBall--; if (yBall < 3) { ballDir = 1; } } else { yBall++; if (yBall > 9) { ballDir = 0; } } } /* this function checks if the computer passed the border of the field */ private void compCheckBorders() { if (compY < y1) { compY = y1; } if (compY > y4) { compY = y4; } if (compX > (x2 + x3) / 2) { compX = (x2 + x3) / 2; } if (compX < (x1 + x4) / 2) { compX = (x1 + x4) / 2; } } 
    

    After all of the coordinates have been set, we can proceed to the final stage, which is to actually paint the screen. This is done by calling the paint() method. We call this at the end of the move() function, by callingrepaint().

     
    /** * paints the screen */ public void paint(Graphics g) { Graphics saved = g; // these fields show the clock in the game String clockMinuteStr = new String(); String clockSecondStr = new String(); // initialize a buffered image if (offscreen != null) { g = offscreen.getGraphics(); } // cleans the screen g.setColor(255, 255, 255); g.fillRect(0, 0, this.getWidth(), this.getHeight()); // define corners of field x1 = myWidth*3/16; x2 = myWidth*13/16; x3 = myWidth - 2; x4 = 2; y1 = myHeight * 1 / 2; y4 = myHeight - 1; // draw solid background g.setColor(255, 255, 255); g.fillRect(offsetWidth, offsetHeight, myWidth, myHeight); g.setColor(0, 0, 0); g.drawImage(screenShot, offsetWidth, offsetHeight, 0); // draw Scores g.fillRect(offsetWidth + myWidth / 4, offsetHeight + myHeight / 8, myWidth / 2, myHeight / 4); g.setColor(255, 255, 255); g.drawRect(offsetWidth + myWidth / 4, offsetHeight + myHeight / 8, myWidth / 2, myHeight / 4); clockMinuteStr = String.valueOf(clockMinute); clockSecondStr = String.valueOf(clockSecond); if (clockMinuteStr.length() == 1) clockMinuteStr = "0" + clockMinuteStr; if (clockSecondStr.length() == 1) clockSecondStr = "0" + clockSecondStr; g.drawString(":", offsetWidth + myWidth / 2, offsetHeight + 15, Graphics.TOP|Graphics.LEFT); g.drawString(clockMinuteStr, offsetWidth + myWidth / 2 - 15, offsetHeight + 17, Graphics.TOP|Graphics.LEFT); g.drawString(clockSecondStr, offsetWidth + myWidth / 2 + 5, offsetHeight + 17, Graphics.TOP|Graphics.LEFT); g.drawString(String.valueOf(myScore), offsetWidth + myWidth / 2 - 25, offsetHeight + 32, Graphics.TOP|Graphics.LEFT); g.drawString(String.valueOf(compScore), offsetWidth + myWidth / 2 + 20, offsetHeight + 32, Graphics.TOP|Graphics.LEFT); // paint player me g.drawImage(mePlayer, offsetWidth + meX, offsetHeight + meY - 19, 0); // paint Computer Player g.drawImage(compPlayer, offsetWidth + compX, offsetHeight + compY - 19, 0); //paintBall g.drawImage(tinyBall, offsetWidth + xBall, offsetHeight + freeBallY - yBall, 0); // paints the buffered image if (g != saved) { saved.drawImage(offscreen, 0, 0, Graphics.LEFT | Graphics.TOP); } } 
    
     
    Handling Call Interrupts

    When an incoming call occurs in the middle of the game, theCanvas screen might disappear, so we might want to freeze the game state (save all of the data regarding the players' positions, ball position, number of point, time left, etc.). In our game, the freezing of the game is done by stopping the timer. There are two functions related to the canvas disappearing and reappearing. hideNotify() is called after theCanvas disappears and showNotify() is called when the Canvas reappears. We stop the timer in the hideNotify() event and we reactivate the timer in the showNotify() event.

     
    /** * called when the screen disappears */ protected void hideNotify() { if (finishGame == 0) { parent.setCurrent("MainMenu"); } // stops the internal timer and thus // freezes the game. stopTimer(); } /** * called when the screen reappears */ protected void showNotify() { // restarts the internal timers. startTimer(); } 
    
    Saving Persistent Data

    J2ME applications have a method to store data even after the user has terminated the application. We manage this data with theRecordStoreclass. In our application, we need to store persistent data in order to enable the player to stop the game at any given moment, exit the application, and return to the game some time later and to continue exactly from the moment it stopped. The data that we need to store includes: time left for game, coordinates of the two player icons, ball coordinates, etc. We arrange all of this data in a byte[] array, and only after that we can store it.

     
    /** * Writes all the game data into recordstore * @param rec */ public void writeRMS(byte[] rec) { try { rs = RecordStore.openRecordStore("pocket", true); if (rs.getNumRecords() > 0) rs.setRecord(1, rec, 0, 31); else rs.addRecord(rec, 0, 31); rs.closeRecordStore(); } catch (Exception e) {} } /** * Reads the data from the recordstore * @return */ public byte[] readRMS() { byte[] rec = new byte[31]; try { rs = RecordStore.openRecordStore("pocket", true); rec = rs.getRecord(1); rs.closeRecordStore(); } catch (Exception e) {} return rec; } /** * * Deletes all the record stores */ public void deleteRMS() { if (RecordStore.listRecordStores() != null) { try { RecordStore.deleteRecordStore ("pocket"); } catch (Exception e) {} } } 
    

    Other Screens

    Two other screen in our application areInstructionsForm (shown in Figure 9) andAboutForm. We extend these two classes from theForm class and also implementCommandListener in order to handle key presses.Form class is part of the "high-level" API, and allows us to easily insert plain text, images, and other items to be displayed.

    Figure 9
    Figure 9. Instruction screen

     
    public class InstructionsForm extends Form implements CommandListener { private TestMidletMIDlet parent; private Command mainMenu = new Command("Back", Command.BACK, 1); public InstructionsForm(TestMidletMIDlet parent) { super("Instructions"); addCommand(mainMenu); setCommandListener(this); // insert some text to be seen. this.append("The objective of this game is to shoot as many baskets as possible while preventing your opponent shoot to your basket.\n\n"); this.append("move your player using 4 for moving left, 2 for moving up, 6 for moving right and 8 for moving down.\n\n"); this.append("press 5 to throw the ball"); this.parent = parent; } public void commandAction(Command c, Displayable d) { if (c == mainMenu) { parent.setCurrent("MainMenu2"); } } } 
    

    Conclusion

    In this article, we have discussed some of the most common features in J2ME environment. These features include theMIDlet class, which is the base class for all J2ME applications, the low-level API's Canvas class, and high-level API classes such as List andForm. We also covered the organizational structure of the game, such as the typical screens of the application.

    As I mentioned before, this is a brief description of a typical J2ME game. Although games are abundant in the handheld environment, there are many other uses for ME applications, such as stock quote readers, RSS readers, etc.

    Resources

      
    http://today.java.net/im/a.gif