JAVA ME

Basics & Game Programming

Latest version tested: Sun Java Wireless Toolkit 2.5.2 for CLDC,
NetBeans 6.1 Mobility Pack, CLDC 1.1, MIDP 2.1
Last modified: October 13, 2008

You can develop Java ME program using either:

For serious programming, I certainly recommend NetBeans with Mobility Pack.

There are quite few Java ME books, but tons of on-line learning resources. These are the steps that I recommend:

Install Sun Java Wireless Toolkit

Step 1: Download
Download Sun Java Wireless Toolkit for CLDC (formerly called J2ME wireless toolkit) from http://java.sun.com/products/sjwtoolkit/.

Step 2: Install
Run the downloaded installer.

Step 3: Read Documentation
Read the installed documentation via "index.html" (in the installed directory). Read "Sun Java Wireless Toolkit for CLDC User's Guide".

Step 4: Try the Demos
"Start" button ⇒ Programs ⇒ Sun Java Wireless Toolkit 2.5.2 ⇒ Open Project... ⇒ choose a demo ⇒ Build ⇒ Run.

Writing First Java ME Program

Step 0: Read Documentation
Read "Sun Java Wireless Toolkit for CLDC User's Guide" chapters 1 and 2 (via "index.html" in the installed directory).

Step 1: Create a New Project
Launch wireless toolkit via "Start" button ⇒ Programs ⇒ Sun Java Wireless Toolkit 2.5.2 ⇒ new Project... ⇒ In "Project Name", enter "Hello". In MIDlet Class Name, enter "HelloMIDlet" ⇒ Create Project ⇒ OK ⇒ Take note of the source file directory, e.g., "c:\WTK2.5.2\apps\Hello\src".

Note: You can change the project directory by editing "$WTK_BASE\wtklib\Windows\ktools.properties" to add the following property:

kvem.apps.dir = your-new-app-directory

Step 2: Write your Source Code
Write the following source code, using a programming text editor, and saved as "HelloMIDlet.java" in the source file directory noted in the previous step.

// #1: Uses Java ME APIs (instead of Java SE)
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
// #2: First MIDlet to say "Hello, world"
public class HelloMIDlet extends MIDlet implements CommandListener {
 
   // #3: Declare instance variables
   private Form mainForm;           // for holding UI components
   private Display currentDisplay;  // the current display screen
   private Command cmdExit;         // exit command
 
   // #4: Called back by the Runtime to start or resume (after being paused) the MIDlet
   public void startApp() {
      mainForm = new Form("HelloMIDlet");   // allocate a Form to hold the UI components
      mainForm.append(new StringItem(null, "Hello, world!"));  // add a String component
      cmdExit = new Command("Exit", Command.EXIT, 0);  // allocate the exit command
      mainForm.addCommand(cmdExit);         // add the command
      mainForm.setCommandListener(this);    // register "this" to handle command
      currentDisplay = Display.getDisplay(this);  // retrieve the current display
      currentDisplay.setCurrent(mainForm);  // use mainForm as the current display
   }
 
   // #5: Called back by the Runtime to pause the MIDlet
   public void pauseApp() {}  // do nothing
 
   // #6: Called back by the Runtime before the MIDlet is destroyed
   public void destroyApp(boolean unconditional) {}  // do nothing
 
   // #7: Handler for the Exit command
   public void commandAction(Command cmd, Displayable displayable) {
      if (cmd == cmdExit) {
         destroyApp(true);
         notifyDestroyed();  // put the midlet into destroy state
      }
   }
}

Explanation:

Step 3: Build & Run the Program
Build ⇒ Run ⇒ Launch ⇒ Exit.

Using the NetBeans Mobility Pack

Step 0: Install NetBeans
Install NetBeans. Read "NetBeans HowTo" on how to install NetBeans and write program in NetBeans.

Step 1: Install Mobility Pack
Download NetBeans Mobility Pack CLDC/MIDP from http://www.netbeans.org/products/mobility/. Run the downloaded installer.

Step 2: Read & Read

Step 3: Write a "Hello-world" Program

refer to the getting starting document and the source code above.

Step 4: Visual Programming

Compared with Sun Java Wireless Toolkit, NetBeans provides a GUI Builder (called Visual Mobile Designer) in addition to source coding. Follow the steps in "Quick Start Guide" to use the Visual Mobile Designer to write a Hello-world Java ME applications.

Next...

Read:

Java ME, is easier than the Java SE and Java EE, simply because the APIs is very much smaller (because mobile devices has limited capability). For example, the java.lang in Java ME (CLDC 1.1) has 17 classes and 1 interface, compared with the java.lang of Java SE (JDK 1.6), which has 35 classes and 8 interfaces.

Java ME's Configurations, Profiles & Optional APIs

Unlike Java SE, which is a single piece of software identical in all the operating platforms for developing desktop applications, Java ME is designed for mobile devices, which have limited and diverse computational power, memory, screen size, and networking capability. Due to the large variety and diversity of mobile devices, it does not make sense to design a one-size-fits-all solution and request all variety of mobile devices to support this piece of software.

Java ME is, therefore, divided into configurations, profiles, and optional APIs. A mobile device manufacturer is free to choose what to support, depending on the device's capability.

A configuration specifies a Java Virtual Machine (JVM) and a base set of core APIs for a certain class of devices. There are currently two configurations defined:

Configuration (CLDC and CDC), by itself, is incomplete for application development. A profile is defined on top of a configuration to provide specific APIs (such as user interface, persistent storage) to make a complete application environment for developing applications. For example, MIDP (Mobile Information Device Profile) is build on top of CLDC; PDAP (PDA Profile) is build on top of CDC.

Mobile devices may choose to support optional APIs, such as Blue tooth API, Wireless Messaging API (WMA 1.0 or 2.0) and many others, depending on their capability.

Configurations and profiles are confusing. As a starter, you need not worry about how to distinguish them. You simply need to understand the combined APIs provided by the "MIDP for CLDC".

Reference: "Introduction to Mobility Java Technology" @ http://developers.sun.com/mobility/getstart/.

MIDP for CLDC

In this writing, we focus on the "MIDP for CLDC". Recall that CLDC is the base, and MIDP builds on top of CLDC to provide a complete application development environment. Currently, there are two versions of CLDC: 1.0 and 1.1 and three versions of MIDP: 1.0, 2.0, 2.1.

CLDC defines the JVM and a base set of core APIs for a class of devices with intermittent connectivity and limited capacity. CLDC specifies core APIs such as java.lang, java.io, java.util:

If you are familiar with Java SE (JDK), you can see that CLDC provides only a small subset of classes, as the mobile devices has limited capability.

CLDC does not provide a complete application development environment, e.g., it lacks the user interface. MIDP is built upon CLDC to provide a complete environment. MIDP specifies these APIs:

Midlet's Life Cycle

