For information about the design and functionality of the MIDlet, see section Design.
For information about the key aspects of implementing the MIDlet, see:
The MIDlet uses a splash screen to provide the user immediate feedback that the MIDlet is about to start. The basic idea is to load the splash screen, show it, and then start loading all the other content in a separate thread. After everything has been loaded, the MIDlet shows the main menu.
/* * first show a splash screen */ SplashView splashView = new SplashView(); display.setCurrent(splashView); /* * load other views in the background */ new Thread() { public void run() { gameView = new GameView(); // enable gestures needed in GameView gesturesSupported = GestureProvider.enableGestures(); menuView = new MenuView(); settingsView = new SettingsView(); try { sleep(1000); } catch (InterruptedException e) { } showMenu(); // when ready, show menu try { join(); } catch (InterruptedException e) { } } }.start();
The entire game engine is a GameCanvas
class. When developing a MIDlet
for a simple device, it is best not to create a game engine that is
too object-oriented, but rather create C-style code in which much
of the data are just simple variables. This provides much better performance.
However, the resulting code is not as readable.
The game engine uses a few threads and a timer. The engine runs a lap timer thread and a game thread in the background. The game thread updates different things with different frequencies. The following code provides an example of how this is implemented:
gameTimer = new Timer(); g = getGraphics(); TimerTask coordAndUi = new TimerTask() { public void run() { updateCoordinates(); track.checkGatePass(carX, carY); render(g); flushGraphics(); } }; TimerTask turning = new TimerTask() { public void run() { if (!stopped) { turnByRotate(); turnByKeyPress(); } } }; TimerTask physics = new TimerTask() { public void run() { int frame = lastFrame; updateForce(frame); lastFrame = currentFrame; updateFriction(); } }; gameTimer.schedule(coordAndUi, 0, 40); gameTimer.schedule(turning, 0, 75); gameTimer.schedule(physics, 0, 200); raceTimer = new Timer(); clock = new TimerTask() { public void run() { currentLapTime += 10; } };
The engine renders the UI every 40 ms, which means the maximum frames per second (FPS) is 25. The engine runs turning and physics updates less often.
All the threads and tasks are
started in the showNotify
method of the GameCanvas
and stopped in the hideNotify
method to ensure that when the
game is inactive (when there is a incoming call or the screen saver
is activated), it does not use any CPU cycles.
The car is implemented as a Sprite
class. The sprite image shows the
car in 36 different positions. In other words, the car is turned 10
degrees between every picture.
Turning the car is simple. On
Series 40 touch and type devices, the MIDlet checks the direction
of the drag gesture with the gesture API and then changes the frame
of the car sprite to match. The following code registers a Canvas
-based class to receive gesture events:
GestureInteractiveZone giz = new GestureInteractiveZone(GestureInteractiveZone.GESTURE_DRAG); GestureRegistrationManager.register(Main.getInstance().getGameView(), giz); GestureRegistrationManager.setListener(Main.getInstance().getGameView(), this);
The above code first defines which
gestures are received (in this case, only drag gestures are received),
then registers the Canvas
with the defined gestures,
and finally sets the class that receives the gestures for the specified Canvas
. In this case, the Canvas
itself
receives the gestures, that is, it implements the GestureListener
interface.
The following
code implements the GestureListener
interface:
public void gestureAction(Object container, GestureInteractiveZone gestureZone, GestureEvent gestureEvent) { switch (gestureEvent.getType()) { /* * rotate car by dragging */ case GestureInteractiveZone.GESTURE_DRAG: long current = System.currentTimeMillis(); if (lastTimestamp == 0) { lastTimestamp = current; } /* * if gesture takes over 20 ms time, handle it */ if ((current - lastTimestamp) > 20) { handleDrag(gestureEvent); lastTimestamp = current; } break; } } private void handleDrag(GestureEvent event) { int startX = event.getStartX(); int xDistance = event.getDragDistanceX(); int endX = startX + xDistance; xDistance = Math.abs(xDistance); /* * check if drag was longer than 5 pixels */ if (xDistance > 5) { Main.getInstance().getGameView().handleDrag(startX, endX); } }
Note that the MIDlet has to wait 20 ms before
actually handling the drag. This is because when you drag your finger
across the screen, the MIDlet actually receives 5-10 drag events.
If the MIDlet does not wait before turning the car, the car turns
too quickly and driving it becomes impossible. The handleDrag
method also filters very short drag events so that accidental touches
to the screen do not turn the car.
On Series 40 full touch devices,
which support the Mobile Sensor API and the landscape mode, the MIDlet
adds the tilt value to a cumulative tilt value every time the turning TimerTask
is run. The more you tilt the device, the larger
the tilt value. When the cumulative tilt value reaches a threshold
value, the MIDlet rotates the car. The following code snippet enables
the accelerometer sensor and handles turning:
private void enableSensors() { if (accelerationProvider != null) { accelerationProvider.close(); } /* * add acceleration listener */ accelerationProvider = AccelerationProvider.getProvider( new AccelerationProvider.Listener() { public void dataReceived(double ax, double ay, double az) { if (isSensorTurning()) { tilt = (int) ay; } } }); if (accelerationProvider != null) { Main.sensorsSupported = true; sensorsEnabled = true; } else { Main.sensorsSupported = false; sensorsEnabled = false; } } private void turnByRotate() { cumulativeTilt += tilt; /* * When cumulative tilt reaches +-4, rotate happens. The more device is rotated, the faster tilt increases/decreases. */ if (isSensorTurning()) { if (cumulativeTilt > 4) { turnRight(); cumulativeTilt = 0; } if (cumulativeTilt < -4) { turnLeft(); cumulativeTilt = 0; } } }
For example, the following code implements the turnRight
method, which turns the car to the right by one
frame:
protected void turnRight() { lastFrame = currentFrame; if (currentFrame == carSprite.getFrameSequenceLength() - 1) { currentFrame = 0; } else { currentFrame++; } carSprite.setFrame(currentFrame); }
The turnRight
method simply moves
the car sprite to the next frame. The only difference to the corresponding turnLeft
method is that the latter decrements the currentFrame
variable:
protected void turnLeft() { lastFrame = currentFrame; if (currentFrame == 0) { currentFrame = carSprite.getFrameSequenceLength() - 1; } else { currentFrame--; } carSprite.setFrame(currentFrame); }
If the Gesture API is not supported on the device, the user can also turn the car using the navigation keys (or keys 4, 6, and 7).
To produce the effect of a car moving on the track, the MIDlet maintains three sets of coordinates:
Camera coordinates
Track coordinates
Car coordinates
The track is positioned at the origo, while the car's starting
coordinates are defined in the Track
classes. The
car coordinates are then updated depending on the speed of the car.
The MIDlet ensures that the car coordinates do not move outside the
track. The camera coordinates follow the car coordinates, and the
MIDlet renders everything so that the camera is in the center of the
screen. If the car moves near a track boundary, the camera coordinates
do not follow the car coordinates all the way, since the track image
must always fill the entire background.
Figure: The car moving on and off the track
The car in the game feels just like
a real race car in that it constantly slides a bit when turned. To
produce this effect, the MIDlet actually updates the force and car
angle less often than the car coordinates (depending on the speed
of the car). This updates the x and y increment values that are used
to move the car. The values are calculated using the current frame
of the car sprite. Since every frame is 10 degrees, multiplying the currentFrame
variable by 10 provides the current angle of
the car. The MIDlet then retrieves the speed of the car and uses basic
trigonometry to determine where the x and y increments should be when
the car is at the given angle. Calculating the increments require
the use of sin and cos values on that current car angle. Since calculating
these values on the fly would significantly slow the method, the angles
are pre-calculated in the GameView
class constructor.
The pre-calculation is possible because the car can only have 36 different
angles.
The following code implements the updateForce
method:
private void updateForce(int frame) { if (frame == -1) { frame = currentFrame; } if (applyGas) { if (speed < MAX_SPEED) { speed++; } } else if (!applyGas && speed > friction) { speed -= 2; } int speedCalc = ((speed + surfaceMultiplier) - friction); if (speedCalc < 0) { speedCalc = 0; } /* * calculate speed components */ yIncrement = (int) (speedCalc * coss[frame]); xIncrement = (int) (speedCalc * sins[frame]); if (speed >= 0 && speed <= 12) { int s = speed * 11 / MAX_SPEED; if (s < 0) { s = 0; } if (!applyGas && speed == friction) { s = 0; } speedNeedle.setFrame(s); } }
The sin and cos values required in the increment calculations are received from two arrays which hold the pre-calculated values. The calculations are inserted in a way that they can be fetched with the frame number. The following code implements the pre-calculation method:
private void preCalculate() { int frames = carSprite.getFrameSequenceLength(); /* * precalculate angles */ sins = new double[frames]; coss = new double[frames]; for (int i = 0; i < frames; i++) { int angle = 360 / frames * i; sins[i] = Math.sin(Math.toRadians(angle)); coss[i] = Math.cos(Math.toRadians(angle)); } }
Slowing the car down when off
track is a simple feature to implement. To determine the surface under
the car, the MIDlet checks the RGB value under the car. The updateFriction
method in the GameView
class
performs the check.
private void updateFriction() { /* * get the color of the ground under center of the car */ int[] rgb = new int[1]; track.getTrackImage().getRGB(rgb, 0, 1, carX, carY, 1, 1); int currentRGB = rgb[0]; /* * rgb is in format 0xAARRGGBB where AA is alpha channel values */ int red = (currentRGB & 0x00FF0000) >> 16; int green = (currentRGB & 0x0000FF00) >> 8; int blue = (currentRGB & 0x000000FF); /* * if the color values are less than 70, there is probably asphalt */ if (red < 70 && green < 70 && blue < 70) { surfaceMultiplier = 0; } else { surfaceMultiplier = -2; } }
The method first determines where the car actually
is on the track. Then, it requests the RGB value under the center
of the car. Retrieving the RGB value is simple because it is supported
by the basic Image
class. Because the RGB value is
in hexadecimal format, the method shifts the bytes accordingly to
get the red, green, and blue values. To check whether the car is off-track,
the method compares the color values to the values of the dark-colored
track. If the values are smaller, it means the car is on the track
and the surfaceMultiplier
, which directly affects
the speed of the car, can be incremented.
The Track
class encapsulates
the track image, the car's starting position, and the lap feature.
The Track
class is meant to be sub-classed, but this
is not enforced. The Monza
, Oval
, and EightLoop
classes all follow the same pattern:
They first set the laplistener
, then set the track
image and define the gates, and finally define starting position and
frame of the car.
Because the track is just an image, the MIDlet
must track the car to determine whether the car has stayed on the
road enough to get a full lap. This is done by defining "gates" on
the track. To drive a full lap, the car must go through all the gates.
When the car moves along the track, the MIDlet monitors how far it
is from a gate. The gates are monitored in order, meaning that in
the beginning of the game, the first gate is monitored, and until
the car has driven through that gate, the monitoring of the next gate
does not begin. When the car goes through the last gate, the laplistener
is notified that a full lap has been completed.
Monitoring is done by checking if the car is inside a quadrangle defined
by the two gate points (+5 pixels per each direction).
The following code implements the basic logic for passing the gates:
public void checkGatePass(int x, int y) { if (currentGate == null) { currentGate = (Gate) gates.elementAt(currentGateIndex); } if (currentGate.isPointNear(x, y)) { currentGate.setPassed(true); currentGateIndex++; if (currentGateIndex > gates.size() - 1) { laps++; listener.lapDriven(laps); currentGateIndex = 0; /* * set the last gate passed for a moment before resetting */ new Thread() { public void run() { try { sleep(500); } catch (InterruptedException e) { } resetGates(); } }.start(); } currentGate = (Gate) gates.elementAt(currentGateIndex); } }
The actual calculation is done in the isPointNear
method of the Gate
class:
public boolean isPointNear(int x, int y) { int rightX, leftX; int topY, bottomY; if (startX > endX) { rightX = startX + 5; leftX = endX - 5; } else { rightX = endX + 5; leftX = startX - 5; } if (startY > endY) { topY = startY + 5; bottomY = endY - 5; } else { topY = endY + 5; bottomY = startY - 5; } return x > leftX && x < rightX && y > bottomY && y < topY; }
When porting the MIDlet to Series 40 full touch UI, the most important things to consider are the missing keys and changed resolution. Since Series 40 full touch devices have a bigger screen and support the landscape mode and the Mobile Sensor API, the Series 40 full touch version of the MIDlet locks the UI in landscape mode and uses tilting for turning the car. Unlike drag-based turning, tilting does not prevent adding a brake button in the UI, so one was added to the bottom-left corner of the screen.
The JAD file of the Series 40 full touch version sets the MIDlet UI to landscape mode, so that if the device supports landscape mode, as Series 40 full touch devices do, the UI shows landscape versions of some images. The following code snippets checks whether the UI is in landscape mode and sets a flag accordingly:
public View() { super(false); setFullScreenMode(true); screenWidth = this.getWidth(); screenHeight = this.getHeight(); if (screenHeight > screenWidth) { Main.landscape = false; } else { Main.landscape = true; } }
The images are positioned relatively, which means that the landscape mode requires minimal hard-coding:
topBar.setPosition((w - topBar.getImage().getWidth()) / 2, 0);
We must consider media as well. For example, the model 308 doesn’t
support RateControl
, therefore, there is a check
if it’s supported. Otherwise, the game has monotone engine sound.
/* * init playback rate controller */ try { rateControl = (RateControl) player.getControl("RateControl"); throttle = rateControl.getMinRate(); rateControl.setRate(throttle); rateControlSupported = true; } catch( Exception e ) { rateControlSupported = false; }
In the regular version of the MIDlet,
the FPS rate is quite good on slower devices, such as Nokia Asha 306
on Nokia Asha software platform, but the game nonetheless seems laggy.
Every time the user touches the screen, the game lags. The problem
remains even after all the GestureListeners
are disabled
and all the touch handling code commented out. Turns out that touching
the screen blocks the separate thread that updates the car coordinates.
However, the rendering takes place inside a TimerTask
, which is not blocked. This results in a quite good FPS rate even
though the game itself feels laggy. The Series 40 full touch version
of the MIDlet removes this lag problem by placing the car coordinates
update also inside a TimerTask
.
The regular version of the MIDlet has only 24 different angles in the car sprite. To improve the gameplay experience, the Series 40 full touch version of the MIDlet uses 36 different angles.
For the Series 40 full touch version of the MIDlet, the PNG files were processed with the OptiPNG application, which decreased the size of the images about 70 kB. However, adding support for the landscape mode required some new images to be added, increasing the overall MIDlet package size.
In the regular version of the MIDlet, checking if the car has passed the next gate on the track is implemented in a relatively complicated way. The Series 40 full touch version of the MIDlet uses a simpler and faster solution. However, even though the new method is faster, the difference is not significant.
When developing a game to a device with limited resources, always look for little tricks to avoid heavy calculations as much as possible. Use threads to ensure that the UI runs smoothly because it is the most important aspect of the whole game.
The Racer MIDlet is relatively simple, so there is plenty of room for improvement, for example:
The tracks could be tile-based, that is, they could be assembled from numerous small pictures. This would allow you to create different kinds of surfaces on which the car would behave differently, for example oil spills, bumps, and pools of water.
To expand the gaming experience to include a social aspect, you could create a feature that allows the user to upload their best lap time to the Internet and there compare it with other users' best lap times.