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() { ... load views try { sleep(1000); } catch (InterruptedException e) { } showMenu(); // when ready, show menu try { join(); } catch (InterruptedException e) { } } }.start();
The whole game engine is a GameCanvas class. When programming for a simple device, do not create a very object oriented game-engine, but rather create more good old C-style code where the majority of the data is just simple variables. This results in much better performance, but the downside is that the code won't be as readable. The game uses some threads and a timer.
The lap timer thread and game thread run in the background. The game thread updates different things with different frequencies.
The threads are implemented as follows:
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 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 full touch devices (that support landscape and acceleration sensors) the MIDlet adds the tilt value to a cumulative tilt every time the turn timertask is executed. The more the device has been tilted, the bigger the tilt value. When the cumulative tilt value reaches the threshold value, the car is rotated. Note that a ClassNotFoundException will be thrown for devices that do not support sensors, when the getProvider method is called, which will not allow the accelerationProvider to be initialized. The application will then use gestures only in order to move the car. The sensorsPromptDeclined boolean variable is used to toggle the help text that shortly appears during the count down before the game starts, on full touch devices, in the special case where the user has declined the sensor security prompt and therefore the gesture help text should appear instead of the sensor help text. It practically removes the text "Press 8 to brake" for full touch devices to distinguish the message from the one used in touch and type that do not support sensors. The following code enables sensors and demonstrates the process:
private void enableSensors() { if (!sensorsPromptDeclined) { if (accelerationProvider != null) { accelerationProvider.close(); } try { accelerationProvider = AccelerationProvider.getProvider( new AccelerationProvider.Listener() { public void dataReceived(double ax, double ay, double az) { if (isSensorTurning()) { tilt = (int) ay; } } }); } catch (Exception e) { if (e.getMessage().equals("could not open sensor")) { sensorsPromptDeclined = true; } } 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; } } }
Here is a snippet of the turnRight method:
protected void turnRight() { lastFrame = currentFrame; if (currentFrame == carSprite.getFrameSequenceLength() - 1) { currentFrame = 0; } else { currentFrame++; } carSprite.setFrame(currentFrame); }
As you can see, the turnRight method is very simple. It just moves the car sprite to the next. The only difference of the turnLeft method is that it decrements the currentFrame variable.
If gestures are not supported by the device, the car can be also rotated with the navigation buttons (or keys 4, 6, and 8).
To get the effect of the car moving on the track you have to maintain different coordinates. There are camera coordinates, track coordinates, and car coordinates. The track is positioned at the origo, and the car start coordinates are defined in Track classes. They are then changed depending on the speed of the car. It is ensured that the car coordinates do not get outside the track. The camera coordinates follow the car coordinates and everything is rendered so that the camera is in the center of the screen. If the car goes near the track boundaries, the camera coordinates do not follow them there because you want that the track image fills the whole background.
Figure: The car moving on and off the track
When programming for a very low-end phone you have to take shortcuts. If you want the car behave like a real car, the first thing that comes to mind is to try to calculate the forces that affect the car when gas is applied. This could work but would probably lead to numerous hours of calculation optimisation that in the end has no value to the user. The best way is to just fake the car behaviour.
The car in this game feels just like a normal racing car that slides a bit when turned. To get this effect you actually update the force and the car angle less often than the coordinates of the car (depending on the speed). This updates the x- and y-increment values which are used to move the car. The values are calculated using the current frame of the car sprite. You know that every frame is 10 degrees so when multiplying the currentFrame variable with 10 you get the current angle of the car. You then get the speed of the car and calculate with basic trigonometry what the x- and y-increments should be when the car is in a certain angle. Calculating the increments requires the use of sin and cos values of the current car angle. Calculating these on the fly would seriously slow down the method. To bypass this, the angles are precalculated in the GameView's constructor. Precalculation is possible because you know that the car can only have 36 different angles.
Here is the code for 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); } }
As you can see, the increment calculations require the use of cos and sin values. The values you get form two arrays, coss and sins, which hold the precalculated values and the calculations are inserted so that you can fetch them with the frame number.
Here is the code of the precalutation 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)); } }
This is a very neat feature but also very simple to implement. To determine where the car is, the MIDlet can check the RGB value that is under the car. The checkSurface method in class GameView checks the RGB.
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; } }
Getting the RGB (in the center of the car) is very simple because it is supported in the basic Image class. Because the RGB value is in hex format you need to shift the bytes accordingly to get the red, green, and blue values. To simply check if the car is off track you need to compare the colour values to the values of the dark-coloured track. If the values are smaller, the MIDlet knows that the car is on the track and can increment the surfaceMultiplier which directly affects the car's speed.
The track class encapsulates the track image, the car's starting position, and the lap feature. The class is meant to be subclassed but it is not enforced in any way. If you look at any of the classes Monza, Oval, or EightLoop, you see a similar pattern: First set the laplistener, then set the track image, define the gates, and finally define the car's starting position and frame.
The lap feature
Because the track is just an image, the MIDlet has to somehow track if the car has stayed on the road to get a full lap. This is accomplished by defining "gates" to the track. To drive a full lap, the car must go through all the gates.
The gates are monitored in order, that is, when you start playing the MIDlet monitors the first gate but won't start to monitor the next gate and until you have driven through the first gate. When the car goes through the last gate, the MIDlet notifies the laplistener that a full lap has been driven. Monitoring is done by checking if the car is inside a quadrangle defined by two gate points (+ 5 pixels per each direction).
Here is the basic logic for the gatepassing:
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 full touch UI, the most important things that have to be considered are the missing keys and changed resolution. Full touch devices have a wider screen and they support the Sensor API, and hence in those devices the car turning is done by tilting the phone in landscape mode. Because tilting does not prevent a softkey brake button to be added (as drag-based turning does), one was added to the bottom-left corner of the screen.
The JAD file orders the game to be in landscape mode, so if the device supports landscape (as full touch devices do), landscape versions of some images are shown.
if (screenHeight > screenWidth) { Main.landscape = false; } else { Main.landscape = true; }
The images, for example, are positioned relatively so that minimum hard-coding for landscape mode was needed.
topBar.setPosition((w - topBar.getImage().getWidth()) / 2, 0);
The FPS rate of Racer was quite good on slower phones, such as Nokia Asha 306, but it still seemed to be quite laggy. Every time the screen was touched, the game lagged. Even though all gesture listeners were set off and all code in touch handling was commented, the problem remained. After some investigations it was found that touching the screen blocks the separate thread that updates the car coordinates. However, the rendering happened in a TimerTask that was not blocked. This resulted in a good FPS rate even though the game seemed to run in snatches. After putting the coordinate updating to a TimerTask, the problem disappeared.
In the original Racer there were 24 different angles in the car sprite. Increasing the number to 36 improved the game experience a little.
Running PNG files through OptiPNG program decreased the size of the images about 70 kilobytes. However, adding support for landscape required some new images to be added. The checking if the car has passed the next gate in track was implemented in quite a complicated way. It was changed to a simplified and faster solution (see the lap feature above). However, although the new method is faster, the difference is not significant.
When developing a game to a device that has limited resources you must always think of little tricks to avoid heavy calculations. You have to use threads to ensure that the UI is smooth because that will be the most important thing in the whole game. This game is in the end quite simple and there are lots of things you can develop. The tracks could be tile based, meaning that they would be assembled from a number of small pictures. This would allow all kinds of different surfaces where the car could behave differently, for example, oil spills, jumps, water, etc. You could create a feature where the user can upload the best lap time to the internet and compare it with others.