A MIDP program is written by subclassing javax.microedition.midlet.MIDlet, and hence is called a midlet. A midlet is run and under the control of the Application Management Software (AMS or the runtime). The AMS can manage multiple midlets at the same time, and needs to content with other applications (such as SMS, MMS) for the limited system resources available in the mobile device.

A midlet extends abstract class MIDlet, which declares three abstract methods for managing its life cycle: startApp(), pauseAPP() and destroyApp(), and other non-abstract methods such as notifyPaused() and notifyDestroy().

A midlet could be in one of these states: constructed, paused, active, and destroyed. The life cycle of a midlet begins when the AMS invokes the constructor to create a new instance of the midlet. After it is constructed, the AMS places the midlet in the paused state. It then invokes Midlet::startApp() to start the midlet and place the midlet in the active state. The AMS may shift the midlet into the paused state by invoking Midlet::pauseApp() (e.g., there is a incoming call or MMS message which requires attention and system resources) and resume its operation by calling startApp() again. In the paused state, the midlet is not terminated, but it should release any resource it obtained in startApp(). The pause/active transition give you the ability to manage resources effectively. Typically, you shall use startApp() to allocate resources such as record stores, network connections, UI components, and use pauseApp() to release these resources. The AMS can put the midlet into the destroyed state from either paused or active state, via a call to destroyApp().

A midlet may voluntarily enter the paused state by invoking Midlet::notifyPaused(). Similarly, it could invoke Midlet::notifyDestroyed() to inform the AMS that it can now be considered destroyed.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
// Testing the Life Cycle of a MIDlet
public class LifeCycleTest extends MIDlet implements CommandListener {
 
   private Form mainForm;           // for holding UI components
   private Display currentDisplay;  // the current display screen
   private Command cmdExit;         // exit command
   private StringItem msg;          String UI Component
 
   // Constructor
   public LifeCycleTest() {
      System.out.println("Constructor is called");
      mainForm = new Form("Midlet Lifecycle Test");
      msg = new StringItem(null, "Constructing Midlet...");
      mainForm.append(msg);
      cmdExit = new Command("Exit", Command.EXIT, 0);
      mainForm.addCommand(cmdExit);
      mainForm.setCommandListener(this);
      currentDisplay = Display.getDisplay(this);
      currentDisplay.setCurrent(mainForm);
   }
 
   public void startApp() {
      System.out.println("startApp() is called");
      msg.setText("Midlet started...");
   }
 
   public void pauseApp() {
      System.out.println("pauseApp() is called");
      msg.setText("Midlet paused...");
   }
 
   public void destroyApp(boolean unconditional) {
      System.out.println("destroyApp() is called");
      msg.setText("Midlet destroyed...");
   }
 
   public void commandAction(Command cmd, Displayable displayable) {
      if (cmd == cmdExit) {
         //destroyApp(true);  // Uncomment and try this
         notifyDestroyed();
      }
   }
}

Try the above midlet. Start the midlet and use the "Exit" command as well as "Hang-up" button to terminate the midlet.

Reference: "MIDlet Life Cycle" @ http://developers.sun.com/mobility/learn/midp/lifecycle/.

MIDP Basics

HTTP Connection

import java.io.*;
import javax.microedition.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
// Testing HTTP Connection by connecting to a web page and retrieve
// BUFFER_SIZE bytes of data.
public class HTTPConnectionTest extends MIDlet implements CommandListener {
 
   private Form mainForm;
   private StringItem msgItem;
   private Command cmdExit,  cmdConnect;
   private Display currentDisplay;
   private HttpConnection conn;
   private InputStream in;
   private byte[] buffer;
   private static final int BUFFER_SIZE = 512;
   private String url = "http://java.sun.com";
 
   // Constructor
   public HTTPConnectionTest() {
      mainForm = new Form("HTTP Connector Test");
      msgItem = new StringItem(null, "");
      mainForm.append(msgItem);
      cmdExit = new Command("Exit", Command.EXIT, 0);
      cmdConnect = new Command("Connect", Command.SCREEN, 0);
      mainForm.addCommand(cmdExit);
      mainForm.addCommand(cmdConnect);
      mainForm.setCommandListener(this);
   }
 
   public void startApp() {
      currentDisplay = Display.getDisplay(this);
      currentDisplay.setCurrent(mainForm);
   }
 
   public void pauseApp() {}
 
   public void destroyApp(boolean unconditional) {}
 
   public void commandAction(Command cmd, Displayable displayable) {
      if (cmd == cmdExit) {
         notifyDestroyed();
      } else if (cmd == cmdConnect) {
         // Start a new thread for network connection (as IO operation blocks)
         Thread t = new Thread() {
            public void run() {
               connect();
               fillBuffer();
            }
         };
         t.start();
      }
   }
 
   // Establish a HTTP connection
   private void connect() {
      try {
         conn = (HttpConnection) Connector.open(url, Connector.READ);
         if (conn.getResponseCode() == HttpConnection.HTTP_OK) {
            in = conn.openInputStream();     // open the network input stream
            buffer = new byte[BUFFER_SIZE];  // allocate the byte buffer for bytes read
         }
      } catch (IOException ex) {
         ex.printStackTrace();
      }
   }
 
   // Returns false if the buffer is not filled to capacity (indicating last segment)
   private boolean fillBuffer() {
      int totalBytesRead = 0;
      try {
         while (totalBytesRead < BUFFER_SIZE) {
            int bytesRead = in.read(buffer, totalBytesRead, BUFFER_SIZE - totalBytesRead);
            if (bytesRead < 0) {  // end of stream
               in.close();
               break;
            }
            totalBytesRead += bytesRead;
         }
      } catch (IOException ex) {
         ex.printStackTrace();
      }
      msgItem.setText(new String(buffer, 0, totalBytesRead));  // show the bytes read
      return (totalBytesRead == BUFFER_SIZE);
   }
}

Explanation:

[TODO] A "Continue" command to browse thru the entire site. Parsing HTTP response message.

Persistent with Record Management System (RMS)

[TODO]

MIDP User Interface

MIDP provides 3 sets of UI classes:

The Display class provides the foundation for visual interaction with a midlet. There is only one instance of Display, which can be retrieved via static method Display.getDisplay(). You can then use Display::setCurrent() and Display::getCurrent() to get/set the current display for a particular Displayable.

Display currentDisplay = Display.getDisplay(this);  // "this" is a MIDlet
Form mainForm = new Form(...);
currentDisplay.setCurrent(mainForm);  // set the current display to a Form (Displayable)

High-Level UI: Displayable, Item, Command and CommandListener

The Displayable is an abstract class of objects that can be placed on the Display. A Displayable may have a title, a ticker, command(s) and an associated command listener. The following methods are provided:

