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.
Splash splash = new Splash(); display.setCurrent(splash); new Thread() { public void run() { // Load everything ... display.setCurrent(menu); // ... }.start(); splash = null;
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.
When developing a game, always remember to separate all the components to dedicated threads. This allows for a smoother gaming experience. In the current MIDlet, the following functions have been separated to dedicated threads:
UI
Map and car related calculations
Lap related calculation
Lap timer
The following code provides an example of how these functions are implemented:
TimerTask ui = new TimerTask() { public void run() { render(g); flushGraphics(); } }; gameLogic = new SimpleThread(40) { public void execute() { if (!stopped) { calculateX(); calculateY(); checkSurface(); handleInstructions(); } } }; TimerTask speedUpdate = new TimerTask() { public void run() { int frame = lastFrame; updateForce(frame); lastFrame = currentFrame; } }; TimerTask keyturning = new TimerTask() { public void run() { if (turnRight) { turnRight(); } else if (turnLeft) { turnLeft(); } } }; gateKeeper = new SimpleThread(40) { public void execute() { track.checkGatePass((-trackX) + (carXPosition + 32), (-trackY) + (carYPosition + 32)); } }; gameLogic.start(); gateKeeper.start(); mTimer.schedule(ui, 0, 40); mTimer.schedule(speedUpdate, 0, 200); mTimer.schedule(keyturning, 0, 100); raceTimer = new Timer(); clock = new TimerTask() { public void run() { currentLap += 10; } };
The UI thread is a TimerTask
scheduled to run every 40 ms. The key point is always to remember
to keep the UI separated from all the heavy calculations.
The speedUpdate
is another TimerTask
that runs
in the same thread as the UI. It does not slow down the UI, because
it is ran every 200 ms, which means that the UI has time to run five
times before this task is run.
All the other threads are SimpleThread
classes. SimpleThread
is a
simple wrapper for a regular Thread
that takes a
sleep time in milliseconds as a parameter to its constructor. It can
be compared to creating a TimerTask
, creating a new Timer
for that task, and scheduling it to run at a given
time.
public abstract class SimpleThread extends Thread{ private boolean running = false; private long timeout = 0; public SimpleThread(long milliseconds) { timeout = milliseconds; } public abstract void execute(); public void run() { running = true; while(running) { execute(); try { sleep(timeout); }catch(InterruptedException e) { //do nothing } } } public void stop() { running = false; } }
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 24 different positions. In other words, the car is turned 15
degrees between every picture.
Figure: Car positions
Turning the car is simple: 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_ALL); touchGameView = new TouchGameView(); //touchGameView.setTrack(new Seissenberg(touchGameView)); GestureRegistrationManager.register(touchGameView, giz); GestureRegistrationManager.setListener(touchGameView, touchGameView);
The above code first defines which gestures are received (in this
case, all 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, implements the GestureListener
interface.
The following
code implements the GestureListener
interface:
public void gestureAction(Object container, GestureInteractiveZone gestureZone, GestureEvent gestureEvent) { switch (gestureEvent.getType()) { case GestureInteractiveZone.GESTURE_DRAG: long current = System.currentTimeMillis(); if (lasttimestamp == 0) { lasttimestamp = current; } if ((current - lasttimestamp) > 20) { handleDrag(gestureEvent); lasttimestamp = current; } break; case GestureInteractiveZone.GESTURE_TAP: int x = gestureEvent.getStartX(); int y = gestureEvent.getStartY(); if(x <= 60 && y <= 30) { reset(); Main.getInstance().showMenu(); } break; } } private void handleDrag(GestureEvent event) { int start_x = event.getStartX(); int Xdistance = event.getDragDistanceX(); int result = start_x + Xdistance; Xdistance = Math.abs(Xdistance); if (Xdistance > 5) { if (speed > friction) { if (result > start_x) { turnRight(); } else { turnLeft(); } } } }
Note that the MIDlet has to wait 20 ms before
actually handling the drag. This is because when the user drags their
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.
For example, the following
code implements the turnRight
method:
protected void turnRight() { lastFrame = currentFrame; if (currentFrame == 23) { 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 it decrements the currentFrame
variable:
protected void turnLeft() { lastFrame = currentFrame; if (currentFrame == 0) { currentFrame = 23; } else { currentFrame--; } carSprite.setFrame(currentFrame); }
To produce the effect of a car moving on the track, the MIDlet actually moves the track under the car. The car is constantly positioned so that more or less 2/3 of the screen is in front of it. When the car gets near the track edge, the MIDlet starts moving the car until the car turns to a direction that requires more track to be shown. The following figure illustrates the behavior of the car. On the left, the car stays still and the map is moved. On the right, the edge of the map is reached and the car moves but the track stays still.
Figure: The car moving on and off the track
In the GameView
class, thecalculateX
and calculateY
methods implement this behavior.
These methods are run in a separate thread because of their heavy
logic. If moving the track image causes less than the screen size
of the track to be shown, the car moves. Otherwise, the track moves.
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 a couple
of frames behind what is drawn. Therefore, the speedUpdate
TimerTask
runs every 200 ms (whereas the UI task
runs every 40 ms).
The speedUpdate
task calls
the updateForce
method. This updates the x
and y
increment values that are used
to move the car. These values are calculated using the current frame
of the car sprite. Every frame is 15 degrees, so multiplying the currentFrame
variable by 15 provides the current angle of
the car. The speed of the car is calculated first, followed by using
basic trigonometry to calculate what the x
and y
increments need to be when the car is in a certain angle.
Calculating the increments requires using the sin
and cos
values on the current car angle. To prevent
this from slowing down the application, the angles are pre-calculated
in the GameView
constructor. The pre-calculation
is possible because the car only has 24 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 += 1; } } else if (!applyGas && speed > friction) { speed -= 2; } int speedCalc = ((speed + surfaceMultiplier) - friction); if (speedCalc < 0) { speedCalc = 0; } y_increment = (int) (speedCalc * coss[frame]); x_increment = (int) (speedCalc * sins[frame]); if(speed >= 0 && speed <= 12) { int s = speed - 1; 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() { //calculate angles sins = new double[24]; coss = new double[24]; for (int i = 0; i < 24; i++) { int angle = 15 * 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 checkSurface
method in the GameView
class
performs the check.
private void checkSurface() { int[] rgb = new int[1]; int absX = (trackX < 0) ? -trackX : trackX; int absY = (trackY < 0) ? -trackY : trackY; track.getTrackImage().getRGB(rgb, 0, 1, absX + carXPosition + 32, absY + carYPosition + 32, 1, 1);//32 is the half of car images width and height int currentRGB = rgb[0];//rgb is in format 0xAARRGGBB where AA is alpha channel values //check if the values of rgb are greater than int red = (currentRGB & 0x00FF0000) >> 16; int green = (currentRGB & 0x0000FF00) >> 8; int blue = (currentRGB & 0x000000FF); curRed = red; curGreen = green; curBlue = blue; if (red > 70 && green > 70 && blue > 70) { surfaceMultiplier = -2; } else { surfaceMultiplier = 0; } }
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 larger, it means the car is on the grass
and the surfaceMultiplier
, which directly affects
the speed of the car, can be incremented.
The Track
class encapsulates
the track image, the starting position of the car, and the lap feature.
The Track
class is meant to be subclassed, but this
is not enforced. The Monza
, SeissenBerg
, 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.
The gate itself is just a line, and 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 calculating the
distance between the car and the gate.
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)) { currentGateIndex++; if(currentGateIndex > gates.size() -1) { laps++; listener.lapDriven(laps); currentGateIndex = 0; } currentGate = (Gate) gates.elementAt(currentGateIndex); } }
The actual calculation is done in the isPointNear
method of the Gate
class:
public boolean isPointNear(double x, double y) { point_x = (int)x; point_y = (int)y; double line_x = 0; double line_y = 0; boolean ret = false; double xdiff = end_x - start_x; double ydiff = end_y - start_y; double u = ((x - start_x)*xdiff + (y - start_y)*ydiff) /(xdiff*xdiff + ydiff*ydiff); if(u < 0) { line_x = start_x; line_y = start_y; }else if(u > 1) { line_x = end_x; line_y = end_y; }else { line_x = (start_x + u*xdiff); line_y = (start_y + u*ydiff); } temp_x = (int) line_x; temp_y = (int) line_y; double xdiff2 = line_x - x; double ydiff2 = line_y - y; double length = Math.sqrt((xdiff2*xdiff2 + ydiff2*ydiff2)); if(length <= LIMIT) ret = true; return ret; }
This algorithm is based on a mathematical formula that you can find here . The basic idea is to calculate the distance of the point from the gate, and then check if the distance is less or equal to the defined limit. If that is the case, that the car is over the gate.
You can see the defined gates by setting
the debug
member variable in track
class to true
. The following figure shows the game
running with gate debugging enabled.
Figure: Game running with gate debugging enabled
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.