WidSets Developer Rewards 2008

WidSets Game Tutorial

Step 2. Developing the game framework

Figure 8. Output after completing this step.

Introduction

In this tutorial step, we'll implement functionality that is common to most real-time games, leaving the paddle-and-ball game logic for the next step. This will allow you to use the widget completed at the end of this step as a template for many kinds of real-time games on WidSets.

We'll leave our configuration (widget.xml) and stylesheet (style.css) files unmodified from the previous step. All of the changes to our widget will be applied through additions to the WSL source code (paddle_game.he). We'll make step-by-step changes to the source code, documenting the changes along the way.

Game loop

The basic requirement of any real-time game is that the action should be dynamic. What this means in practice is that we need to set up some kind of game loop that gets called periodically. This will be responsible for:

  1. Calculating how much time has elapsed between the previous frame and this frame (delta).
  2. Handling input e.g. processing key events. In WidSets we will actually handle this outside of the game loop as you will see later.
  3. Moving our game objects. This should be done in accordance with the delta value.
  4. Collision detection and handling.
  5. Rendering the scene.

These five stages are present in most real-time games. However, the implementation of the last four stages will obviously be dependent on the nature of the game.

We need to execute our game loop at a fast rate in order to give the illusion of objects moving smoothly around the game field. A frame rate of 30 frames per second (FPS) or more is ideal (below 25 FPS things can start to look a bit jerky). Add the following to the WSL script source code after the declaration of other constants:

const int FPS_TARGET = 35;
const int DELAY = 1000 / FPS_TARGET;

These two constants represent the target frame rate (FPS_TARGET) for the widget and the desired delay (DELAY), in milliseconds, between each frame to achieve this target frame rate. Now add these to the list of variables:

Timer scheduler;
int tick;
long timeOld;
String fpsText = "?";
long fpsTimeOld;

These variables represent:

Now add the following two functions to the Functions section of the WSL source code:

void cancelTimer()
{
  if(scheduler != null)
  {
    scheduler.cancel();
    scheduler = null;
  }
}

void resumeGame()
{
  cancelTimer();
  timeOld = currentTimeMillis();
  scheduler = schedule(DELAY, DELAY, nextFrame);
}

This cancelTimer() function will be responsible for canceling our Timer object (scheduler). In effect, this will cancel further calls to our game loop function. We'll need to call cancelTimer() when we pause the game or when we close/remove the widget (thus freeing processor time for other widgets). The resumeGame() function will start (or restart) our Timer object. To ensure that there are no other scheduled tasks on the scheduler, we first cancel the timer (if it's not null). Then we set timeOld to the current time to ensure our FPS and delta calculations are accurate. Finally, we schedule our game loop function nextFrame() to execute at a fixed interval given by the period DELAY milliseconds starting DELAY milliseconds from now. Check out the various schedule(...) functions documented in the Apidocs for an overview of how to setup timers. From the documentation, you can see that the third argument of this version of the function has to be another function that implements the TimerCallback interface.

Let's now create the timer callback function nextFrame() i.e. our game loop:

void nextFrame(Timer t)
{
  /*-------
   | Tick |
   -------*/
  tick++;

  /*---------------
   | Move objects |
   ---------------*/
  // ...

  /*---------------------
   | Collision handling |
   ---------------------*/
  // ...

  /*---------
   | Render |
   ---------*/
  render();
}

void render()
{
  gameCanvas.repaint(false);
  flushScreen(false);
}

The first statement in our game loop simply increments the tick variable indicating that another frame is to be shown. We don't have any game specific logic yet, so there is no code for moving our objects or handling collision. However, we do call the function render() at the end of the game loop (defined just below nextFrame()). A call to render() results in the gameCanvas being marked for a redraw i.e. made "dirty". The flushscreen() method then requests that all objects marked up for redraw are repainted. In the case of our gameCanvas this results in our paint() function getting called (the paint() function represents the PaintCallback interface). Passing true to the function forces all UI objects to be redrawn). Please note that we are writing our code for readability and maintainability. When our widget is finished, we should probably inline many of the functions in the game loop, as this will be the main performance bottleneck in the game.

Let's alter the paint() function so that we can visually see that our game loop works. First, find the paint() function and delete the call to draw the string "Paddle Game" to the screen (left over from the previous step). Now alter the paint() function as follows (the ...'s represents existing code that shouldn't be altered):

void paint(Component c, Graphics g, Style s, int width, int height)
{
  ... Check for orientation change ...
  
  // Draw animated red circle
  g.setColor(0xFF0000);
  int d = min(width, height) / 2;
  g.fillArc((width - d) / 2, (height - d) / 2, d, d, 0, (tick % 90) * 4);
  
  // Display tick count
  g.setColor(0xFFFFFF);
  g.drawString("Tick = " + tick, 4, 4, TOP | LEFT);
}