// Get the height and width of the Displayable
public int getHeight();
public int getWidth();
// Getter/Setter for Title and Ticker
public String getTitle();
public void setTitle(String title);
public Ticker getTicker();
public void setTicker(Ticker ticker);
// Add/Remove Command and register command listener
public void addCommand(Command cmd);
public void removeCommand(Command cmd);
public void setCommandListener(CommandListener listener);

Command objects can be associated with the soft buttons (two or three). The constructor of Command take three arguments:

public Command(String label, int type, int priority);

The CommandListener interface declares one abstract method:

public void commandAction(Command c, Displayable d); // indicate a Command event has occurred on Displayable d

In your commandAction() handler, you can either check for the Command object, command type, or Displayable, and program appropriate action.

// Command definitions
Command cmdExit = new Command("Exit", Command.EXIT, 1);
Command cmdContinue = new Command("Continue", Command.SCREEN, 1);
......
 
// Command event handler
public void commandAction(Command cmd, Displayable d) {
   // check for the Command object
   if (cmd == cmdExit) {
      ...
   } else if (cmd == cmdContinue) {
      ...
   }
   // or check for the Command's type
   int type = cmd.getCommandType();
   if (type == Command.EXIT) {
      ...
   } else if (type == Command.SCREEN) {
      ...
   }
}

Code Example: TextBoxes

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
public class TextBoxTest extends MIDlet implements CommandListener {
 
   private Display currentDisplay;
   private TextBox tbox1;
   private TextBox tbox2;
   private Command cmdExit;
   private Command cmdNext;
 
   public void startApp() {
      // Allocate Displayable UI components
      tbox1 = new TextBox("Page 1", "This is page number 1", 40, TextField.ANY);
      tbox2 = new TextBox("Page 2", "This is page number 2", 40, TextField.ANY);
      // Allocate Commands
      cmdExit = new Command("Exit", Command.EXIT, 2);
      cmdNext = new Command("Next Page", Command.SCREEN, 1);
      // Associate Command with appropriate Displayable UI component
      tbox1.addCommand(cmdNext);
      tbox2.addCommand(cmdExit);
      // Register command listener for the Displayables
      tbox1.setCommandListener(this);
      tbox2.setCommandListener(this);
      // Retrieve and setup the current Display
      currentDisplay = Display.getDisplay(this);
      currentDisplay.setCurrent(tbox1);      
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   public void commandAction(Command cmd, Displayable displayable) {
      if (cmd == cmdNext) {
         // Change current display to the next page
         currentDisplay.setCurrent(tbox2);
      } else if (cmd == cmdExit) {
         destroyApp(true);
         notifyDestroyed();
      }
   }
}

Alert

Alert can be used for providing "dialog". There are two types of Alert: one display for a set period of time and does not require user's interaction; the other interrupt the program until user provides a response. Alert is a Displayable. Hence, it can have title, ticker, command(s) and can register command listener. Alert has the following constructors:

public Alert(String title);     // construct an empty Alert with the given title
public Alert(String title, String alertText, Image alertImage, AlertType type);

The available AlertTypes are: ALARM, CONFIRMATION, ERROR, INFO, WARNING. You can use methods getTimeout() and setTimeout() to control the timeout (in milliseconds or FOREVER). Alert can be associated with an image or a sound effect.

Code Example: Guessing a number

import java.util.Random;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
// Guess a number between 1 to 99
public class NumberGuess extends MIDlet implements CommandListener {
 
   private TextBox textBox;
   private Command cmdExit,  cmdTry;
   private Alert alert;
   private Random rand = new Random();
   private int secretNumber;
   private int numTrial = 0;
   private String msgPrompt = "Enter your guess (between 1 to 99): ";
 
   // Constructor
   public NumberGuess() {
      textBox = new TextBox("Number Guess", msgPrompt, 60, TextField.PLAIN);
      cmdExit = new Command("Exit", Command.EXIT, 2);
      cmdTry = new Command("Try", Command.SCREEN, 1);
      textBox.addCommand(cmdTry);
      textBox.addCommand(cmdExit);
      textBox.setCommandListener(this);
      alert = new Alert("Result", "", null, AlertType.CONFIRMATION);
      // Setup a random secret number between 1 and 99
      secretNumber = rand.nextInt(100);
   }
 
   public void startApp() {
      Display.getDisplay(this).setCurrent(textBox);
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // Handler for command event
   public void commandAction(Command command, Displayable displayable) {
      if (command == cmdTry) {
         textBox.delete(0, msgPrompt.length());  // remove prompting text
         alert.setString(checkNumber(textBox.getString())); // display alert on result
         Display.getDisplay(this).setCurrent(alert);
         textBox.setString(msgPrompt);
      } else if (command == cmdExit) {
         destroyApp(true);
         notifyDestroyed();
      }
   }
 
   // Check if the guess is correct, and display the appropriate alert
   private String checkNumber(String strNumberIn) {
      int numberIn = Integer.parseInt(strNumberIn);
      numTrial++;
      if (numberIn == secretNumber) {
         // Regenerate secret number for the next game
         int numTrialSaved = numTrial;
         numTrial = 0;
         secretNumber = rand.nextInt(100);
         return "Congratulation\nYou got it in " + numTrialSaved + " trials";
      } else if (numberIn > secretNumber) {
         return "Try Lower";
      } else {
         return "Try Higher";
      }
   }
}

Code Example: Form (TextField, StringItem) with Items and ItemStateListener

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
// A Form with Items (TextField, StringItem) for simple addition and multiplication
public class FormTextFieldTest extends MIDlet implements CommandListener, ItemStateListener {
 
   private Form mainForm;
   private TextField tfNum1, tfNum2, tfOper, tfResult;
   private StringItem msg;
   private double num1, num2, result;
   private String oper;
   private Command cmdExit, cmdGo;
    
   public FormTextFieldTest() {
      // Setup the Form and Items
      mainForm = new Form("Form and TextField Test");
      tfNum1 = new TextField("Number 1:", "", 10, TextField.DECIMAL);
      tfNum2 = new TextField("Number 2:", "", 10, TextField.DECIMAL);
      tfOper = new TextField("Operation:", "", 1, TextField.ANY);
      tfResult = new TextField("Result: ", "", 10, TextField.DECIMAL);
      msg = new StringItem("", "Type the numbers, set operation to 'A' for addition " +
              "or 'M' for multiplication, then push 'Go'.");
      mainForm.append(tfNum1);
      mainForm.append(tfNum2);
      mainForm.append(tfOper);
      mainForm.append(tfResult);
      mainForm.append(msg);
 
      // Setup Commands, register command listener and item-state listener
      cmdExit = new Command("Exit", Command.EXIT, 2);
      cmdGo = new Command("GO", Command.SCREEN, 1);
      mainForm.addCommand(cmdExit);
      mainForm.addCommand(cmdGo);
      mainForm.setCommandListener(this);
      mainForm.setItemStateListener(this);
   }
 
