Implementation

For information about the design and functionality of the MIDlet, see section Design.

For information about the key aspects of implementing the MIDlet, see:

Splash screen

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;

Game engine

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.

Turning the car

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);

    }

Moving the car on the track

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.

Defining how the car behaves

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

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.

Defining the track

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

Concluding notes

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.