The first two statements result in a filled red arc (think of a pie with different sized sections cut out) being displayed in the centre of the canvas. This just demonstrates how we can animate our graphics now we have a game loop (it isn't part of the game). The proportion of the pie shown depends on the value of tick. The Graphics section in the Apidocs gives a full list of the drawing methods available inside paint() (using the graphics context). The remaining two statements result in the value of tick (frame count) being displayed in the top left corner of the canvas in white text.

Now we need to call the function resumeGame() (defined earlier) in order to set our game loop running. Add the following to the openWidget() function just after the call to calculateViewport(...):

resumeGame();

Before we test out our code so far, we need to ensure that we stop the timer when we close, reload or remove the widget. Failure to do this may result in the timer running whilst other widgets are being used (wasting processor cycles, using up battery life and potentially slowing down the WidSets client and/or other widgets). Add this line to the top of the cleanup() function:

cancelTimer();

Try running the widget now (either in the emulator or on a real device). Figure 9 demonstrates what you should see.

Figure 9. The game loop in action.

Note that whilst the widget is open, the game loop continues to execute even if we open the menu. This is not ideal behaviour, so let's fix that..

Game state

It is probably safe to assume that when the user opens up the game menu, they want the game to pause. In the remaining part of this tutorial step we will implement game state, so that we can start, restart, pause and un-pause our game according to the actions of the user. The following state transitions diagram (Figure 10) illustrates how we want our widget to behave (also see Figure 8):

state transition diagram

Figure 10. State transition diagram for the game framework.

Let's introduce a variable to keep track of whether the game is paused. Add the following to the class variables (the Variables section):

boolean isPaused;

Note that instead of using this variable, we could perhaps just check if the scheduler is null.

Now add the following code to the top of the resumeGame() function:

isPaused = false;

Now we need to add a function that's complementary to resumeGame():

void pauseGame()
{
  isPaused = true;
  cancelTimer();
  render();
}

This code is relatively straightforward. First we set the isPaused variable to true, then we cancel our scheduler. Finally we call render() to repaint the canvas. We have to force a redraw in order to ensure the paused message is drawn. Add the following code to the end of paint():

if(isPaused)
  g.drawString(
    "Paused (press FIRE)",
    width / 2, height / 2,
    BOTTOM | HCENTER);

When isPaused is true we display the message "Paused (press FIRE)" in the centre of the canvas. So, when do we actually call pauseGame()? Looking at the state transition diagram (Figure 10) you can see that we pause the game when the menu is opened. Therefore, add the following statement to the top of getMenu() (remember this is a Script interface function):

pauseGame();

We should also automatically pause the game when we detect that the display orientation has changed. Change the code at the top of the paint() function to the following:

int sw, int sh = getScreenSize();
if(sw != screenWidth)
{
  pauseGame();
  calculateViewport(sw, sh);
  gameFlow.repaint(true);
  flushScreen(true);
}

We've basically altered the existing code so that when the display orientation changes, the game is paused (if it isn't already paused) and the whole UI is repainted. We have to repaint everything because the new screen orientation may require us to rescale the game area (through a call to calculateViewport(...)).

From the state transition diagram you can see that we should resume (un-pause) the game when the FIRE or '5' key is pressed. Modify the keyAction() interface function:

boolean keyAction(Component source, int op, int code)
{
  if(source.equals(gameShell))
  {
    if(op == KEY_PRESSED)
    {
      switch(code)
      {
        case KEY_FIRE:
        case '5':
        if(isPaused)
          resumeGame();
      }
    }
    return true;
  }
  return false;
}

We first check whether gameCanvas is currently focused. If it is not then the function returns false, meaning the key event was not consumed and can be processed elsewhere. If it is true, then we can assume that this key event corresponds to some kind of game action and we use a switch statement to check which key was pressed. If it was the FIRE or '5' key, and the game is in the paused state (Figure 10), we call resumeGame() to start (or resume) the game action.

It's now time to implement the "New game" option in the menu. Add the following to the Functions section:

void newGame()
{
  tick = 0;
  
  // Reset game variables
  // ...
  
  pauseGame();
}

When we create our game objects we'll reset them in the newGame() function. For now, we'll just reset the frame count (tick) and ensure the game is paused.

In the actionPerformed() interface function replace the setBubble(...) call in the CMD_NEW_GAME switch case with a call to newGame(). Also replace the resumeGame() function call in openWidget() to newGame() – this ensures we reset our game when we open up the widget. The complete code listing for this step can be found at the bottom of the page.

Calculating frame rate

We have nearly developed a generic framework suitable for any kind of real-time game on WidSets. To finish off, let's implement a method of calculating frames per second (FPS) in our games. We have already introduced all of the variables we need to track FPS performance (at the beginning of this tutorial step). Now add the following just beneath the nextFrame() function:

int tick()
{
  long timeCurr = currentTimeMillis();
  
  // Calculate frame rate
  if((++tick & 127) == 0)
  {
    int fps = 128000 / (timeCurr - fpsTimeOld);
    fpsTimeOld = timeCurr;
    fpsText = fps + " / " + FPS_TARGET;
  }
  
  int delta = int(timeCurr - timeOld);
  timeOld = timeCurr;
  	
  // Ensure maximum of 80 milliseconds
  return min(delta, 80);
}

This function is designed to be called once every frame. The function works as follows. First we assign the current time to timeCurr with a call to the API function currentTimeMillis(). Then, using an if statement, we use a bitwise mask to check if the last 6 bits (26+1 - 1 = 127) of the integer tick equals zero (this is quick binary trick). This condition is met once every 128 frames. Using this information, together with the timestamp of the last FPS calculation, we determine the new FPS count and set fpsTest accordingly. After the if statement we determine the time difference between the last frame and the current frame and assign this to delta. Finally we return delta, ensuring that the value is no greater than 80 milliseconds (to prevent the occasional anomalous delta value from messing up our game logic).

To make use of this new function, replace the tick increment statement in nextFrame() with the following:

int delta = tick();

We'll demonstrate how we can put delta to good use in the next tutorial step.

There's one last task to perform in this tutorial – displaying our FPS calculations on the canvas. Add the following to the end of paint():

g.setColor(0x00FF00);
g.drawString(fpsText, width, height, BOTTOM | RIGHT);

This should result in the string fpsText being shown in green in the bottom right of our game canvas (ignore the first reading given). Run the widget to check if your widget functions the same as shown in Figure 8.

Resources

Download the widget files for step 2

Next step

Step 3. Implementing the game logic

Tutorial links