   public void startApp() {
      Display.getDisplay(this).setCurrent(mainForm);
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // Handler for command event
   public void commandAction(Command command, Displayable displayable) {
      if (command == cmdGo) {
         if (oper.equalsIgnoreCase("A")) {
            result = num1 + num2;
         } else if (oper.equalsIgnoreCase("M")) {
            result = num1 * num2;
         }
         tfResult.setString(result + "");
      } else if (command == cmdExit) {
         destroyApp(true);
         notifyDestroyed();
      }
   }
 
   // Handler for item-state change event
   public void itemStateChanged(Item item) {
      if (item == tfNum1) {
         num1 = Double.parseDouble(tfNum1.getString());
      }
      if (item == tfNum2) {
         num2 = Double.parseDouble(tfNum2.getString());
      }
      if (item == tfOper) {
         oper = tfOper.getString();
      }
   }
}

Low-level UI for Custom Graphics: Canvas and Graphics

[TODO]

MIDP Game API

MIDP Game API (in package javax.microedition.lcdui.game) enhances the graphics capabilities (of the MIDP UI package javax.microedition.lcdui) to better support game development. It provides five new classes: GameCanvas, Sprite, Layer, TiledLayer, and LayerManager, as shown below:

GameCanvas

GameCanvas extends Canvas, and adds:

  1. off-screen graphics buffer for double-buffered rendering. You can retrieve the off-screen graphics handler via method GameCanvas::getGraphics() and transfer the contents to the on-screen buffer via GameCanvas::flushGraphics().
  2. the ability to query key status, via method GameCanvas::getKeyState().

Code Example: A key-controlled moving avatar built upon GameCanvas

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
// Testing the MIDP Game API's GameCanvas class: off-screen graphics buffer and key state.
public class GameCanvasTest extends MIDlet {
 
   // Allocate a GameCanvas, set it to the current display, and start the game thread
   public void startApp() {
      GameMain game = new GameMain(); // GameMain extends GameCanvas for the game UI
      Display.getDisplay(this).setCurrent(game);
      new Thread(game).start();       // GameMain implements Runnable to run the game thread
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // The GameMain class is designed as an inner class, which extends GameCanvas for drawing
   // the game graphics, and implements Runnable to run the game logic in its own thread.
   class GameMain extends GameCanvas implements Runnable {
 
      // Avatar - a Pacman
      private int xCenter,  yCenter;   // (x,y) position of the pacman's center
      private static final int RADIUS = 20;  // radius
      private int speed = 3;      // speed of move, in pixels
      private int xEye,  yEye;    // (x,y) position of the pacman's eye
      private int direction = 0;  // direction of move, in degrees counter-clockwise
      private int animationCount = 0;        // counter to control avatar's animation
 
      private static final int INFO_AREA_HEIGHT = 20;  // height of the info display area
      private static final int UPDATE_INTERVAL = 100;  // game update interval in milliseconds
 
      // Constructor
      public GameMain() {
         super(true);
      }
 
      // Running behavior of the game thread
      public void run() {
         int canvasWidth = getWidth();
         int canvasHeight = getHeight();
 
         // Initialize the Pacman
         xCenter = canvasWidth / 2;    // place the pacman in the center of canvas
         yCenter = canvasHeight / 2;
         direction = 0;          // move right
         xEye = xCenter;
         yEye = yCenter - RADIUS / 2;
         animationCount = 0;
         // bounds for the pacman's center (x, y)
         int xMin = RADIUS;
         int xMax = canvasWidth - 1 - RADIUS;
         int yMin = RADIUS + INFO_AREA_HEIGHT;
         int yMax = canvasHeight - 1 - RADIUS;
 
         // Retrieve the off-screen graphics buffer for graphics drawing
         Graphics g = getGraphics();
 
         // Game loop
         while (true) {
            // Check key state for user input
            int keyState = getKeyStates();
            if ((keyState & RIGHT_PRESSED) != 0) {
               xCenter += speed;
               if (xCenter > xMax) {
                  xCenter = xMax;
               }
               direction = 0;
               xEye = xCenter;
               yEye = yCenter - RADIUS / 2;
            } else if ((keyState & UP_PRESSED) != 0) {
               yCenter -= speed;
               if (yCenter < yMin) {
                  yCenter = yMin;
               }
               direction = 90;  // degrees counter-clockwise
               xEye = xCenter - RADIUS / 2;
               yEye = yCenter;
            } else if ((keyState & LEFT_PRESSED) != 0) {
               xCenter -= speed;
               if (xCenter < xMin) {
                  xCenter = xMin;
               }
               direction = 180;
               xEye = xCenter;
               yEye = yCenter - RADIUS / 2;
            } else if ((keyState & DOWN_PRESSED) != 0) {
               yCenter += speed;
               if (yCenter > yMax) {
                  yCenter = yMax;
               }
               direction = 270;
               xEye = xCenter + RADIUS / 2;
               yEye = yCenter;
            }
 
            // Clear screen by filling a rectangle over the entire screen
            g.setColor(0x007fcf);
            g.fillRect(0, 0, canvasWidth, canvasHeight);
 
            // Draw the Pacman
            // Circular body with a open mouth, width of the mouth varies based on animation count.
            g.setColor(0xffff00);
            g.fillArc(xCenter - RADIUS, yCenter - RADIUS, 2 * RADIUS, 2 * RADIUS,
                    5 * animationCount + direction, 360 - 10 * animationCount);
            animationCount = (animationCount + 1) % 6;   // 6 different opening
            // Draw the Pacman's eye, which alternately opens and closes
            g.setColor(0x000000);
            g.drawArc(xEye, yEye, 4, 4, 0, 360);     // outline
            if (animationCount < 3) {
               g.fillArc(xEye, yEye, 4, 4, 0, 360);  // close via fill
            }
 
            // Display information in the info area: (x, y) of the Pacman
            g.setColor(0xffffff);
            g.fillRect(0, 0, canvasWidth, INFO_AREA_HEIGHT);
            g.setColor(0x000000);
            g.drawString("(" + xCenter + "," + yCenter + ")", 4, 0, Graphics.TOP | Graphics.LEFT);
 
            // Flush the off-screen buffer to the display
            flushGraphics();
 
            // Provide delay to achieve the targeted refresh rate,
            // also yield for other threads to perform their tasks.
            try {
               Thread.sleep(UPDATE_INTERVAL);
            } catch (InterruptedException e) { }
         }
      }
   }
}

Explanation

Code Example: A Bouncing Ball

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
// A Bouncing Ball in MIDP
public class BouncingBall extends MIDlet {
 
