Developers
Local Navigation
Richard Evers, Editor
In this article details will be provided on how to use the MIDP 2.0 gaming package, javax.microedition.lcdui.game. Source code will also be provided for a basic JavaTM ME game that's a decent example of what's possible with very little time and coding effort. It was constructed to be easy to alter without focusing on optimization.
Topics within this article include:
There are six classes in the MIDP 2.0 gaming package:
- Canvas
- GameCanvas
- LayerManager
- Layer
- TiledLayer
- Sprite
You will also need to use the java.lang.Object.Thread class to make any game you develop work.
The Canvas and GameCanvas Classes
The GameCanvas class inherits from Canvas and then adds functionality to make gaming as easy as possible. The most useful addition is a dedicated offscreen buffer that is used to pre-create screens before display. I'll review the Canvas class first, then delve into GameCanvas.
javax.microedition.lcdui.Canvas
Canvas is a base class that is used to write software that handles low-level events and makes graphic calls. It is interchangeable with standard Screen classes which makes it possible to use Canvas instead of higher-level screens.
Common game action fields are provided:
- UP
- DOWN
- LEFT
- RIGHT
- FIRE
BlackBerry® devices traditionally use 2' for UP, 8' for DOWN, 4' for LEFT, 6' for RIGHT and SPACE for FIRE.
General purpose key fields are also provided:
- GAME_A
- GAME_B
- GAME_C
- GAME_D
Call getKeyCode() to determine the physical key that correponds to each game action field and GAME_? field.
Known key fields are also provided
- KEY_NUM0 - set to 0'
- KEY_NUM1 - set to 1'
- KEY_NUM2 - set to 2'
- KEY_NUM3 - set to 3'
- KEY_NUM4 - set to 4'
- KEY_NUM5 - set to 5'
- KEY_NUM6 - set to 6'
- KEY_NUM7 - set to 7'
- KEY_NUM8 - set to 8'
- KEY_NUM9 - set to 9'
- KEY_STAR - set to *'
- KEY_POUND - set to #'
Methods
Canvas has a single constructor that does not take an argument. The following is a detailed discussion of each method.
int getGameAction(int keyCode)
Returns the game action associated with the passed device keyCode, or returns zero if no action is associated with the keyCode. Returned game actions can be UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C or GAME_D. This method throws IllegalArgumentException if the passed keyCode is invalid.
int getKeyCode(int gameAction)
Returns the device-specific key value for the passed game action key code. Valid game actions can be UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C or GAME_D. This method throws IllegalArgumentException if the passed gameAction is invalid.
String getKeyName(int keyCode)
Returns a text string that describes a key. This usually matches the text printed on the key. The returned String can be displayed to aid users. In general, you'll get back text identifiers for UP, DOWN, LEFT, RIGHT and FIRE, plus keys such as ENTER, ESCAPE, SPACE, BACKSPACE and DELETE depending on mapping. If no text identifier exists, you'll get back an ASCII representation, such as 9'.
To get the key name of game action codes such as GAME_A, GAME_B, etc., try this:
String gameA = getKeyName(getKeyCode(GAME_A));
This method throws IllegalArgumentException if the passed keyCode is invalid.
boolean hasPointerEvents()
Returns true if the device supports pointer press and release events. Currently returns false on all BlackBerry devices.
boolean hasPointerMotionEvents()
Returns true if the platform supports pointer motion events (pointer dragged). Currently returns false on all BlackBerry devices.
boolean hasRepeatEvents()
Returns true if the platform can generate repeat events when a key is held down. Currently returns false on all BlackBerry devices since trackwheel roll events don't repeat.
protected void hideNotify()
This method is always called after the Canvas has been removed from the display. Note that the BlackBerry JavaDocs states that calling hideNotify() will stop all calls to key, pointer, paint, and commandAction() methods, and that calls can be restarted by calling showNotify(). In reality, the default implementations of this method and showNotify() are empty and are also not overridden in the GameCanvas class.
boolean isDoubleBuffered()
Returns true if graphics is double buffered. Returns true on BlackBerry devices.
protected void keyPressed(int keyCode)
Automatically called when a key is pressed. Canvas has an empty implementation of this method and GameCanvas does not redefine it.
protected void keyReleased(int keyCode)
Automatically called when a key is released. Canvas has an empty implementation of this method and GameCanvas does not redefine it.
protected void keyRepeated(int keyCode)
Called when a key is repeated (held down). Canvas has an empty implementation of this method and GameCanvas does not redefine it.
protected abstract void paint(Graphics g)
Renders the Canvas where the Graphic object's clip region defines the area of the screen that is considered to be invalid. Your application must implement this method to paint any graphics.
The Graphics object that is passed to the paint() method has the following properties:
- the destination is the actual display, or if double buffering is in effect, a buffer for the display
- the clip region that includes at least one pixel within this Canvas
- the current color, which is set to black
- the font is the same as the font that is returned by Font.getDefaultFont()
- the stroke style, which is set to SOLID
- the origin of the coordinate system is located at the upper-left corner of the Canvas
- the Canvas is visible -- isShown() will return true
protected void pointerDragged(int x, int y)
Called when the pointer has been dragged. Make sure to call hasPointerMotionEvents() to determine if pointer events are supported. Canvas has an empty implementation of this method because pointer events are not currently supported on BlackBerry devices.
protected void pointerPressed(int x, int y)
Called when the pointer is pressed. Canvas has an empty implementation of this method because pointer events are not currently supported on BlackBerry devices.
protected void pointerReleased(int x, int y)
Called when the pointer is released. Canvas has an empty implementation of this method because pointer events are not currently supported on BlackBerry devices.
void repaint()
Requests a repaint for the entire Canvas. Calling repaint(0, 0, getWidth(), getHeight()); will achieve the same result.
void repaint(int x, int y, int width, int height)
Requests a repaint for the specified region of the Screen where x' and y' are the top-left coordinates to start repainting. Note that repaint() will not block while paint() is occurring, and calling repaint() can trigger a later call to paint().
void serviceRepaints()
Forces any pending repaint() requests to be serviced immediately. This method blocks until the pending requests have been serviced. If there are no pending repaints, or if this canvas is not visible, it will do nothing and return immediately.
This method blocks until the call to paint() returns. Your application has no control over which thread calls paint(). If the caller of serviceRepaints() holds a lock that the paint() method acquires, this may result in deadlock. Callers of serviceRepaints() must not hold any locks that might be acquired within the paint() method.
The Display.callSerially() method provides a way to call back an application after painting has been completed, avoiding the danger of deadlock.
void setFullScreenMode(boolean mode)
Controls whether the Canvas is in full-screen mode (true) or in normal screen mode (false). The title and separator fields (if they exist) are removed from the vertical field manager in full screen mode, and are restored in normal screen mode.
protected void showNotify()
This method is always called before the Canvas is made visible. The default implementations of this method and the hideNotify() method are empty and are not overridden in the GameCanvas class.
protected void sizeChanged(int w, int h)
Called when the drawable area of the Canvas has been changed. The default implementation of this method is empty and is not overridden in the GameCanvas class.
javax.microedition.lcdui.GameCanvas
GameCanvas extends Canvas, and provides game-specific functionality such as an automatic off-screen graphics buffer and the ability to poll key status. Use of an off-screen graphics buffer (also known as double buffering) stops display flickering by writing to the offscreen screen buffer before making it visible.
Public constants defined in this class are bit representations of Canvas game actions.
- UP_PRESSED = 1 << Canvas.UP;
Result = 00000000 00000010 (0x0002) - LEFT_PRESSED = 1 << Canvas.LEFT;
Result = 00000000 00000100 (0x0004) - RIGHT_PRESSED = 1 << Canvas.RIGHT;
Result = 00000000 00100000 (0x0020) - DOWN_PRESSED = 1 << Canvas.DOWN;
Result = 00000000 01000000 (0x0040) - FIRE_PRESSED = 1 << Canvas.FIRE;
Result = 00000001 00000000 (0x0100) - GAME_A_PRESSED = 1 << Canvas.GAME_A;
Result = 00000010 00000000 (0x0200) - GAME_B_PRESSED = 1 << Canvas.GAME_B;
Result = 00000100 00000000 (0x0400) - GAME_C_PRESSED = 1 << Canvas.GAME_C;
Result = 00001000 00000000 (0x0800) - GAME_D_PRESSED = 1 << Canvas.GAME_D;
Result = 00010000 00000000 (0x1000)
Methods
Game Canvas has a single constructor:
GameCanvas(boolean suppressKeyEvents)
Pass true if you only need to query the status of keys using getKeyStates(). This will improve performance slightly during execution. Pass false if you also need to call the keyPressed(), keyRepeated() and keyReleased() methods.
Note: showNotify() and hideNotify() are not implemented in Canvas or GameCanvas therefore you cannot start and stop key suppression after object creation.
The following is a detailed discussion of each method.
void flushGraphics()
Writes the off-screen buffer to the display. The contents of the off-screen buffer are not changed after write. This method returns after the flush completes, so you can render the next frame to the off-screen buffer when the method returns. This method does nothing and returns immediately if the GameCanvas is not on display or if the flush request cannot occur because the system is busy.
void flushGraphics(int x, int y, int width, int height)
Writes the passed region of the off-screen buffer to the display. Only the intersecting region is written if the passed region extends beyond the bounds of the GameCanvas. Nothing is written if the passed width or height is less than 1.
This method returns after the flush completes, so you can render the next frame to the off-screen buffer when the method returns. This method does nothing and returns immediately if the GameCanvas is not on display or if the flush request cannot occur because the system is busy.
The parameters are x (left), y (top), width and height.
protected Graphics getGraphics()
Gets the off-screen buffer used to render a GameCanvas. Once rendered, call flushGraphics() to display the buffer. Call this method after your game starts to get an off-screen buffer then reuse the buffer throughout your game. A newly created Graphics object has the following properties:
- destination is set to the off-screen buffer
- clip region is the entire buffer
- current color is set to black
- font is identical to Font.getDefaultFont();
- stroke style is SOLID
- origin of the coordinate system is the upper-left corner of the buffer
int getKeyStates()
Gets the state of the game keys. Each bit in the returned integer represents a specific key on the device. Each bit will be set if the corresponding key is currently down or has been pressed at least once since the last time this method was called. The bit will be 0 if the key is currently up and has not been pressed at all since the last time this method was called. This latching behavior ensures that a rapid key press and release will always be caught by the game loop, regardless of how slowly the loop runs.
For example:
// Get the key state and store it
int keyState = getKeyStates();
if ((keyState & LEFT_KEY) != 0) {
positionX--;
} else if ((keyState & RIGHT_KEY) != 0) {
positionX++;
}
Calling this method clears any latched state. Another call to getKeyStates() immediately after a prior call will report the system's best idea of the current state of the keys, the latched bits having been cleared by the first call.
This method will return 0 if the GameCanvas is not visible and will return 0 when the screen first becomes visible. If any key is held down while the screen state goes from invisible to visible, the user must release the key then try again before it can be reported.
void paint(Graphics g)
Overrides Canvas.paint() and renders the off-screen buffer at top left (0,0). Rendering of the screen buffer is subject to the clip region and the origin translation of the Graphics object. This method throws NullPointerException if the passed Graphics object is null
The following are two classes from the Bone game that show how to use Canvas.
Source code: BoneCanvas
package com.blackberrydeveloperjournal.bone;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.Timer;
public class BoneCanvas extends
javax.microedition.lcdui.game.GameCanvas {
// adjust font, background and grass colors
// black
private static final int FONT_COLOR = 0x000000;
// light cyan
private static final int BACK_COLOR = 0xCCFFFF;
// light green - same as Tuft sprite color
private static final int TUFT_COLOR = 0x33FF33;
// adjust to arrange where the points, countdown
// and game over messages will display
private static final int LINE_POINTS = 1;
private static final int LINE_COUNTDOWN = 2;
private static final int LINE_OVER = 3;
private static final int LINE_MAX = LINE_OVER;
// adjust grass depth
private static final int DEPTH = 25;
// adjust game duration
private static final int GAME_MINUTES = 4;
// work constants
public static int WIDTH;
private static int HEIGHT;
private static int HEIGHT_FONT;
private static int WIDTH_POINTS, WIDTH_COUNTDOWN, WIDTH_EOG;
private static int X_TOP, Y_TOP;
private static Font CURRENT_FONT;
private static int RIGHT = 1;
private static int LEFT = 0;
private int GAME_DURATION = GAME_MINUTES * 60;
private int gameCountdown = GAME_DURATION;
private int playerScore = 0;
private boolean gameIsOver;
private String countdownBuffer, countdownLabel, pointsLabel,
eogLabel;
private BoneManager hManager;
private Bone hMIDlet;
private BoneTime hBoneTime;
public BoneCanvas( Bone obj ) throws Exception {
super( false );
hMIDlet = obj;
X_TOP = Y_TOP = 0;
WIDTH = getWidth();
HEIGHT = getHeight();
CURRENT_FONT = getGraphics().getFont();
HEIGHT_FONT = CURRENT_FONT.getHeight();
countdownLabel = "Countdown: ";
WIDTH_COUNTDOWN = CURRENT_FONT.stringWidth
( countdownLabel + "0:00" );
pointsLabel = "Points: ";
WIDTH_POINTS = CURRENT_FONT.stringWidth
( pointsLabel + "0000" );
eogLabel = "End of Game";
WIDTH_EOG = CURRENT_FONT.stringWidth( eogLabel );
if ( hManager == null ) {
int yStart = Y_TOP + (LINE_MAX*HEIGHT_FONT);
int yEnd = HEIGHT - (LINE_MAX*HEIGHT_FONT)-DEPTH;
gameIsOver = false;
// set a timer to go off every second for
// accurate time tracking
hBoneTime = new BoneTime( GAME_DURATION );
new Timer().schedule( hBoneTime, 0, 1000 );
hManager = new BoneManager
( X_TOP, yStart, WIDTH, yEnd );
}
}
// overrides Canvas.paint()
public void paint( Graphics g ) {
// paint the background
g.setColor( BACK_COLOR );
g.fillRect( X_TOP, Y_TOP, WIDTH, HEIGHT );
// paint the grass
g.setColor( TUFT_COLOR );
g.fillRect
( X_TOP, Y_TOP + HEIGHT - DEPTH, WIDTH, HEIGHT );
// paint the layer manager
try {
hManager.paint( g );
} catch( Exception err ) {
hMIDlet.showException( err );
}
// determine current time
if ( getTimeLeft() != gameCountdown ) {
gameCountdown = getTimeLeft();
int gameNow = getTimeLeft();
int seconds = gameCountdown % 60;
countdownBuffer = (gameCountdown / 60) + ":";
if ( seconds < 10 )
countdownBuffer += "0" + seconds;
else
countdownBuffer += seconds;
}
// draw the time remaining in the game and points scored
g.setFont( CURRENT_FONT );
g.setColor( FONT_COLOR );
g.drawString( countdownLabel + countdownBuffer,
(WIDTH - WIDTH_COUNTDOWN)/2,
Y_TOP + (LINE_COUNTDOWN*HEIGHT_FONT), g.LEFT | g.TOP );
g.drawString( pointsLabel + playerScore,
(WIDTH - WIDTH_POINTS)/2,
Y_TOP + (LINE_POINTS*HEIGHT_FONT), g.LEFT | g.TOP );
if ( gameIsOver ) {
hMIDlet.setMenuStart();
// clear the time region
g.setColor( BACK_COLOR );
g.fillRect( X_TOP, Y_TOP +
(LINE_COUNTDOWN*HEIGHT_FONT),
WIDTH, HEIGHT_FONT + 1 );
// draw end of game message
g.setColor( FONT_COLOR );
g.setFont( CURRENT_FONT );
g.drawString( eogLabel, (WIDTH - WIDTH_EOG)/2,
Y_TOP + (LINE_OVER*HEIGHT_FONT),
g.LEFT | g.TOP );
}
}
// returns time remaining until game ends
// also sets gameIsOver flag when time runs out
private int getTimeLeft() {
int timeLeft = hBoneTime.getRemainingTime();
if ( timeLeft == 0 )
gameIsOver = true;
return timeLeft;
}
private void gameStop() { gameIsOver = true; }
private void gameRun() { gameIsOver = false; }
public void canvasBegin() {
gameRun();
Display hDisplay = Display.getDisplay( hMIDlet );
// make current, visible and in focus
hDisplay.setCurrent( this );
repaint();
hDisplay = null;
}
public void canvasInitialize() {
hManager.restoreToDefault();
gameRun();
playerScore = 0;
gameCountdown = GAME_DURATION;
repaint();
}
public void canvasRefresh() {
if ( !gameIsOver ) {
int keyState = getKeyStates();
// 6 on 87xx, 72xx and 71xx BlackBerry devices
if (( keyState & LEFT_PRESSED) > 0 )
hManager.setDirection( LEFT );
// 4 on 87xx, 72xx and 71xx BlackBerry devices
if (( keyState & RIGHT_PRESSED ) > 0)
hManager.setDirection( RIGHT );
// 2 on 87xx, 72xx and 71xx BlackBerry devices
if (( keyState & UP_PRESSED ) > 0 )
hManager.walkerHop();
playerScore += hManager.movement();
try {
paint( getGraphics() );
// display offscreen buffer
flushGraphics();
} catch(Exception err) {
hMIDlet.showException(err);
}
// end the game, allow user to restart via menu
// or quit entirely
if ( gameCountdown == 0 ) {
gameStop();
hMIDlet.pauseApp();
}
}
}
}
Source code: BoneTime
package com.blackberrydeveloperjournal.bone;
import java.util.TimerTask;
public class BoneTime extends TimerTask {
private int remainingTime;
public BoneTime( int timeLimit ) {
remainingTime = timeLimit;
}
// called each time the timer is triggered
public void run() {
--remainingTime;
}
public int getRemainingTime() {
return remainingTime;
}
}
The BoneTime class is used by the BoneCanvas class to keep accurate track of elapsed game time. It's initialized by passing the time limit in seconds, and is called every second thereafter to decrement the time limit.
The BoneCanvas class draws and updates the screen, keeps track of the game duration, and handles keys. It was designed so you can easily change the color of the text, background and grass, the regions where game points, remaining time and the game over message are displayed, the depth of the grass, and duration of the game.
The constructor determines the width and height of the display, the height of the current font, sets up the game points, countdown and end of game strings, starts the game timer and then calls BoneManager to create and handle Sprites.
javax.microedition.lcdui.LayerManager
The LayerManager simplifies rendering of Layers that have been added to it by automatically rendering the correct regions of each Layer in the appropriate order.
The LayerManager maintains an ordered list to which Layers can be added, inserted and removed. A Layer at index 0 is closest to the user while a Layer with the highest index is furthest away from the user. If a Layer is removed, the indices of subsequent Layers will be adjusted to maintain continuity.
The view window controls the size of the visible region and its position relative to the LayerManager's coordinate system. Changing the position of the view window enables effects such as scrolling or panning of the user's view. For example, to scroll to the right, simply move the view window's location to the right. The size of the view window controls how large the user's view will be, and is usually fixed at a size that is appropriate for the device's screen.
In the following example, the view window is set to 85 x 85 pixels and is located at (52, 11) in the LayerManager's coordinate system. The Layers appear at their respective positions relative to the LayerManager's origin.
The paint() method includes an (x,y) location that controls where the view window is rendered relative to the screen. Changing these parameters does not change the contents of the view window - it simply changes the location where the view window is drawn. Note that this location is relative to the origin of the Graphics object, and is subject to the translation attributes of the Graphics object.
For example, if a game uses the top of the screen to display the current score, the view window may be rendered at (17, 17) to provide enough space for the score.
Methods
void append(Layer l)
Appends a Layer to the list of existing Layers such that it has the highest index that is furthest from the user. The Layer is first removed from this LayerManager if it has already been added. Pass the Layer to be added when calling this function. This method will throw NullPointerException if the passed Layer is null.
Layer getLayerAt(int index)
Returns the Layer at the passed index location. This method will throw IndexOutOfBoundsException if the passed index is less than zero, or if it is equal to or greater than the number of Layers added to this LayerManager.
int getSize()
Returns the number of Layers in this LayerManager.
void insert(Layer l, int index)
Inserts the passed Layer in this LayerManager at the passed index position.
This method throws:
- NullPointerException
The passed Layer is null - IndexOutOfBoundsException
The passed index is less than 0 or greater than the number of Layers already added to this LayerManager
void paint(Graphics g, int x, int y)
Renders the LayerManager's current view window at the passed location using the passed Graphics object.
LayerManager renders each layer in order of descending index. Layers that are completely outside of the view window are not rendered. The passed coordinates are used to render the LayerManager's view window relative to the origin of the passed Graphics object. For example, a game may use the top of the screen to display the current score. The view window could be rendered at (0, 20) to render the game's layers below that area. This location is relative to the Graphics object's origin, so translating the Graphics object will change where the view window is rendered on the screen.
The Graphics object clip region is intersected with a region having the same dimensions as the view window and located at (x,y). The LayerManager translates the graphics object such that the point (x,y) corresponds to the location of the viewWindow in the coordinate system of the LayerManager. The Layers are then rendered in the appropriate order. The translation and clip region of the Graphics object are restored to their prior values before this method returns.
Rendering is subject to the clip region and translation of the Graphics object. Thus, only part of the passed view window may be rendered if the clip region is not large enough.
This method may ignore Layers that are invisible or that would be rendered entirely outside of the Graphics object's clip region to improve performance. The attributes of the Graphics object are not restored to a known state between calls to the Layers' paint methods. The clip region may extend beyond the bounds of a Layer.
To call this method, pass the graphics instance with which to draw the LayerManager and the horizontal and vertical locations to render the view window relative to the Graphics' translated origin.
This method will throw NullPointerException if the passed Graphics object is null.
void remove(Layer l)
Removes the passed Layer from this LayerManager. This method does nothing if the passed Layer has not been previously added to this LayerManager.
This method will throw NullPointerException if the passed Layer is null.
void setViewWindow(int x, int y, int width, int height)
Sets the view window on the LayerManager. The view window is the region that the LayerManager draws when its paint() method is called. This allows the developer to control the size of the visible region, as well as the location of the view window relative to the LayerManager's coordinate system.
The view window stays constant until it is modified by another call to this method. By default, the view window is located at (0,0) in the LayerManager's coordinate system and its width and height are both set to Integer.MAX_VALUE.
To call this method, pass the horizontal and vertical locations of the view window relative to the LayerManager's origin, and the width and height of the view window.
This method throws IllegalArgumentException if the width or height is less than 0.
The following is the BoneManager class of the Bone game. It is derived from LayerManager.
Source code for BoneManager
package com.blackberrydeveloperjournal.bone;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class BoneManager extends
javax.microedition.lcdui.game.LayerManager {
public static int HEIGHT;
private static int WIDTH;
private static int X_TOP, Y_TOP;
private static int RIGHT = 1;
private static int LEFT = 0;
private int travelDirection = RIGHT;
// adjust total number of hurtling Sprites (left & right)
private static int MAX_MISSILES = 8;
private Walk walker;
private Missile[] missileRight;
private Missile[] missileLeft;
private Tuft tufts;
private int offsetTL, defTLOffset;
public BoneManager
( int xPos, int yPos, int width, int height )
throws Exception {
X_TOP = xPos;
Y_TOP = yPos;
WIDTH = width;
HEIGHT = height;
// set view window for entire screen
setViewWindow( 0, 0, WIDTH, HEIGHT );
offsetTL = defTLOffset = tufts.WIDTH;
// create a walking Sprite
if ( walker == null ) {
walker = new Walk( offsetTL + WIDTH/2,
HEIGHT - Walk.HEIGHT - 1 );
append( walker );
}
// create RIGHT sprites that will
// hurtle towards the stick man!
if ( missileRight == null ) {
missileRight = new Missile[ MAX_MISSILES/2 ];
for ( int element=missileRight.length-1;
element >= 0; --element ) {
missileRight[ element ] = new Missile( RIGHT );
append( missileRight[ element ] );
}
}
// create LEFT sprites that will hurtle towards
// the stick man!
if ( missileLeft == null ) {
missileLeft = new Missile[ MAX_MISSILES/2 ];
for ( int element=missileLeft.length-1;
element >= 0; --element ) {
missileLeft[ element ] = new Missile( LEFT );
append( missileLeft[ element ] );
}
}
// create animated background
if ( tufts == null ) {
tufts = new Tuft();
append( tufts );
}
}
// set walker's direction: RIGHT or LEFT
public void setDirection( int direction ) {
travelDirection = direction;
}
// make walker hop
public void walkerHop() {
walker.doHop();
}
// reset game to default settings
public void restoreToDefault() {
offsetTL = defTLOffset;
travelDirection = RIGHT;
if ( walker != null )
walker.defaultState();
if ( tufts != null )
tufts.defaultState();
if ( missileRight != null )
for ( int element=missileRight.length-1;
element >= 0; --element )
missileRight[ element ].defaultState();
if ( missileLeft != null )
for ( int element=missileLeft.length-1;
element >=0; --element )
missileLeft[ element ].defaultState();
}
public void paint( Graphics g ) {
setViewWindow( offsetTL, 0, WIDTH, HEIGHT );
paint( g, X_TOP, Y_TOP );
}
// advance all moving objects
public int movement() {
tufts.movement();
walker.movement( travelDirection );
offsetTL = ( travelDirection == RIGHT )
? offsetTL+1 : offsetTL-1;
int score = getScore( missileRight )
+ getScore( missileLeft );
handleMove();
return score;
}
// calculate score of missiles hitting walker
// versus walker jumping over missiles
private int getScore( Missile[] missile ) {
int score = 0;
for ( int element=missile.length-1;
element >= 0; --element ) {
score += missile[ element ].movement
( walker, offsetTL, offsetTL + WIDTH );
score -= walker.isCollision( missile[ element ] );
}
return score;
}
private void handleMove() {
// update after passing a single animated Tuft object
if ( offsetTL % defTLOffset == 0 ) {
int width = defTLOffset;
if ( travelDirection == RIGHT )
width = -defTLOffset;
// Sprite inherits from
// javax.microedition.lcdui.game.Layer.move()
walker.move( width, 0 );
offsetTL += width;
moveMissile( missileLeft, width );
moveMissile( missileRight, width );
}
}
private void moveMissile( Missile[] missile, int width ) {
for ( int element=missile.length-1;
element >=0; --element )
missile[ element ].move( width, 0 );
}
}
The BoneManager class creates and maintains a Walk Sprite in the form of a stick man that walks in a stationary position, hops in the "air" and changes direction. It also creates and maintains simplistic Missile Sprites that hurtle towards the stick man. Points are earned or lost when the stick man either misses a Missile, or is hit by a Missile. A user-defined constant, MAX_MISSILES, has been provided for you to adjust the total number of Missile Sprites in the game. Note that this constant value will split evenly to create Missiles that hurtle from the left and right sides.
There should be no major surprises in the BoneManager constructor. When called, the x and y offsets, plus the width and height, are passed. The width and height are used to set the view window to the entire screen.
A single Walk Sprite is created in the mid-point of the screen, with a minuscule offset of the width of an animated tuft of grass from the Tuft class. The vertical positioning is set by the height of the screen minus the height of the Walk Sprite image.
The right and left Missile Sprites are created next, followed by an animated TiledLayer Tuft of grass.
There are a few public and private methods that are worth mentioning.
setDirection() is called to set the direction that the Walk Sprite is travelling. Pass RIGHT (1) or LEFT(0).
walkerHop() is called to force the Walk Sprite to hop. You can adjust the height of the hop in the Walk class.
restoreToDefault() is called to reset the Spites and values used in the game to their default settings.
movement() is called to advance all "moving" objects. This includes the animated Turf TiledLayer, the Walk Sprite, and the Missile Sprites. Note that handleMove() takes care of Sprite movement.
handleMove() only acts when the current x offset can be evenly divided by the width of an animated Turf object. If so, then the Walk object is "moved" by the width of a Turf object, then the right and left Missile Sprites are moved by the same distance by calling moveMissile().
getScore() is called to calculate the total number of Missiles avoided and the total number of Walk collisions that were detected.
javax.microedition.lcdui.Layer
Layer is an abstract class used to represent a visual element within a game. Each Layer has the top-left position, width, height and visibility status. The Layer's top-left co-ordinates (x,y) are always set relative to the origin of the Graphics object passed to Layer's abstract paint() method, which must be implemented within a subclass. Note that paint() is the only abstract method within this class. All of the other methods and the constructor have been implemented.
Methods
Layer has a single constructor that takes two arguments to set the internal width and height variables.
Layer(int width, int height)
int getHeight()
Returns the height of this layer in pixels.
int getWidth()
Returns the width of this layer in pixels.
int getX()
Returns the left-most upper corner position of this Layer in the painter's coordinate system.
int getY()
Returns the top corner position of this in the painter's coordinate system.
boolean isVisible()
Returns true if the Layer is visible else returns false.
void move(int dx, int dy)
Updates the upper-left origin of the Layer by adding "dx" pixels to the left coordinate and "dy" pixels to the top coordinate where either parameter can be a positive or a negative value. The Layer's coordinates will wrap if the passed values would lead to either the x coordinate or y coordinate to exceed Integer.MAX_VALUE or drop below Integer.MIN_VALUE.
abstract void paint(Graphics g)
Abstract method to paint this Layer if it is visible. Note that a subclass must override and implement this method.
void setPosition(int x, int y)
Changes the upper-left origin of the Layer to the passed coordinates in the painter's coordinate system.
void setVisible(boolean visible)
Pass true to make the Layer visible, and pass false to make the Layer invisible.
javax.microedition.lcdui.TiledLayer
Layer is an abstract class used to represent a visual element within a game. Each Layer has the top-left position, width, height and visibility status. The Layer's top-left co-ordinates (x,y) are always set relative to the origin of the Graphics object passed to Layer's abstract paint() method, which must be implemented within a subclass. Note that paint() is the only abstract method within this class. All of the other methods and the constructor have been implemented.
Methods
TiledLayer has a single constructor that takes five arguments.
TiledLayer(
int columns,
int rows,
Image image,
int tileWidth,
int tileHeight
)
"columns" is the width of the TiledLayer expressed in the number of "tileWidth" cells.
"rows" is the height of the TiledLayer expressed in the number of "tileHeight" cells.
"image" is the image used to created the static tile set.
The final parameters, "tileWidth" and "tileHeight", are the width and height, in pixels, for one tile.
Here's the syntax in basic form:
Image myImage = Image.createImage("MyImage.png");
TiledLayer myLayer = new TiledLayer(20, 10, myImage, 200, 100);
The TiledLayer class is used to work with images that can be used to construct backgrounds and virtual gaming areas through use and reuse of static "tiles." For example, using ladders, platforms and more. The trick is to pass an image to the constructor that includes all background components that you'll need, organized into equal-sized cells that can be mathematically determined.
For example:
The graphic image used to create each individual tile can take several forms as shown on the left, where the static tiles will end up being referenced by cell identifier. as shown on the right. Note that index referencing of static tiles starts at 1.
Placing of static tiles on the display is handled by setting cells in the grid to use a tile at a specific starting offset on the screen. All cells in the screen grid are initially empty (set to 0), and can be modified by using setCell() and fillCells().
Each static tile will have the dimensions of tileWidth and tileHeight. The source image width must be an integer multiple of tileWidth, and the source height must be an integer multiple of tileHeight. IllegalArgumentException will be thrown if either condition is not met.
The static tile set can be changed using setStaticTileSet(). Use of this method will come at a cost of time and increased memory use. You can avoid reliance on this method by using animated tiles instead of individual static tiles through the use of createAnimatedTile(). This method returns a negative index offset.
This method throws:
- NullPointerException
- Image is null
- IllegalArgumentException
- The number of rows or columns is less than 1
- tileHeight or tileWidth is less than 1
- Image width is not an integer multiple of the tileWidth
- Image height is not an integer multiple of the tileHeight
int createAnimatedTile(int staticTileIndex)
Creates a new animated tile within a set and returns the index as a negative value (-1, then -2, -3, etc.). The animated tile can be associated with a blank area (index 0) or a static tile with a positive index value.
This method will throw IndexOutOfBoundsException if the staticTileIndex is invalid.
void fillCells(int col, int row, int numCols, int numRows,
int tileIndex)
Sets a series of screen cells with the passed static tile index, animated tile index, or a blank tile using index 0. You also need to pass the start column and row of the top-left cell in the region, the number of columns and rows in the region along with the index of the tile to place in all cells in the region.
This method throws:
- IndexOutOfBoundsException
- The passed rectangular region extends beyond the bounds of the TiledLayer grid
- No tile is associated with index tileIndex
- IllegalArgumentException
- numCols is less than zero
- numRows is less than zero
int getAnimatedTile(int animatedTileIndex)
Returns the tile index referenced by the passed index of an animated tile. This method will throw IndexOutOfBoundsException if the passed animated tile index is invalid.
int getCell(int col, int row)
Pass the column and row of the cell to check to get the index value of the static or animated tile in a screen cell. An index value of 0 will be returned if the cell is empty. This method throws IndexOutOfBoundsException if the passed row or col is outside the bounds of the TiledLayer grid.
int getCellHeight()
Returns the pixel height of a cell in the TiledLayer grid.
int getCellWidth()
Returns the pixel width of a cell in the TiledLayer grid.
int getColumns()
Returns the number of columns in the TiledLayer grid. Call Layer.getWidth() to get the overall width of the TiledLayer, in pixels.
int getRows()
Returns the number of rows within the TiledLayer grid. Call Layer.getHeight() to get the overall height of the TiledLayer, in pixels.
void paint(Graphics g)
Draws the entire TiledLayer subject to the clip region of the Graphics object. The upper left corner is rendered at the TiledLayer's current position relative to the origin of the Graphics object. The current position of the upper-left corner can be retrieved by calling Layer.getX() and Layer.getY(). If the TiledLayer's Image is subject to change, then TiledLayer is rendered using the current contents of the Image. This method throws NullPointerException if the passed Graphics object is null.
Note that this method overrides Layer.paint().
void setAnimatedTile(int animatedTileIndex,
int staticTileIndex)
Sets a direct relationship between an animated tile and a static tile. This allows you to animate a section of the display grid through a simple change.
For example, compare the water in the following two images to see how animation can occur by calling setAnimatedTile() to change the static tile associated with a specific animated tile on display.
As shown, the water region is filled with an animated tile having an index of -1 which had been initially associated with static tile 5. The entire water area is animated by changing the associated static tile by using setAnimatedTile(-1, 7).
In this example, the display grid could look like this:
Note that the parameter, staticTileIndex, must either be the index of a static tile, or 0 if a blank tile is to be used.
This method throws:
- IndexOutOfBoundsException
- The staticTileIndex is invalid
- IndexOutOfBoundsException
- The animated tile index is invalid
void setCell(int col, int row, int tileIndex)
Sets the contents of a display cell to a static tile index or an animated tile index. It may also be left empty by passing an index of 0. Pass the column and row within the grid to set, and the index value.
This method throws:
- IndexOutOfBoundsException
- There is no tile with index tileIndex
- row or col is outside the bounds of the TiledLayer grid
void setStaticTileSet(Image image, int tileWidth,
int tileHeight)
Replaces the current static tile set with a new static tile set. If the new static tile set has as many or more tiles than the previous static tile set, then the animated tiles and cell contents will be preserved. If not, then the contents of the grid will be cleared (all cells set to index 0) and all animated tiles will be deleted. Use this method sparingly since it can be memory and time consuming Where possible, use animated tiles to animate tile appearance rather than change the tile set.
Pass the Image to use for creating the static tile set, and the width and height in pixels of a single tile.
This method throws:
- NullPointerException
- Passed Image is null
- llegalArgumentException
- tileHeight or tileWidth is less than 1
- Image width is not an integer multiple of the tileWidth
- Image height is not an integer multiple of the tileHeight
Source code for Tuft
package com.blackberrydeveloperjournal.bone;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
// Draw background objects to give the appearance of movement
public class Tuft extends TiledLayer {
// width & height of a single frame within the source image
public static final int WIDTH = 16;
private static final int HEIGHT = 16;
// number of frames within the source image
private static final int FRAMES = 4;
// size of tile array where element 0 is a blank tile
private static final int TILES = 2;
// order to play the animated tile
private static final int[] PLAY_ORDER
= { 2, 3, 2, 4, 2, 5 };
// number of elements in array above
private static final int PLAY_SIZE = 5;
// bitwise test to reduce polling period
// set alternate patterns to increase delay
// binary 00000101
private static final int POLLING_PERIOD = 5;
private static int COLUMNS;
private static int X_UPPER = 0;
private static int Y_UPPER = BoneManager.HEIGHT - HEIGHT;
private int index = 0;
private int thisTile = 0;
private int delayCounter = 0;
public Tuft() throws Exception {
// Note: calcColumns() sets COLUMN
super( calcColumns( BoneCanvas.WIDTH ), 1,
Image.createImage( "img/tuft.png" ), WIDTH, HEIGHT );
// set Layer's position
setPosition( X_UPPER, Y_UPPER );
// create a new animated tile
// returns the index that refers to the new
// animated tile. Note that referencing begins
// at 1 where 0 is a empty tile
index = createAnimatedTile( TILES );
// blanket the ground with grass
for ( int element = COLUMNS-1; element >= 0; --element )
setCell( element, 0, index );
}
// used in call to super() where nothing can be done
// before making the call
private static int calcColumns( int screenWidth ) {
COLUMNS = ((screenWidth / WIDTH) + 1)*(FRAMES-1);
return COLUMNS;
}
// adjust to provide screen "movement"
public void movement() {
// decrease animation speed slightly
if ((delayCounter++ & POLLING_PERIOD)
== POLLING_PERIOD ) {
// animate grass
// remove animation to increase speed
setAnimatedTile( index, PLAY_ORDER[thisTile++] );
if ( thisTile > PLAY_SIZE )
thisTile = 0;
}
}
// restore to default state
public void defaultState() {
setPosition( X_UPPER, Y_UPPER );
thisTile = delayCounter = 0;
setAnimatedTile( index, PLAY_ORDER[thisTile] );
}
}
The Turf class handles an animated TiledLayer object that resembles blades of grass. This class was created to lay down a dense layer of animated grass, which is updated on a regular basis to switch image frames to generate an illusion of directional movement on the screen.
The class opens up with a series of constants that can be altered as required.
The WIDTH and HEIGHT are set to the x and y dimensions of each frame within the source image. In this case A 16 x 16 frame was used with 4 FRAMES in the image.
The TILES constant, set to 2, is a bit of a no-brainer in that referencing in TiledLayer objects starts at 1 where 0 is used to display a blank tile.
PLAY_ORDER is an int array that lays down the sequence to play the animated tiles. PLAY_SIZE indicates how large the PLAY_ORDER array is.
POLLING_PERIOD is a binary value used in a bitwise test to determine when the animated sequencing is refreshed. Set it to 0 or 1 for the fastest (aka most CPU wasting) refresh cycle. It has been set to 5 (binary 00000101) to reduce the refresh rate and speed up the game slightly.
The Tuft() constructor initially calls super(), and relies on calcColumns() to set and pass back the COLUMNS constant based on the canvas width. The result is used to load the tuft.png image.
The Layer's position is set via setPosition(), with a new animated tile created by calling createAnimatedTile()
Afterwards, a loop is entered to blanket the "ground" with animated "grass."
There are three other methods in this class.
calcColumns() to determine how many columns are available to display animated tufts of grass, movement() to animate the tufs of grass, and defaultState() to restore everything to the state in which it was first created.
javax.microedition.lcdui.Sprite
This class is my favorite! While TiledLayer is really useful to create static and animated backgrounds and more, Sprites are cool in the extreme! I mean, what game could possibly exist without the use of tiny images blasting around, doing whatever needs to be done to make the game exciting? Sprites can be created to include a slew of static images that can displayed in whatever sequence you want. The Sprite can also be transformed through mirroring or changing the axis, always fixed on single point of reference. This has the potential to be really cool with the right approach.
Fields
- TRANS_MIRROR
Reflect Sprite about its vertical center. - TRANS_MIRROR_ROT90
Reflect Sprite about its vertical center and then rotate clockwise by 90 degrees. - TRANS_MIRROR_ROT180
Reflect Sprite about its vertical center and then rotate clockwise by 180 degrees. - TRANS_MIRROR_ROT270
Reflect Sprite about its vertical center and then rotate clockwise by 270 degrees. - TRANS_NONE
No transform applied to Sprite. - TRANS_ROT90
Rotate Sprite clockwise by 90 degrees. - TRANS_ROT180
Rotate Sprite clockwise by 180 degrees. - TRANS_ROT270
Rotate Sprite clockwise by 270 degrees.
Constructors
Sprite(Image image)
Creates a new non-animated Sprite from the passed Image. This does not allow splitting of the image into frames for animation. Calling this constructor is equivalent to calling new Sprite(image, image.getWidth(), image.getHeight()). The Sprite is visible by default and its upper-left corner is positioned at (0,0) in the painter's coordinate system. This method throws NullPointerException if the passed Image is null.
Sprite(Image image, int frameWidth, int frameHeight)
Creates a new animated Sprite using frames within the passed Image. All frames must be equally sized using the passed dimensions, and can be laid out horizontally, vertically, or as a grid within the passed Image. The image width must be an integer multiple of the frame width, and the height must be an integer multiple of the frame height. The values returned by Layer.getWidth() and Layer.getHeight() will reflect the frame width and frame height subject to the Sprite's current transform.
Sprites are organized in a default frame sequence that match the raw frame numbers, with the index starting at frame 0. Frame sequencing can be changed with setFrameSequence().
The Sprite is visible by default and its upper-left corner is positioned at (0,0) in the painter's coordinate system.
This method throws:
- NullPointerException
- Image is null
- IllegalArgumentException
- frameHeight or frameWidth is less than 1
- Image width is not an integer multiple of the frameWidth
- Image height is not an integer multiple of the frameHeight
Sprite(Sprite s)
Creates a new Sprite from another Sprite. All attributes of the passed Sprite, such as the raw frames, position, frame sequence, current frame, reference point, collision rectangle, transform, and visibility are duplicated in the new Sprite.
This method throws NullPointerException if the passed Sprite is null.
Methods
boolean collidesWith(Image image, int x, int y,
boolean pixelLevel)
Checks for a collision between this Sprite and the passed Image that has its upper left corner in the passed location. A collision is only detected if opaque pixels collide when pixel-level detection is used. This means that an opaque pixel in the Sprite would have to collide with an opaque pixel in the Image for a collision to be detected. Only pixels within the Sprite's collision rectangle are checked.
If pixel-level detection is not used, then this method checks if the Sprite's collision rectangle intersects with the Image's bounds.
Any transform that has been applied to the Sprite will be accounted for. The Sprite must be visible to detect a collision.
To call this method, pass the image to test for collision, the horizontal and vertical locations of the Image's upper left corner, and true to test for collision on a pixel-by-pixel basis or false to test using simple bounds checking.
This method will only return true if this Sprite has collided with the Image.
This method throws NullPointerException if the Image is null.
boolean collidesWith(Sprite s, boolean pixelLevel)
Checks for a collision between this Sprite and the passed Sprite. A collision is only detected if opaque pixels collide when pixel-level detection is used. This means that an opaque pixel in the first Sprite would have to collide with an opaque pixel in the second Sprite for a collision to be detected. Only those pixels within the Sprites' respective collision rectangles are checked.
This method checks if the Sprites' collision rectangles intersect when pixel-level detection is not used.
Any transform that has been applied to the Sprite will be accounted for. The Sprite must be visible to detect a collision.
When you call this method, pass the Sprite to test for collision with, and a boolean value of true to test for collision on a pixel-by-pixel basis, or false to test using simple bounds checking.. This method only returns true if the two Sprites have collided.
This method throws NullPointerException if Sprite is null.
boolean collidesWith(TiledLayer t, boolean pixelLevel)
Checks for a collision between this Sprite and the passed Sprite. A collision is detected only if opaque pixels collide when pixel-level detection is used. This means that an opaque pixel in the first Sprite would have to collide with an opaque pixel in the second Sprite for a collision to be detected. Only those pixels within the Sprites' respective collision rectangles are checked.
This method checks if the Sprites' collision rectangles intersect when pixel-level detection is not used.
Any transform that has been applied to the Sprite will be accounted for. The Sprite must be visible to detect a collision.
This method returns true if the two Sprites have collided.
This method throws NullPointerException if Sprite is null.
void defineCollisionRectangle(int x, int y, int width,
int height)
Defines the Sprite's bounding rectangle for collision detection purposes. This rectangle is relative to the un-transformed Sprite's upper-left corner and defines the area that is checked for collision detection. Only pixels within the collision rectangle are checked when pixel-level detection is used. By default, a Sprite's collision rectangle is located at 0,0 and has the Sprite's dimensions. The collision rectangle may be larger or smaller than the default rectangle. If it's larger, then the pixels outside the bounds of the Sprite are considered transparent for pixel-level collision detection.
To call this method, pass the horizontal and vertical location of the collision rectangle relative to the untransformed Sprite's left and top edges, and the width and height of the collision rectangle.
This method throws IllegalArgumentException if the passed width or height is less than 0.
void defineReferencePixel(int x, int y)
Defines the Sprite's reference pixel by its location relative to the upper-left corner of the Sprite's un-transformed frame, which may lay outside of the frame's bounds.
When a transformation is applied, the reference pixel is defined relative to the Sprite's initial upper-left corner before transformation. This corner may no longer appear as the upper-left corner in the painter's coordinate system under current transformation.
A Sprite's reference pixel is located at (0,0) by default. This means that the pixel is in the upper-left corner of the raw frame.
Changing the reference pixel does not change the Sprite's position in the painter's coordinate system. This means that the values returned by getX() and getY() will not change as a result of defining the reference pixel. However, subsequent calls to methods that involve the reference pixel will be impacted by its new definition.
Call this method by passing the horizontal location of the reference pixel, relative to the left edge of the un-transformed frame, and the vertical location of the reference pixel, relative to the top edge of the un-transformed frame.
int getFrame()
Returns the current index in the frame sequence, and not the index of the actual frame that is displayed.
int getFrameSequenceLength()
Returns the number of elements in the frame sequence. This does not reflect the number of raw frames, which will be the same if the default frame sequence is used.
int getRawFrameCount()
Returns the number of raw frames for this Sprite and not the length of the Sprite's frame sequence. These will be the same if the default frame sequence is used.
int getRefPixelX()
Returns the horizontal position of this Sprite's reference pixel in the painter's coordinate system.
int getRefPixelY()
Returns the vertical position of this Sprite's reference pixel in the painter's coordinate system.
void nextFrame()
Positions to the next frame in the frame sequence, which will be the first entry in the sequence after reaching the end of the sequence.
void paint(Graphics g)
Draws the current Sprite frame using the passed Graphics object. The Sprite's upper left corner is rendered at the Sprite's current position relative to the origin of the Graphics object. The current position of the Sprite's upper-left corner can be determine by calling Layer.getX() and Layer.getY(). Rendering is subject to the clip region of the Graphics object. The Sprite will be drawn only if it is visible.
This method overrides Layer.paint()
This method throws NullPointerException if the passed Graphics object is null.
void prevFrame()
Positions to the previous frame in the frame sequence. Note that this will be the last entry in the sequence after reaching the first entry of the sequence.
void setFrame(int sequenceIndex)
Selects the current frame within the frame sequence, which will be rendered after paint(Graphics) is called. The parameter, "sequenceIndex", is the desired entry within the frame sequence, and not the index of the actual frame.
This method throws:
- IndexOutOfBoundsException
- sequenceIndex is less than 0
- sequenceIndex is equal to or greater than the length of the current frame sequence, or the number of raw frames for the default sequence.
void setFrameSequence(int[] sequence)
Sprite frames are displayed in order by default. This method permits setting of an arbitrary sequence of available frames by passing an array of integers containing the frame sequence. The Sprite will revert to the default frame sequence if null is passed as a parameter. Note that the current index in the frame sequence is reset to zero after calling this method. Also note that the contents of the sequence array are copied when this method is called, therefore any changes made to the array after this method returns will have no effect on the Sprite's frame sequence.
This methods throws:
- ArrayIndexOutOfBoundsException
- sequence is populated and any member of the array has a value less than 0 or greater than or equal to the number of frames as reported by getRawFrameCount()
- IllegalArgumentException
- The array has less than 1 element
void setImage(Image img, int frameWidth, int frameHeight)
Replaces the current raw frames of the Sprite with a new set of raw frames. The values returned by Layer.getWidth() and Layer.getHeight() will reflect the new frame width and height subject to the Sprite's current transform.
Changing the image for the Sprite could change the number of raw frames. The current frame will remain unchanged if the new frame set has as many or more raw frames than the previous frame set.
If setFrameSequence() is used to define a custom frame sequence, the default sequence will remain unchanged. If a custom frame sequence is not defined, then the default frame sequence will be updated to be the default frame sequence for the new frame set. In other words, the new default frame sequence will include all of the frames from the new raw frame set, as if this new image had been used in the constructor.
If the new frame set has fewer frames than the previous frame set, then the current frame will be reset to entry 0. Any custom frame sequence will be discarded and the frame sequence will revert to the default frame sequence for the new frame set.
The reference point location will be unchanged after calling this method in terms of its defined location within the Sprite and its position in the painter's coordinate system. However, if the frame size is changed and the Sprite has been transformed, the position of the Sprite's upper-left corner may change such that the reference point remains stationary.
If the Sprite's frame size is changed by this method, the collision rectangle is reset to its default value. This means that it is set to the new bounds of the untransformed Sprite.
To call this method, pass the Image to use for the Sprite, and the frame width and height in pixels of the individual raw frames.
This method throws:
- NullPointerException
- The passed Image is null
- IllegalArgumentException
- frameHeight or frameWidth is less than 1
- The Image width is not an integer multiple of the frameWidth
- The Image height is not an integer multiple of the frameHeight
void setRefPixelPosition(int x, int y)
Sets this Sprite's reference pixel to the passed horizontal and vertical location in the painter's coordinate system.
void setTransform(int transform)
Sets the Sprite's transform to alter its rendered appearance. Transforms are applied to the original Sprite image. They are not cumulative and can't be combined.
Since some transforms involve rotations of 90 or 270 degrees, their use may result in the overall width and height of the Sprite being swapped. As a result, the values returned by Layer.getWidth() and Layer.getHeight() may change.
The collision rectangle is also modified by the transform so that it remains static relative to the pixel data of the Sprite. Similarly, the defined reference pixel is unchanged by this method, but its visual location within the Sprite may change as a result.
This method repositions the Sprite so that the location of the reference pixel in the painter's coordinate system does not change as a result of changing the transform. Thus, the reference pixel effectively becomes the centerpoint for the transform. Consequently, the values returned by getRefPixelX() and getRefPixelY() will be the same before and after the transform is applied, but the values returned by getX() and getY() may change.
To call this method, simply pass the desired transform for this Sprite. This includes:
- TRANS_MIRROR
Reflect Sprite about its vertical center. - TRANS_MIRROR_ROT90
Reflect Sprite about its vertical center and then rotate clockwise by 90 degrees. - TRANS_MIRROR_ROT180
Reflect Sprite about its vertical center and then rotate clockwise by 180 degrees. - TRANS_MIRROR_ROT270
Reflect Sprite about its vertical center and then rotate clockwise by 270 degrees. - TRANS_NONE
No transform applied to Sprite. - TRANS_ROT90
Rotate Sprite clockwise by 90 degrees. - TRANS_ROT180
Rotate Sprite clockwise by 180 degrees. - TRANS_ROT270
Rotate Sprite clockwise by 270 degrees.
This method throws IllegalArgumentException if the passed transform is invalid.
Source code for Walk
package com.blackberrydeveloperjournal.bone;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class Walk extends Sprite {
// sprite size in pixels
public static final int WIDTH = 23;
public static final int HEIGHT = 60;
// frame display sequence
private static final int[] PLAY_ORDER = { 0, 1, 2, 3, 4 };
// adjust hopping frame # of PLAY_ORDER as required
private static final int BONE_FRAME = 3;
// walking speed: 0 and 1 are the fastest,
// 2 and greater are slower
private static final int POLLING_PERIOD = 1;
// # of steps to reach top of hop
// adjust to alter hop height
private static final int GROUND_LEVEL = 7;
private int hopStage = GROUND_LEVEL;
private static int RIGHT = 1;
private static int LEFT = 0;
private int currentDirection = RIGHT;
private int xSprite, ySprite;
private int pointsScored = 0;
private int delayCounter = 0;
public Walk( int xOrigin, int yOrigin ) throws Exception {
super( Image.createImage( "img/walk.png"),
WIDTH, HEIGHT );
xSprite = xOrigin;
ySprite = yOrigin;
// set reference pixel to the center of the sprite
defineReferencePixel( WIDTH/2, 0 );
setRefPixelPosition( xSprite, ySprite );
setFrameSequence( PLAY_ORDER );
}
// make sprite hop if warranted
public void doHop() {
if ( hopStage == GROUND_LEVEL ) {
setFrameSequence( null );
setFrame( BONE_FRAME );
// trigger hop
hopStage--;
}
}
// handle sprite direction, animation and hopping
public void movement( int direction ) {
// default to right
int transform = TRANS_NONE;
int toMove = 1;
if ( direction == LEFT ) {
transform = TRANS_MIRROR;
toMove = -1;
}
// keep walker stationary as background moves
move( toMove, 0 );
// change direction is warranted
if ( currentDirection != direction ) {
currentDirection = direction;
setTransform( transform );
}
// delay movement based on bit match to POLLING_PERIOD
if ((delayCounter++ & POLLING_PERIOD)
== POLLING_PERIOD ) {
// not hopping
if ( hopStage == GROUND_LEVEL ) {
nextFrame();
} else {
int yPos = getRefPixelY();
// climbing
if ( --hopStage > 0 ) {
yPos += (hopStage * -hopStage);
// dropping
} else if ( hopStage != -GROUND_LEVEL + 1 ) {
yPos += (hopStage * hopStage);
// hop completed
} else {
pointsScored = 0;
hopStage = GROUND_LEVEL;
yPos = ySprite;
setFrameSequence( PLAY_ORDER );
}
setRefPixelPosition( getRefPixelX(), yPos );
}
}
}
// increase score for successful hop
public int bumpPoints() {
return ++pointsScored;
}
// decrease score on collision
public int isCollision( Missile missile ) {
int collisions = 0;
if ( collidesWith( missile, true )) {
missile.defaultState();
collisions++;
}
return collisions ;
}
// restore default state
public void defaultState() {
pointsScored = delayCounter = 0;
hopStage = GROUND_LEVEL;
setFrameSequence( PLAY_ORDER );
setRefPixelPosition( xSprite, ySprite );
setTransform( TRANS_NONE );
currentDirection = RIGHT;
}
}
The Walk class is responsible for the walking "stick man" Sprite. A few constants can be adjusted to reflect whatever you want to do with this code. WIDTH and HEIGHT reflect the dimensions of the Sprite image. In this case, each frame within the walk.png image is 23 pixels wide and 60 pixels high.
PLAY_ORDER, which is the frame display sequence, is set to play the Sprite in frame sequence. Feel free to adjust the sequencing if you believe that it looks better.
BONE_FRAME, which is set to array element 3 in the PLAY_ORDER, is the image used when the Spite "hops."
POLLING_PERIOD is set to quickly refresh the Sprite frame every second increment. Set more complex bit patterns if you want to slow it down even more.
currentDirection is set by default to make the Sprite walk towards the right.
GROUND_LEVEL is set to 7, which means that when the Sprite "hops", it will take 7 steps to reach the top, and another 7 steps to return to the original position.
Source code for Missile
package com.blackberrydeveloperjournal.bone;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.Random;
public class Missile extends Sprite {
private static final int WIDTH = 20;
private static final int HEIGHT = 20;
// ints are faster than booleans
private static final int YES = 1;
private static final int NO = 0;
private static final int RIGHT = 1;
private static final int LEFT = 0;
// speed of movement: 0 is fastest, 1 is slower
// other bit patterns will slow it down even more
private static final int POLLING_PERIOD = 1;
// adjust: range 2 to any positive value
// within reason where 2 is slowest
private static final int ATTACK_RATE = 5;
// adjust: a lower value means Spites return to game faster
private static final int RND_MAX = 25;
private Random rndObj = new Random();
private int rndCompare;
private int travelDirection;
private int hoppedOver;
private int spriteY;
private int delayCounter = 0;
public Missile( int direction ) throws Exception {
super( Image.createImage( "img/missile.png" ),
WIDTH, HEIGHT );
// create random value to test for
// Sprite's reentrance into the game
rndCompare = rndObj.nextInt( RND_MAX );
spriteY = BoneManager.HEIGHT - HEIGHT - 2;
hoppedOver = NO;
travelDirection = direction;
int transform = TRANS_NONE;
if ( travelDirection == RIGHT )
transform = TRANS_MIRROR;
setTransform( transform );
setVisible( false );
}
// move and otherwise maintain Sprite
public int movement( Walk walker, int leftEdge,
int rightEdge) {
int xRef = getRefPixelX();
int xRefPos = xRef + WIDTH;
int xRefNeg = xRef - WIDTH;
// hide Sprite when it moves beyond
// the edge of the display
if ( xRefNeg >= rightEdge || xRefPos <= leftEdge )
setVisible( false );
int ret = 0;
int toMove = 1;
// Sprite is visible and in play
if ( isVisible() ) {
// show next animated frame
if (( delayCounter++ & POLLING_PERIOD)
== POLLING_PERIOD )
nextFrame();
// precalc for speed and reduced code size
int walkX = walker.getRefPixelX();
boolean isMore = xRef > (walkX + walker.WIDTH );
boolean isLess = xRef < walkX;
boolean bump = false;
if ( travelDirection == RIGHT ) {
toMove = ATTACK_RATE;
if ( hoppedOver == NO && isMore ) {
hoppedOver = YES;
bump = true;
}
// leftward bound
} else {
toMove = -ATTACK_RATE;
if ( hoppedOver == NO && isLess ) {
hoppedOver = YES;
bump = true;
}
}
if ( bump )
ret = walker.bumpPoints();
move( toMove, 0 );
// Sprite is not visible
// test if it's time to let the Sprite back in the game
} else if ( rndObj.nextInt( RND_MAX ) == rndCompare ) {
int edge = leftEdge;
toMove = 1;
// place Sprite's outer edge on screem
if ( travelDirection == LEFT ) {
edge = rightEdge;
toMove = -1;
}
hoppedOver = NO;
setVisible( true );
setRefPixelPosition( edge, spriteY );
move( toMove, 0 );
}
return ret;
}
// restore Sprite to default state
public void defaultState() {
hoppedOver = NO;
delayCounter = 0;
setVisible( false );
}
}
The Missile class is responsible for the Sprite missiles that are hurtled towards the "stick man" Sprite from the right and left sides.
As before, a few constants have been provided to adjust as required. WIDTH and HEIGHT reflect the dimensions of the Sprite image. In this case, each frame within the missile.png image is 20 pixels square.
POLLING_PERIOD is set to quickly refresh the Sprite frame every second increment.
ATTACK_RATE is set to move the Missile Sprites 5 pixels each time. Adjust this to increase or decrease the speed of the missiles.
RND_MAX, which is set to 25, is used to set the highest number generated by the random number generator. A lower value means that Missile Sprites will return to the game faster, where a higher value will keep them out longer.
The constructor takes a single parameter used to set the direction that the Missile moves. Pass 1 for RIGHT and 0 for LEFT.
The movement() method handles movement of a Missile Sprite and takes three parameters: The Walk Sprite, and the left and right edges of display area.
A test is first made to determine if the missile has travelled beyond the display border. If so, then it is made invisible.
If the missile is still visible, then a series of events may occur.
To start, the next Sprite frame is displayed if the delayCounter matches the bit pattern of the POLLING_PERIOD.
After that, some calculations are made to increase speed, reduce code size and improve readability.
First, the x coordinate of the Walk Sprite's reference pixel is used to set two boolean values to true if the Walk Sprite managed to hop over the Missile Sprite. These booleans are indirectly used later to increase the game points.
The Missile Sprite is also moved by ATTACK_RATE pixels in the direction of movement.
If the Missile Sprite is not visible, then a test is made to determine if it's okay to let the Missile back in the game. If so, then the Missile's reference pixel is set, it is made visible, and is moved by one pixel in the direction it is travelling.
The final method in this class, defaultState(), resets the Missile Sprite to its default state.
The Bone Class
The following is the main Bone class that takes care of all the pedestrian duties of the application such as creating menus, starting, pausing and stopping the application, and displaying exceptions. The code is fairly straightforward so I won't go into detail about it afterwards.
Source code for Bone
package com.blackberrydeveloperjournal.bone;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class Bone extends MIDlet implements CommandListener {
private boolean MENU_ADD = true;
private boolean MENU_REMOVE = false;
private boolean pauseMenuUsed = MENU_REMOVE;
private boolean playMenuUsed = MENU_REMOVE;
private boolean replayMenuUsed = MENU_REMOVE;
private Command gamePlay = new Command
( "Play", Command.SCREEN, 1 );
private Command gamePause = new Command
( "Pause", Command.SCREEN, 1 );
private Command gameReplay = new Command
( "Replay", Command.SCREEN, 1 );
private Command gameExit = new Command
( "Exit", Command.EXIT, 10 );
private BoneThread gameThread;
private BoneCanvas gameCanvas;
public Bone() {
try {
gameCanvas = new BoneCanvas( this );
gameCanvas.addCommand( gamePause );
pauseMenuUsed = true;
gameCanvas.addCommand( gameExit );
gameCanvas.setCommandListener( this );
} catch( Exception err ) {
showException( err );
}
}
// signals the MIDlet that it has entered the Active state
public void startApp() throws MIDletStateChangeException {
if ( gameCanvas != null ) {
if (gameThread != null ) {
handleMenuPlay( MENU_REMOVE );
handleMenuPause( MENU_ADD );
gameCanvas.getKeyStates();
gameThread.restart();
} else {
gameThread = new BoneThread( gameCanvas );
gameCanvas.canvasBegin();
gameThread.start();
}
}
}
// signals the MIDlet to stop and enter the Paused state
public void pauseApp() {
// pause game
if ( gameThread != null )
gameThread.pause();
// adjust menu to allow restart of game
if ( gameCanvas != null ) {
handleMenuPause( MENU_REMOVE );
handleMenuReplay( MENU_REMOVE );
handleMenuPlay( MENU_ADD );
}
}
// signals the MIDlet to terminate and
// enter the Destroyed state
public void destroyApp( boolean unconditional )
throws MIDletStateChangeException {
// stop game
if ( gameThread != null )
gameThread.stop();
// deactivate objects and then call garbage collector
gameCanvas = null;
gameThread = null;
System.gc();
}
// handle play, pause, replay and exit commands
public void commandAction( Command c, Displayable d ) {
// start the game
if ( c == gamePlay ) {
handleMenuPlay( MENU_REMOVE );
handleMenuPause( MENU_ADD );
gameCanvas.getKeyStates();
gameThread.restart();
}
// pause the game
if ( c == gamePause ) {
handleMenuPause( MENU_REMOVE );
handleMenuPlay( MENU_ADD );
gameThread.pause();
}
// restart the game
if ( c == gameReplay ) {
handleMenuReplay( MENU_REMOVE );
handleMenuPause( MENU_ADD );
gameCanvas.canvasInitialize();
gameThread.restart();
}
// quit the game
if ( c == gameExit || c == Alert.DISMISS_COMMAND ) {
try {
destroyApp( false );
notifyDestroyed();
} catch ( MIDletStateChangeException err ) {
showException( err );
}
}
}
// display exception
public void showException( Exception err ) {
String theError = (err.getMessage() == null)
? err.getClass().getName()
: err.getClass().getName() + " - "
+ err.getMessage();
Alert promptError = new Alert( "Exception", theError,
null, AlertType.ERROR );
promptError.setCommandListener( this );
promptError.setTimeout( Alert.FOREVER );
Display.getDisplay( this ).setCurrent( promptError );
}
// add or remove Play menu item
private void handleMenuPlay( boolean enable ) {
if ( enable == MENU_ADD
&& playMenuUsed == MENU_REMOVE ) {
gameCanvas.addCommand( gamePlay );
playMenuUsed = MENU_ADD;
} else if ( enable == MENU_REMOVE
&& playMenuUsed == MENU_ADD ) {
gameCanvas.removeCommand( gamePlay );
playMenuUsed = MENU_REMOVE;
}
}
// add or remove Pause menu item
private void handleMenuPause( boolean enable ) {
if ( enable == MENU_ADD &&
pauseMenuUsed == MENU_REMOVE ) {
gameCanvas.addCommand( gamePause );
pauseMenuUsed = MENU_ADD;
} else if ( enable == MENU_REMOVE &&
pauseMenuUsed == MENU_ADD ) {
gameCanvas.removeCommand( gamePause );
pauseMenuUsed = MENU_REMOVE;
}
}
// add or remove Replay menu item
private void handleMenuReplay( boolean enable ) {
if ( enable == MENU_ADD &&
replayMenuUsed == MENU_REMOVE ) {
gameCanvas.addCommand( gameReplay );
replayMenuUsed = MENU_ADD;
} else if ( enable == MENU_REMOVE &&
replayMenuUsed == MENU_ADD ) {
gameCanvas.removeCommand( gameReplay );
replayMenuUsed = MENU_REMOVE;
}
}
// called externally
public void setMenuStart() {
handleMenuPause( MENU_REMOVE );
handleMenuPlay( MENU_REMOVE );
handleMenuReplay( MENU_ADD );
}
}
The BoneThread Class
The final class handles the game thread that is responsible for all gaming activity. The code is even more straightforward than the Bone class and doesn't need further explanation.
Source code for BoneThread
package com.blackberrydeveloperjournal.bone;
public class BoneThread extends Thread {
private BoneCanvas hCanvas;
private boolean requestStop = false;
private boolean requestPause = false;
// constructor
BoneThread( BoneCanvas target ) {
hCanvas = target;
}
// begin the game
public void run() {
hCanvas.getKeyStates();
requestStop = requestPause = false;
do {
while ( requestPause ) {
synchronized(this) {
try {
// make thread wait for another thread
// to invoke the notify() method or the
// notifyAll() method for this object
wait();
} catch( Exception err ) {}
}
}
// poll keys and move display layers
hCanvas.canvasRefresh();
// pause to allow other threads to operate
synchronized( this ) {
try {
// make thread wait for 1 millisecond or
// for another thread to invoke the
// notify() method or the notifyAll()
// method for this object
wait( 1 );
} catch( Exception err ) {}
}
} while ( !requestStop );
}
// pause game
public void pause() {
requestPause = true;
}
// restart game
public void restart() {
requestPause = false;
doNotify();
}
// stop game
public void stop() {
requestStop = true;
doNotify();
}
// wakes up a single thread that is waiting on this
// object's monitor
private void doNotify() {
synchronized( this ) {
notify();
}
}
}
The Final Components
The Walk Sprite image
The Missile Sprite image
The Turf TiledLayer image
The Bone icon
Summary
This article was written to make it easier for you to develop games. In documenting the Java ME Gaming API and providing working source code for the Bones game, you now should have a reasonable grasp of game development on devices that support Java ME and MIDP 2.0.
Download The Source
Please email your comments, suggestions and editorial submissions to