By Jonathan Knudsen, March 2003
[SimpleGame source code][muTank source code]
MIDP 2.0 includes a Game API that simplifies writing 2D games. The API is compact, comprising only five classes in the javax.microedition.lcdui.game
package. These five classes provide two important capabilities:
GameCanvas
class makes it possible to paint a screen and respond to input in the body of a game loop, instead of relying on the system's paint and input threads.Building a Game Loop with GameCanvas
GameCanvas
is a Canvas
with additional capabilities; it provides methods for immediate painting and for examining the state of the device keys. These new methods make it possible to enclose all of a game's functionality in a single loop, under control of a single thread. To see why this is attractive, think about how you would implement a typical game using Canvas
:
public
class
MicroTankCanvas
extends
Canvas
implements
Runnable
{
public
void
run()
{
while
(
true)
{
// Update the game state.
repaint();
// Delay one time step.
}
}
public
void
paint(Graphics g)
{
// Painting code goes here.
}
protected
void
keyPressed(
int
keyCode)
{
// Respond to key presses here.
}
}
It's not a pretty picture. The run()
method, which runs in an application thread, updates the game once each time step. Typical tasks would be to update the position of a ball or spaceship and to animate characters or vehicles. Each time through the loop, repaint()
is called to update the screen. The system delivers key events to keyPressed()
, which updates the game state appropriately.
The problem is that everything's in a different thread, and the game code is confusingly spread over three different methods. When the main animation loop in run()
calls repaint()
, there's no way of knowing exactly when the system will call paint()
. When the system calls keyPressed()
, there's no way to know what's going on with the other parts of the application. If your code in keyPressed()
is making updates to the game state at the same time the screen is being rendered in paint()
, the screen may end up looking strange. If it takes longer to render the screen than a single time step in run()
, the animation may look jerky or strange.
GameCanvas
allows you to bypass the normal painting and key-event mechanisms so that all game logic can be contained in a single loop. First, GameCanvas
allows you to access its Graphics
object directly using getGraphics()
. Any rendering on the returned Graphics
object is done in an offscreen buffer. You can then copy the buffer to the screen using flushGraphics()
, which does not return until the screen has been updated. This approach gives you finer control than calling repaint()
. The repaint()
method returns immeditately and your application has no guarantees about exactly when the system will call paint()
to update the screen.
GameCanvas
also contains a method for obtaining the current state of the device's keys, a technique called polling. Instead of waiting for the system to call keyPressed()
, you can determine immediately which keys are pressed by calling GameCanvas
's getKeyStates()
method.
A typical game loop using GameCanvas
looks like this:
public
class
MicroTankCanvas
extends
GameCanvas
implements
Runnable
{
public
void
run()
{
Graphics g
=
getGraphics();
while
(
true)
{
// Update the game state.
int
keyState
=
getKeyStates();
// Respond to key presses here.
// Painting code goes here.
flushGraphics();
// Delay one time step.
}
}
}
The following example demonstrates a basic game loop. It shows a rotating X that you can move around the screen using the arrow keys. The run()
method is extremely clean, thanks to GameCanvas
.
import
javax.microedition.lcdui.*;
import
javax.microedition.lcdui.game.*;
public
class
SimpleGameCanvas
extends
GameCanvas
implements
Runnable
{
private
volatile
boolean
mTrucking;
private
long
mFrameDelay;
private
int
mX, mY;
private
int
mState;
public
SimpleGameCanvas()
{
super(
true);
mX
=
getWidth()
/
2;
mY
=
getHeight()
/
2;
mState
=
0;
mFrameDelay
=
20;
}
public
void
start()
{
mTrucking
=
true;
Thread t
=
new
Thread(
this);
t.
start();
}
public
void
stop()
{
mTrucking
=
false;
}
public
void
run()
{
Graphics g
=
getGraphics();
while
(mTrucking
==
true)
{
tick();
input();
render(g);
try
{
Thread.
sleep(mFrameDelay);
}
catch
(InterruptedException ie)
{ stop();
}
}
}
private
void
tick()
{
mState
=
(mState
+
1)
%
20;
}
private
void
input()
{
int
keyStates
=
getKeyStates();
if
((keyStates
&
LEFT_PRESSED)
!=
0)
mX
=
Math.
max(
0, mX
-
1);
if
((keyStates
&
RIGHT_PRESSED)
!=
0)
mX
=
Math.
min(
getWidth(), mX
+
1);
if
((keyStates
&
UP_PRESSED)
!=
0)
mY
=
Math.
max(
0, mY
-
1);
if
((keyStates
&
DOWN_PRESSED)
!=
0)
mY
=
Math.
min(
getHeight(), mY
+
1);
}
private
void
render(Graphics g)
{
g.
setColor(
0xffffff);
g.
fillRect(
0,
0,
getWidth(),
getHeight());
g.
setColor(
0x0000ff);
g.
drawLine(mX, mY, mX
-
10
+
mState, mY
-
10);
g.
drawLine(mX, mY, mX
+
10, mY
-
10
+
mState);
g.
drawLine(mX, mY, mX
+
10
-
mState, mY
+
10);
g.
drawLine(mX, mY, mX
-
10, mY
+
10
-
mState);
flushGraphics();
}
}
The example code for this article includes a MIDlet that uses this canvas. Try running SimpleGameMIDlet
to see how it works. You'll see something like a starfish doing calisthenics (perhaps compensating for its missing leg).
SimpleGameMIDlet Screen Shot
Game Scenes Are Like Onions
Typical 2D action games consist of a background and various animated characters. Although you can paint this kind of scene yourself, the Game API enables you to build scenes using layers. You could make one layer a city background, and another a car. Placing the car layer on top of the background creates a complete scene. Using the car as a separate layer makes it easy to manipulate it independent of the background, and of any other layers in the scene.
The Game API provides flexible support for layers with four classes:
Layer
is the abstract parent of all layers. It defines the basic attributes of a layer, which include a position, a size, and whether or not the layer is visible. Each subclass of Layer
must define a paint()
method to render the layer on a Graphics
drawing surface. Two concrete subclasses, TiledLayer
and Sprite
, should fulfill your 2D game desires.TiledLayer
is useful for creating background images. You can use a small set of source image tiles to create large images efficiently.Sprite
is an animated layer. You supply the source frames and have full control over the animation. Sprite
also offers the ability to mirror and rotate the source frames in multiples of 90 degrees.LayerManager
is a very handy class that keeps track of all the layers in your scene. A single call to LayerManager
's paint()
method is sufficient to render all of the contained layers.Using TiledLayer
TiledLayer
is simple to understand, although it contains some deeper nuances that are not obvious at first glance. The fundamental idea is that a source image provides a set of tiles that can be arranged to form a large scene. For example, the following image is 64 x 48 pixels.
Source Image,/
This image can be divided into 12 tiles, each 16 x 16 pixels. TiledLayer
assigns each tile a number, starting with 1 in the upper left corner. The tiles in the source image are numbered as follows:
Tile Numbering
It's simple enough to create a TiledLayer
in code. You need to specify the number of columns and rows, the source image, and the size in pixels of the tiles in the source image. This fragment shows how to load the image and create a TiledLayer
.
Image image
=
Image.
createImage(
"/board.png");
TiledLayer tiledLayer
=
new
TiledLayer(
10,
10, image,
16,
16);
In the example, the new TiledLayer
has 10 columns and 10 rows. The tiles taken from the image are 16 pixels square.
The fun part is creating a scene using these tiles. To assign a tile to a cell, invoke setCell()
. You need to supply the column and row number of the cell and the tile number. For example, you could assign tile 5 to the third cell in the second row by calling setCell(2, 1, 5)
. If these parameters look wrong, please note that the tile index starts at 1, while column and row numbers start at 0. By default, all cells in a new TiledLayer
have a tile value of 0, which means they are empty.
The following excerpt shows one way to populate a TiledLayer
, using an integer array. In a real game, TiledLayer
s could be defined from resource files, which would allow more flexibility in defining backgrounds and enhancing the game with new boards or levels.
private
TiledLayer
createBoard()
{
Image image
=
null;
try
{
image
=
Image.
createImage(
"/board.png");
}
catch
(IOException ioe)
{
return
null;
}
TiledLayer tiledLayer
=
new
TiledLayer(
10,
10, image,
16,
16);
int
[] map
=
{
1,
1,
1,
1,
11,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
9,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
7,
1,
0,
0,
0,
0,
0,
1,
1,
1,
1,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
7,
11,
0,
0,
0,
0,
0,
0,
0,
7,
6,
0,
0,
0,
0,
0,
0,
0,
7,
6,
0,
0,
0
}
;
for
(
int
i
=
0; i
<
map.length; i
++
)
{
int
column
=
i
%
10;
int
row
=
(i
-
column)
/
10;
tiledLayer.
setCell(column, row, map[i]);
}
return
tiledLayer;
}
To show this TiledLayer
on the screen, you need to pass a Graphics
object to its paint()
method.
TiledLayer
also supports animated tiles, which makes it easy to move a set of cells through a sequence of tiles. For more details, see the API documentation for TiledLayer
.
Using Sprite
s for Character Animation
The other concrete Layer
provided in the Game API is Sprite
. In a way, Sprite
is the conceptual inverse of TileLayer
. While TiledLayer
uses a palette of source image tiles to create a large scene, Sprite
uses a sequence of source image frames for animation.
All you need to create a Sprite
is a source image and the size of each frame. In TiledLayer
, the source image is divided into evenly sized tiles; in Sprite
, the sub-images are called frames instead. In the following example, a source image tank.png is used to create a Sprite
with a frame size of 32 x 32 pixels.
private
MicroTankSprite
createTank()
{
Image image
=
null;
try
{
image
=
Image.
createImage(
"/tank.png");
}
catch
(IOException ioe)
{
return
null;
}
return
new
MicroTankSprite(image,
32,
32);
}
Each frame of the source image has a number, starting from 0 and counting up. (Don't get confused here; remember that tile numbers start at 1.) The Sprite
has a frame sequence that determines the order in which the frames will be shown. The default frame sequence for a new Sprite
simply starts at 0 and counts up through the available frames.
To move to the next or previous frame in the frame sequence, use Sprite
's nextFrame()
and prevFrame()
methods. These methods wrap around to the beginning or end of the frame sequence. For example, if the Sprite
is showing the last frame in its frame sequence, calling nextFrame()
will show the first frame in the frame sequence.
To specify a frame sequence that is different from the default, pass the sequence, represented as an integer array, to setFrameSequence()
.
You can jump to a particular point in the current frame sequence by calling setFrame()
. There is no way to jump to a specific frame number. You can only jump to a certain point in the frame sequence.
Frame changes only become visible the next time the Sprite
is rendered, using the paint()
method inherited from Layer
.
Sprite
can also transform source frames. Frames may be rotated by multiples of 90 degrees or mirrored, or a combination of both. Constants in the Sprite
class enumerate the possibilities. The Sprite
's current transformation can be set by passing one of these constants to setTransform()
. The following example mirrors the current frame around its vertical center and rotates it by 90 degrees:
// Sprite sprite = ...
sprite.setTransform(Sprite.TRANS_MIRROR_ROT90);
Transformations are applied so that the Sprite
's reference pixel does not move. By default, the reference pixel of a Sprite
is located at 0, 0 in the Sprite
's coordinate space, at its upper left corner. When a transformation is applied, the location of the reference pixel is also transformed. The location of the Sprite
is adjusted so that the reference pixel stays in the same place.
You can change the location of the reference pixel with the defineReferencePixel()
method. For many types of animations you will define the reference pixel to be the center of the sprite.
Finally, Sprite
provides several collidesWith()
methods for detecting collisions with other Sprite
s, TiledLayer
s, or Image
s. You can detect collisions using collision rectangles (fast but sloppy) or at the pixel level (slow but accurate). The nuances of these methods are elusive; see the API documentation for details.
The muTank Example
The muTank example demonstrates the use of TiledLayer
, Sprite
, and LayerManager
.
The important classes are MicroTankCanvas
, which contains most of the code, and MicroTankSprite
, which encapsulates the behavior of the tank.
MicroTankSprite
makes extensive use of transformations. Using a source image with only three frames, MicroTankSprite
can show the tank pointing in any of 16 different directions. Two exposed public methods, turn()
and forward()
, make the tank easy to control.
MicroTankCanvas
is a GameCanvas
subclass and contains an animation loop in run()
that should look familiar to you. The tick()
method checks to see if the tank has collided with the board. If so, its last movement is reversed using MicroTankSprite
's undo()
method. The input()
method simply checks for key presses and adjusts the direction or position of the tank accordingly. The render()
method uses a LayerManager
to handle painting. The LayerManager
contains two layers, one for the tank, one for the board.
The debug()
method, called from the game loop, compares the elapsed time through the game loop with the desired loop time (80 milliseconds) and displays the percentage of time used on the screen. It is for diagnostic purposes only, and would be removed before the game was shipped to customers.
The timing of the game loop is more sophisticated than in the previous SimpleGameCanvas
. To try to perform one iteration of the game loop every 80 milliseconds accurately, MicroTankCanvas
measures the time it takes to perform tick()
, input()
, and render()
. It then sleeps for the remainder of the 80-millisecond cycle, keeping the total time through each loop as close as possible to 80 milliseconds.
MIDP 2.0's Game API provides a framework that simplifies developing 2D action games. First, the GameCanvas
class provides painting and input methods that make a tight game loop possible. Next, a framework of layers makes it possible to create complex scenes. TiledLayer
assembles a large background or scene from a palette of source image tiles. Sprite
is appropriate for animated characters and can detect collisions with other objects in the game. LayerManager
is the glue that holds layers together. The muTank example provides a foundation of working code to demonstrate the Game API.
About the Author: Jonathan Knudsen is the author of several books, including Wireless Java (second edition) , The Unofficial Guide to LEGO MINDSTORMS Robots , Learning Java (second edition) , and Java 2D Graphics . Jonathan has written extensively about Java and Lego robots, including articles for JavaWorld, EXE, NZZ Folio, and the O'Reilly Network. Jonathan holds a degree in mechanical engineering from Princeton University.