   // Allocate a GameCanvas, set it to the current display, and start the game thread
   public void startApp() {
      GameMain game = new GameMain(); // GameMain extends GameCanvas for the game UI
      Display.getDisplay(this).setCurrent(game);
      new Thread(game).start();       // GameMain implements Runnable to run the game thread
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // The GameMain class is designed as an inner class, which extends GameCanvas for drawing
   // the game graphics, and implements Runnable to run the game logic in its own thread.
   class GameMain extends GameCanvas implements Runnable {
 
      // Avatar - the Bouncing Ball
      private int xCenter,  yCenter;        // (x,y) of the center of the ball
      private int speedX = 2,  speedY = 3;  // speed in x and y direction
      private static final int RADIUS = 80; // radius
 
      private static final int INFO_AREA_HEIGHT = 20;  // height of the info display area
      private static final int UPDATE_INTERVAL = 30;  // game update interval in milliseconds
 
      // Constructor
      public GameMain() {
         super(true);
      }
 
      // Running behavior of the game thread
      public void run() {
         int canvasWidth = getWidth();
         int canvasHeight = getHeight();
 
         // Initializes the Ball
         xCenter = canvasWidth / 2;   // place the ball in the center of the canvas
         yCenter = canvasHeight / 2;
 
         // Bounds of the center of the ball
         int xMin = RADIUS;
         int yMin = RADIUS + INFO_AREA_HEIGHT;
         int xMax = canvasWidth - 1 - RADIUS;
         int yMax = canvasHeight - 1 - RADIUS;
 
         // Retrieve the off-screen graphics buffer for graphics drawing
         Graphics g = getGraphics();
 
         // Game loop
         while (true) {
            // Update the ball's position
            xCenter += speedX;
            yCenter += speedY;
            // Check if the ball hit the bound. 'Reflect' the ball if so.
            if (xCenter > xMax) {
               xCenter = xMax;
               speedX = -speedX;
            } else if (xCenter < xMin) {
               xCenter = xMin;
               speedX = -speedX;
            }
            if (yCenter > yMax) {
               yCenter = yMax;
               speedY = -speedY;
            } else if (yCenter < yMin) {
               yCenter = yMin;
               speedY = -speedY;
            }
 
            // Clear screen by filling a rectangle over the entire screen
            g.setColor(0x000000);
            g.fillRect(0, 0, canvasWidth, canvasHeight);
 
            // Draw the ball
            g.setColor(0xffffff);
            g.fillArc(xCenter - RADIUS, yCenter - RADIUS, 2 * RADIUS, 2 * RADIUS, 0, 360);
 
            // Display information in the infor area: (x, y) of the ball
            g.setColor(0xffffff);
            g.fillRect(0, 0, canvasWidth, INFO_AREA_HEIGHT);
            g.setColor(0x000000);
            g.drawString("(" + xCenter + "," + yCenter + ")", 4, 0, Graphics.TOP | Graphics.LEFT);
 
            // Flush the off-screen buffer to the display
            flushGraphics();
 
            // Provide delay to achieve the targeted refresh rate,
            // also yield for other threads to perform their tasks.
            try {
               Thread.sleep(UPDATE_INTERVAL);
            } catch (InterruptedException e) {
            }
         }
      }
   }
}

Explanation

This bouncing ball is straight-forward. However, I have problem writing multiple balls, because MIDP's Math class does not provide arc-tangent function.

Sprite

The Sprite class allow you to render an animated sprite with several image frames stored as one single image for better management of animated frames. Two examples of animated frame images are given below:


The Sprite class is able to extract the frames (given the frame width and height) and number them starting from 0 for the top-left frame, in row-major manner. Read the API documentation of the <code>Sprite</code> class for more illustration.

You construct a sprite as follow:


// Load the image (containing the animated frames) for the sprite
Image img = Image.createImage("/pacman.png");
// Create a Sprite from the image giving the frame's width and height
Sprite sprite = new Sprite(img, 25, 25);
// Select a frame for display
sprite.nextFrame();  // Use the next frame
sprite.setFrame(2);  // select a particular frame number

The Sprite class also provide basic transforms via the method Sprite::setTransform(type). The type includes TRANS_NONE, TRANS_ROT90, TRANS_ROT180, TRANS_ROT270, TRANS_MIRROR, TRANS_MIRROR_ROT90, TRANS_MIRROR_ROT180, TRANS_MIRROR_ROT270. For rotation, you can use setRefPixelPosition(x, y) to set the rotation center (by default, it rotates about (0, 0), which is the top-left corner of the image). Note that: (a) you cannot concatenate multiple transforms, (b) rotation of arbitrary degree in not supported, and (c) the rotational degree is measured clockwise (instead of the usual convection of counter-clockwise).

A sprite can detect collision with another sprite via the method Sprite::collidesWith(). You can set the collision detection at "pixel-level" or "bounding-box-level".

Code Example: Sprite with animated frames, transform, and collision detection

import java.io.IOException;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
// Testing the MIDP Game API's Sprite class: animation and collision detection.
public class SpriteTest extends MIDlet {
 
   // Allocate a GameCanvas, set it to the current display, and start the game thread
   public void startApp() {
      GameMain game = new GameMain(); // GameMain extends GameCanvas for the game UI
      Display.getDisplay(this).setCurrent(game);
      new Thread(game).start();       // GameMain implements Runnable to run the game thread
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // The GameMain class is designed as an inner class, which extends GameCanvas for drawing
   // the game graphics, and implements Runnable to run the game logic in its own thread.
   class GameMain extends GameCanvas implements Runnable {
 
      // Avatar - Pacman with animation
      private Sprite pacman;
      private String pacmanImageFilename = "/images/PacmanFrames.png";
      private int pacmanXCenter,  pacmanYCenter;  // (x,y) of the center of the Pacman
      private int pacmanSpeed = 3;                // speed of move, in pixels
 
      // Avatar - Ghost with animation
      private Sprite ghost;
      private String ghostImageFilename = "/images/GhostFrames.png";
      private int ghostXCenter,  ghostYCenter;
 
      private static final int FRAME_WIDTH = 25;
      private static final int FRAME_HEIGHT = 25;
      private static final int FRAME_RADIUS = FRAME_WIDTH / 2 + 1;      
       
      // Collision Detection
      private boolean hasCollided;  // flag indicating collision
 
      private static final int UPDATE_INTERVAL = 100; // milliseconds
      private static final int INFO_AREA_HEIGHT = 20;  // height of the info display area
 
      // Constructor
      public GameMain() {
         super(true);
      }
 
      // Run the game loop in its own thread
      public void run() {
         int canvasWidth = getWidth();
         int canvasHeight = getHeight();
 
         // Construct the sprites
         try {
            Image imgPacman = Image.createImage(pacmanImageFilename);
            pacman = new Sprite(imgPacman, FRAME_WIDTH, FRAME_HEIGHT);
            pacman.setRefPixelPosition(FRAME_RADIUS, FRAME_RADIUS);  // set rotation center
 
            Image imgGhost = Image.createImage(ghostImageFilename);
            ghost = new Sprite(imgGhost, FRAME_WIDTH, FRAME_HEIGHT);
         } catch (IOException e) {
            e.printStackTrace();
         }
 
         // Position pacman at the center
         pacmanXCenter = canvasWidth / 2;
         pacmanYCenter = canvasHeight / 2;
         // Position ghost at the corner
         ghostXCenter = canvasWidth / 4;
         ghostYCenter = canvasHeight / 4;
         // Pacman's bounds
         int pacmanXMin = FRAME_RADIUS;
         int pacmanXMax = canvasWidth - FRAME_RADIUS;
         int pacmanYMin = FRAME_RADIUS + INFO_AREA_HEIGHT;
         int pacmanYMax = canvasHeight - FRAME_RADIUS;
 
         // Retrieve the off-screen graphics buffer for graphics drawing
         Graphics g = getGraphics();
 
         // Game loop
         while (true) {
            // Check key state for user input
            int keyState = getKeyStates();
            if ((keyState & RIGHT_PRESSED) != 0) {
               pacmanXCenter += pacmanSpeed;
               if (pacmanXCenter > pacmanXMax) {
                  pacmanXCenter = pacmanXMax;
               }
               pacman.setTransform(Sprite.TRANS_NONE);
            } else if ((keyState & UP_PRESSED) != 0) {
               pacmanYCenter -= pacmanSpeed;
               if (pacmanYCenter < pacmanYMin) {
                  pacmanYCenter = pacmanYMin;
               }
               pacman.setTransform(Sprite.TRANS_ROT270); // clockwise
            } else if ((keyState & LEFT_PRESSED) != 0) {
               pacmanXCenter -= pacmanSpeed;
               if (pacmanXCenter < pacmanXMin) {
                  pacmanXCenter = pacmanXMin;
               }
               pacman.setTransform(Sprite.TRANS_MIRROR);
            } else if ((keyState & DOWN_PRESSED) != 0) {
               pacmanYCenter += pacmanSpeed;
               if (pacmanYCenter > pacmanYMax) {
                  pacmanYCenter = pacmanYMax;
               }
               pacman.setTransform(Sprite.TRANS_ROT90); // clockwise
            }
 
            // Clear screen by filling a rectangle over the entire screen
            g.setColor(0x007fcf);
            g.fillRect(0, 0, canvasWidth, canvasHeight);
 
            // Draw the sprites
            pacman.setPosition(pacmanXCenter - FRAME_RADIUS, pacmanYCenter - FRAME_RADIUS);
            pacman.paint(g);
            pacman.nextFrame();  // use next frame for the next refresh
 
            ghost.setPosition(ghostXCenter - FRAME_RADIUS, ghostYCenter - FRAME_RADIUS);
            ghost.paint(g);
            ghost.nextFrame();
 
            // Collision detection
            hasCollided = pacman.collidesWith(ghost, true);
 
            // Display info
            g.setColor(0xffffff);
            g.fillRect(0, 0, canvasWidth, INFO_AREA_HEIGHT);
            g.setColor(0x000000);
            g.drawString("(" + pacmanXCenter + "," + pacmanYCenter + ")", 4, 0, Graphics.TOP | Graphics.LEFT);
            if (hasCollided) {
               g.setColor(0xff0000);
               g.drawString("Collided", canvasWidth / 2, 0, Graphics.TOP | Graphics.LEFT);
            }
 
            // flush the off-screen buffer to the display
            flushGraphics();
 
            // Provide delay to achieve the targeted refresh rate,
            // also yield for other threads to perform their tasks.
            try {
               Thread.sleep(UPDATE_INTERVAL);
            } catch (InterruptedException e) {
            }
         }
      }
   }
}

Explanation

[TODO]

Layer & TiledLayer

A Layer is a rectangular visual panel with a width and height, and can be position on the screen with its top-left corner at (x, y) of the screen (via method setPosition(x, y)). Layer is an abstract class. Sprite (described earlier) and TiledLayer are implementations of the Layer class.

A TiledLayer is a special layer that is divided into grid of cells. Each cells can be filled with a selected tile frame. In this way, you can reuse the tile frames instead of providing a huge image. TiledLayer is often used for background.

To use a TiledLayer, you keep the tile frames in a single image (similar to sprite) and specify the width and height of the tile frame. The frames will be extracted and numbered starting from 1 (unlike sprite frame which starts from 0) for the top-left tile, in row-major manner. You then divide the tile layer into cells (of rows and columns). For each cell, you can assign a tile frame index (from 1 to n). Index 0 indicates that the cell is empty. You can use negative index for animated tile cells. Each negative index is associated with a positive tile frame index, and the associated tile frame can be changed on the fly. For example, the animated tile index -1 can be made alternate between tile frame index 5 and 6. Read the API documentation of TiledLayer for more illustrations.

You can construct a TiledLayer as follow:


// Load the image (containing the tile frames) for the TiledLayer
Image img = Image.createImage("/pacman.png");
// Compute the number of rows and columns of tiles
int tileRows = ....
int tileCols = ....
// Create a TiledLayer from the image giving the numbers of rows and columns, tile frame's width and height
TiledLayer background = new TiledLayer(tileCols, tileRows, img, tileWidth, tileHeight);
// Select a static tile (from 1 to n) for a particular cell
background.setCell(aCol, aRow, aStaticTileIndex);
// Set up animated tile by retrieving the next available animated tile index (starting from -1)
int animatedTileIndex = background.createAnimatedTile(5);  // init to static tile index 5
background.setCell(aCol, aRow, animatedTileIndex);
// Animate tile by changing its associated static tile index
background.setAnimatedTile(animatedTileIndex, 7);

Code Example: TiledLayer with static and animated tiles

import java.io.IOException;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
// Testing the MIDP Game API's TiledLayer class: static and animated tiles.
public class TiledLayerTest extends MIDlet {
 
   // Allocate a GameCanvas, set it to the current display, and start the game thread
   public void startApp() {
      GameMain game = new GameMain(); // GameMain extends GameCanvas for the game UI
      Display.getDisplay(this).setCurrent(game);
      new Thread(game).start();       // GameMain implements Runnable to run the game thread
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // The GameMain class is designed as an inner class, which extends GameCanvas for drawing
   // the game graphics, and implements Runnable to run the game logic in its own thread.
   class GameMain extends GameCanvas implements Runnable {
 
      // Use a TiledLayer for the background
      private TiledLayer background;
      private String imgTilesFilename = "/images/Tiles.png";
      private static final int TILE_WIDTH = 32;
      private static final int TILE_HEIGHT = 32;
      private int tileRows;  // number of rows of tiles
      private int tileCols;  // number of columns of tiles
      private int animatedTileIndex1, animatedTileIndex2;
      private int frameCount = 0;  // for controlling animated tile
 
      private static final int UPDATE_INTERVAL = 100;  // game update interval in milliseconds
 
      // Constructor
      public GameMain() {
         super(true);
      }
 
      // Running behavior of the game thread
      public void run() {
         int canvasWidth = getWidth();
         int canvasHeight = getHeight();
 
         // Compute the number of rows and columns of of background tiled layer
         tileCols = canvasWidth / TILE_WIDTH;
         tileRows = canvasHeight / TILE_HEIGHT;
         // Top-left corner (x,y) of the tiled layer
         int tiledLayerX = (canvasWidth - tileCols * TILE_WIDTH) / 2;
         int tiledLayerY = (canvasHeight - tileRows * TILE_HEIGHT) / 2;
 
         // Set up the TiledLayer and place the chosen tile on each cell
         try {
            Image imgTiles = Image.createImage(imgTilesFilename);
            background = new TiledLayer(tileCols, tileRows, imgTiles, TILE_WIDTH, TILE_HEIGHT);
            // Retrieve the next available animated tile index (negative number)
            animatedTileIndex1 = background.createAnimatedTile(5);  // init to tile index 5
            animatedTileIndex2 = background.createAnimatedTile(7);  // init to tile index 7
            for (int col = 0; col < tileCols; col++) {
               // Top row of sky (static tile index 8)
               background.setCell(col, 0, 8);
               // Bottom two rows of water (animated tiles) 
               background.setCell(col, tileRows - 2, animatedTileIndex1);
               background.setCell(col, tileRows - 1, animatedTileIndex2);
            }
            for (int col = 1; col < tileCols - 1; col++) {
               // grassland in between (static tile index 4), sides are empty (index 0)
               for (int row = 1; row < tileRows - 2; row++) {
                  background.setCell(col, row, 4);
               }
            }
         } catch (IOException e) {
            e.printStackTrace();
         }
 
         // Retrieve the off-screen graphics buffer for graphics drawing
         Graphics g = getGraphics();
 
         // Game loop
         while (true) {
            // clear screen
            g.setColor(0xffffff);
            g.fillRect(0, 0, canvasWidth, canvasHeight);
 
            // Change the index for animated tiles
            if (frameCount == 0) {
               background.setAnimatedTile(animatedTileIndex1, 5);
               background.setAnimatedTile(animatedTileIndex2, 7);
            } else if (frameCount == 1) {
               background.setAnimatedTile(animatedTileIndex1, 6);
               background.setAnimatedTile(animatedTileIndex2, 6);
            } else {
               background.setAnimatedTile(animatedTileIndex1, 7);
               background.setAnimatedTile(animatedTileIndex2, 5);
            }
            frameCount = (frameCount + 1) % 3;
 
            // Position the TiledLayer in the center of the canvas
            background.setPosition(tiledLayerX, tiledLayerY);
            background.paint(g);
 
            // flush the off-screen buffer to the display
            flushGraphics();
 
            // Provide delay to achieve the targeted refresh rate,
            // also yield for other threads to perform their tasks.
            try {
               Thread.sleep(UPDATE_INTERVAL);
            } catch (InterruptedException e) {
            }
         }
      }
   }
}

Explanation

[TODO]

LayerManager

The LayerManager can be used to manage a collection of Layers, and place them on the screen with a specific z-order. Each Layer is given an index corresponding to its z-order, with value of 0 closer to you, and highest number further away from you. Layers can be added into the LayerManager (via insert() or append()) and removed (via remove()). Recall that Layer is an abstract class, with implementations in Sprite and TiledLayer classes.

// Construct a LayerManager
LayerManager manager = new LayerManager();
// Construct Layers (of Sprite and TiledLayer) and add into the LayerManager
Sprite sprite1 = new Sprite(...);
manager.insert(sprite1, 0);  // add this layer with z-index of 0 (nearest to you)
Sprite sprite2 = new Sprite(...);
manager.insert(sprite2, 0);  // add this layer with z-index of 0, the rest of layers adjust automatically
// more sprites
.....
TiledLayout background = new TiledLayout(...);
manager.append(background);  // append this layer to the end with highest z-index (furthest from you)
// Remove a Sprite by removing its layer - the layer numbers adjusted.
manager.remove(sprite2);

Code Example: LayerManager with Layers of Sprite and TiledLayer

import java.io.IOException;
import java.util.Random;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
// This example illustrate all the classes provided by MIDP Game API:
// GameCanvas, Sprite, TiledLayer, and LayerManager which manages Layers of Sprites 
// and TiledLayer.
public class LayerManagerTest extends MIDlet {
 
   // Allocate a GameCanvas, set it to the current display, and start the game thread
   public void startApp() {
      GameMain game = new GameMain(); // GameMain extends GameCanvas for the game UI
      Display.getDisplay(this).setCurrent(game);
      new Thread(game).start();       // GameMain implements Runnable to run the game thread
   }
 
   public void pauseApp() { }
 
   public void destroyApp(boolean unconditional) { }
 
   // The GameMain class is designed as an inner class, which extends GameCanvas for drawing
   // the game graphics, and implements Runnable to run the game logic in its own thread.
   class GameMain extends GameCanvas implements Runnable {
 
      // Sprite Layer: Pacman
      private Sprite pacman;
      private String pacmanImageFilename = "/images/PacmanFrames.png";
      private int pacmanXCenter,  pacmanYCenter;  // (x,y) of the center
      private int pacmanSpeed = 3;                // speed of move, in pixels
      private static final int PACMAN_SIZE = 25;
      private static final int PACMAN_RADIUS = PACMAN_SIZE / 2 + 1;
 
      // Sprite Layers: Ghosts
      private int numGhosts = 5;
      private Sprite[] ghosts = new Sprite[numGhosts];
      private String ghostImageFilename = "/images/GhostSmallFrames.png";
      private static final int GHOST_SIZE = 20;
      private static final int GHOST_RADIUS = GHOST_SIZE / 2;
 
      // TiledLayer: background
      private TiledLayer background;
      private String tileImageFilename = "/images/Tiles.png";
      private static final int TILE_WIDTH = 32;
      private static final int TILE_HEIGHT = 32;
      private int tileRows;  // number of rows of tiles
      private int tileCols;  // number of columns of tiles
      private int animatedTileIndex;
      private int animationCount = 0;
 
      // LayerManager which manages all the layers - Sprites and TiledLayer
      private LayerManager manager;
      private boolean gameOver = false;
 
      private static final int UPDATE_INTERVAL = 100; // milliseconds
      private Random rand = new Random();
 
      // Constructor
      public GameMain() {
         super(true);
      }
 
      // Run the game loop in its own thread
      public void run() {
         int canvasWidth = getWidth();
         int canvasHeight = getHeight();
 
         // Compute the number of rows and columns of TiledLayer
         tileCols = canvasWidth / TILE_WIDTH;
         tileRows = canvasHeight / TILE_HEIGHT;
         // Compute all the bounds
         int backgroundXMin = (canvasWidth - tileCols * TILE_WIDTH) / 2;
         int backgroundYMin = (canvasHeight - tileRows * TILE_HEIGHT) / 2;
         int backgroundXMax = canvasWidth - backgroundXMin;
         int backgroundYMax = canvasHeight - backgroundYMin;
         int pacmanXMin = backgroundXMin + PACMAN_RADIUS;
         int pacmanXMax = backgroundXMax - PACMAN_RADIUS;
         int pacmanYMin = backgroundYMin + PACMAN_RADIUS;
         int pacmanYMax = backgroundYMax - PACMAN_RADIUS;
         int ghostMinX = backgroundXMin + GHOST_SIZE;
         int ghostMaxX = backgroundXMax - GHOST_SIZE;
         int ghostMinY = backgroundYMin + GHOST_SIZE;
         int ghostMaxY = backgroundYMax - GHOST_SIZE;
 
         // Setup the layer manager
         manager = new LayerManager();
 
         // Construct the Sprites & TiledLayer
         try {
            Image imgPacman = Image.createImage(pacmanImageFilename);
            pacman = new Sprite(imgPacman, PACMAN_SIZE, PACMAN_SIZE);
            pacman.setRefPixelPosition(PACMAN_RADIUS, PACMAN_RADIUS);  // set rotation center
            manager.append(pacman);            // add into the LayerManager as last layer
            pacmanXCenter = canvasWidth / 2;   // Position pacman at the center
            pacmanYCenter = canvasHeight / 2;
 
            Image imgTiles = Image.createImage(tileImageFilename);
            background = new TiledLayer(tileCols, tileRows, imgTiles, TILE_WIDTH, TILE_HEIGHT);
            animatedTileIndex = background.createAnimatedTile(5);
            for (int col = 1; col < tileCols - 1; col++) {
               for (int row = 0; row < tileRows; row++) {
                  background.setCell(col, row, 4);  // static layer index 4
               }
            }
            for (int row = 0; row < tileRows; row++) {
               background.setCell(0, row, animatedTileIndex);    // animated tile
               background.setCell(tileCols - 1, row, animatedTileIndex);
            }
            background.setPosition(backgroundXMin, backgroundYMin);
            manager.append(background);    // add into the LayerManager as last layer
 
            Image imgGhost = Image.createImage(ghostImageFilename);
            for (int i = 0; i < numGhosts; i++) {
               ghosts[i] = new Sprite(imgGhost, GHOST_SIZE, GHOST_SIZE);
               manager.insert(ghosts[i], 0);  // add into LayerManager as first layer
               // Randomly position the ghosts
               int ghostXCenter = rand.nextInt(ghostMaxX - ghostMinX) + ghostMinX;
               int ghostYCenter = rand.nextInt(ghostMaxY - ghostMinY) + ghostMinY;
               ghosts[i].setPosition(ghostXCenter - GHOST_RADIUS, ghostYCenter - GHOST_RADIUS);
            }
         } catch (IOException e) {
            e.printStackTrace();
         }
 
         // Retrieve the off-screen graphics buffer for graphics drawing
         Graphics g = getGraphics();
 
         // Game loop
         while (!gameOver) {
            // Check key state for user input
            int keyState = getKeyStates();
            if ((keyState & RIGHT_PRESSED) != 0) {
               pacmanXCenter += pacmanSpeed;
               if (pacmanXCenter > pacmanXMax) {
                  pacmanXCenter = pacmanXMax;
               }
               pacman.setTransform(Sprite.TRANS_NONE);
            } else if ((keyState & UP_PRESSED) != 0) {
               pacmanYCenter -= pacmanSpeed;
               if (pacmanYCenter < pacmanYMin) {
                  pacmanYCenter = pacmanYMin;
               }
               pacman.setTransform(Sprite.TRANS_ROT270); // clockwise
            } else if ((keyState & LEFT_PRESSED) != 0) {
               pacmanXCenter -= pacmanSpeed;
               if (pacmanXCenter < pacmanXMin) {
                  pacmanXCenter = pacmanXMin;
               }
               pacman.setTransform(Sprite.TRANS_MIRROR);
            } else if ((keyState & DOWN_PRESSED) != 0) {
               pacmanYCenter += pacmanSpeed;
               if (pacmanYCenter > pacmanYMax) {
                  pacmanYCenter = pacmanYMax;
               }
               pacman.setTransform(Sprite.TRANS_ROT90); // clockwise
            }
 
            // clear screen
            g.setColor(0x000000);
            g.fillRect(0, 0, canvasWidth, canvasHeight);
 
            // Draw the avatar
            pacman.setPosition(pacmanXCenter - PACMAN_RADIUS, pacmanYCenter - PACMAN_RADIUS);
            pacman.nextFrame();  // use next frame for the next refresh
 
            for (int i = 0; i < numGhosts; i++) {
               ghosts[i].nextFrame();
            }
       
            if (animationCount == 0) {
               background.setAnimatedTile(animatedTileIndex, 5);
            } else if (animationCount == 1) {
               background.setAnimatedTile(animatedTileIndex, 6);
            } else {
               background.setAnimatedTile(animatedTileIndex, 7);
            }
            animationCount = (animationCount + 1) % 3;
 
            // Collision detection
            int numLayer = manager.getSize();
            if (numLayer == 2) {  // exclude the background and pacman layers
               gameOver = true;
            }
            for (int layerIndex = 0; layerIndex < numLayer - 2; layerIndex++) {
               Layer layer = manager.getLayerAt(layerIndex);
               if (pacman.collidesWith((Sprite) layer, true)) {
                  manager.remove(layer);  // remove the layer of this ghost
                  break; // need to break the loop as the layer number changes
               }
            }
 
            manager.paint(g, 0, 0);  // paint all layers
            if (gameOver) {
               Font font = g.getFont();
               String msg = "GAME OVER!";
               g.setColor(0xffffff);
               int msgWidth = font.stringWidth(msg);
               int msgX = (canvasWidth - msgWidth) / 2;
               int msgY = canvasHeight / 2;
               g.fillRect(msgX - 4, msgY - 2, msgWidth + 8, 20);
               g.setColor(0xff0000);
               g.drawString(msg, msgX, msgY, Graphics.TOP | Graphics.LEFT);
            }
 
            // flush the off-screen buffer to the display
            flushGraphics();
 
            // Provide delay to achieve the targeted refresh rate,
            // also yield for other threads to perform their tasks.
            try {
               Thread.sleep(UPDATE_INTERVAL);
            } catch (InterruptedException e) {
            }
         }
      }
   }
}

Explanation

[TODO]

Complete MIDP Game Examples

[TODO]

Reference: "Getting Started With the MIDP 2.0 Game API" @ http://developers.sun.com/mobility/midp/articles/gameapi/

 

REFERENCES & RESOURCES