WidSets Developer Rewards 2008

WidSets Game Tutorial

Step 1. Creating a widget

Figure 4. Output after completing this step.

Widget files

Create a directory called widgets within the SDK installation directory (named devkit by default). Within this, create another directory called paddle_game – this is where we'll store our widget files.

Now create three blank files and then save them with the following file names:

You should have the following directory structure /devkit/widgets/paddle_game/(widget files)

Leave all three files open in a text editor. I personally use UltraEdit with a custom wordfile installed for development as this enables syntax highlighting and code folding. However, any plain text editor will work fine.

We'll now add content to our three widget files. The developer wiki contains a detailed explanation of the files needed to create a widget, so we won't go into too much detail in this tutorial. If you have any experience in Mobile Java (J2ME), you may also want to check out the developer wiki for a guide to Porting from MIDP to WSL.

Configuration file

In the widget configuration file widget.xml, type in the following (or download):

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <widget spec_version="2.1">
  3.    <info>
  4.       <name>Paddle Game</name>
  5.       <version>0.1</version>
  6.       <author>Your Name Here</author>
  7.       <clientversion>2.0</clientversion>
  8.       <shortdescription>Paddle Game</shortdescription>
  9.       <longdescription>Simple paddle game</longdescription>
  10.       <tags>fun retro action arcade pong paddle game</tags>
  11.    </info>
  12.    <parameters>
  13.       <parameter
  14.          type="string"
  15.          name="widgetname"
  16.          description="Name of widget"
  17.          editable="no">
  18.          Paddle Game
  19.       </parameter>
  20.    </parameters>
  21.    <resources>
  22.       <code src="paddle_game.he"/>
  23.       <stylesheet src="style.css"/>
  24.    </resources>
  25.    <layout minimizedheight="3em">
  26.       <view id="viewMini" class="mini">
  27.          <label class="label">${widgetname}</label>
  28.       </view>
  29.    </layout>
  30. </widget>

The widget.xml file lists information about the widget (author, description etc.), server side parameters, the services and filters used by the widget (HTTP, web services, XML filters etc.), links to resource files (images, sound, stylesheet and other binary files) and the layout of various user interface components (the widgets appearance on the dashboard, when maximised etc.). On line 22 and 23 of this file, we link to the WSL source code and our stylesheet (it is also possible to embed the stylesheet information directly into the widget.xml file). Some of the elements in the configuration file are mandatory, others are optional – visit the wiki for a detailed description of the various configuration properties. Change the author name on line 6 to a name of your choice. There is only one view defined in the layout section which defines the layout of the widget when shown on the dashboard (when minimised). We could also define the view for the maximised view of our widget (when opened) but instead we'll create this view dynamically in the WSL source code.

Stylesheet

Write the following to the style.css file (or download):

  1. mini
  2. {
  3.    background: solid white;
  4. }
  5.    
  6. label
  7. {
  8.    align: hcenter vcenter;
  9.    color: black;
  10. }
  11.    
  12. gameFlow
  13. {
  14.    align: hcenter vcenter;
  15.    background: solid green;
  16. }
  17.    
  18. gameCanvas
  19. {
  20.    background: solid black;
  21. }

Styles are used to decorate user interface components. If you've used CSS for web pages, you should be very familiar with the syntax and structure of WidSets stylesheet files. As you might expect, there is an article on the wiki that covers the WidSets stylesheet format.

The first stylesheet element (mini) defines the look of our widget on the dashboard. By using the same class name in both the stylesheet and the configuration file (widget.xml line 26), we can link this style element to the view component. The second stylesheet element (label) is applied to the widget label shown in the dashboard/minimised view. Again, this is linked to our label component (widget.xml line 27). The gameFlow and gameCanvas elements will be used to style our maximised view components defined in the WSL source code (Figure 6 centre).

WSL source code

Enter the following into the WSL source code file paddle_game.he (or download):

  1. class
  2. {
  3.    
  4.    /*------------
  5.     | Constants |
  6.     ------------*/
  7.    
  8.    // General commands
  9.    const int CMD_BACK = 1;
  10.    const int CMD_NEW_GAME = 10;
  11.    const int CMD_HELP = 11;
  12.    const int CMD_ABOUT = 12;
  13.    
  14.    // Fixed point constants
  15.    const int SHIFT = 16; // 16:16 FP
  16.    const int ONE_F = 1 << SHIFT;
  17.    
  18.    const int WORLD_SIZE_F = 32 << SHIFT; // 32x32 units
  19.    
  20.    // Menu items
  21.    final MenuItem MENU_BACK = new MenuItem(CMD_BACK, "Back");
  22.    final MenuItem MENU_OPTIONS = new MenuItem(OPEN_MENU, "Options");
  23.    
  24.    // Game options menu
  25.    Menu MENU = new Menu()
  26.       .add(CMD_NEW_GAME, "New game")
  27.       .add(CMD_HELP, "Help")
  28.       .add(CMD_ABOUT, "About");
  29.       
  30.    /*------------
  31.     | Variables |
  32.     ------------*/
  33.    
  34.    // UI
  35.    Shell gameShell;
  36.    Flow gameFlow;
  37.    Canvas gameCanvas;
  38.    
  39.    int screenWidth;
  40.    int screenHeight;
  41.    int viewportSize;
  42.    
  43.    int scale; // Pixels per world unit
  44.    
  45.    /*------------------------
  46.     | Fixed-point functions |
  47.     ------------------------*/
  48.    
  49.    // Multiply (input limit 6:16)
  50.    int ml(int x, int y)
  51.    {
  52.       return ((x >> 6) * (y >> 6)) >> 4;
  53.    }
  54.    
  55.    // Divide (input limit 6:16)
  56.    int dv(int x, int y)
  57.    {
  58.       return ((x << 6) / (y >> 6)) << 4;
  59.    }
  60.    
  61.    // Integer to FP
  62.    int i2f(int x)
  63.    {
  64.       return x << SHIFT;
  65.    }
  66.    
  67.    // Fixed-point to integer
  68.    int f2i(int x)
  69.    {
  70.       return x >> SHIFT;
  71.    }
  72.    
  73.    /*------------
  74.     | Functions |
  75.     ------------*/
  76.    
  77.    // Calculate viewport size (and determine scale factor)
  78.    void calculateViewport(int sw, int sh)
  79.    {
  80.       screenWidth = sw;
  81.       screenHeight = sh;
  82.       
  83.       // Viewport is square
  84.       viewportSize = min(sw, sh);
  85.       viewportSize -= viewportSize % f2i(WORLD_SIZE_F);
  86.       
  87.       // Recompute UI layout
  88.       gameFlow.setPreferredSize(sw, sh);
  89.       gameCanvas.setPreferredSize(viewportSize, viewportSize); // Square viewport
  90.       gameShell.computeLayout();
  91.       
  92.       // Pixels per world unit
  93.       scale = viewportSize / f2i(WORLD_SIZE_F);
  94.       
  95.       // How many pixels per world unit?
  96.       setBubble(null, "Pixels per world unit = " + scale);
  97.    }
  98.    
  99.    // Cleanup (stop timers, free resources etc.)
  100.    void cleanup()
  101.    {
  102.       gameCanvas = null;
  103.       gameFlow = null;
  104.       gameShell = null;
  105.    }
  106.    
  107.    /*-------------------
  108.     | System callbacks |
  109.     -------------------*/
  110.    
  111.    // Paint the game canvas
  112.    void paint(Component c, Graphics g, Style s, int width, int height)
  113.    {
  114.       
  115.       // Determine if orientation has changed?
  116.       // E.g. landscape <-> portrait
  117.       int sw, int sh = getScreenSize();
  118.       if(sw != screenWidth)
  119.          calculateViewport(sw, sh);
  120.       
  121.       // Display a simple message
  122.       g.setColor(0xFFFFFF);
  123.       g.drawString("Paddle Game", width / 2, height / 2, BOTTOM | HCENTER);
  124.       
  125.    }
  126.    
  127.    // Softkey mapping
  128.    MenuItem getSoftKey(Shell shell, Component focused, int key)
  129.    {
  130.       if(key == SOFTKEY_BACK)
  131.          return MENU_BACK;
  132.       else if(key == SOFTKEY_OK)
  133.          return MENU_OPTIONS;
  134.       
  135.       return null;
  136.    }
  137.    
  138.    // Get the current menu
  139.    Menu getMenu(Shell shell, Component source)
  140.    {
  141.       return MENU;
  142.    }
  143.    
  144.    // Key event handler
  145.    boolean keyAction(Component source, int op, int code)
  146.    {
  147.       return false;
  148.    }
  149.    
  150.    // Action event handler
  151.    void actionPerformed(Shell shell, Component source, int action)
  152.    {
  153.       switch(action)
  154.       {
  155.          case CMD_NEW_GAME:
  156.             setBubble(null, "New game...");
  157.             break;
  158.          case CMD_HELP:
  159.             setBubble(null, "Help...");
  160.             break;
  161.          case CMD_ABOUT:
  162.             setBubble(null, "Paddle Game");
  163.             break;
  164.          case CMD_BACK: // Exit
  165.             popShell(shell);
  166.             break;
  167.       }
  168.    }
  169.    
  170.    // Start widget (create minimised view)
  171.    void startWidget()
  172.    {
  173.       Flow flow = createView("viewMini", getStyle("default"));
  174.       setMinimizedView(flow);
  175.    }
  176.    
  177.    // Open widget (create maximised view)
  178.    Shell openWidget()
  179.    {
  180.       // Setup UI
  181.       gameCanvas = new Canvas(getStyle("gameCanvas"));
  182.       gameFlow = new Flow(getStyle("gameFlow"));
  183.       gameFlow.add(gameCanvas);
  184.       gameShell = new Shell(gameFlow);
  185.       
  186.       // Determine viewport size and game unit scaling
  187.       calculateViewport(getScreenSize());
  188.                
  189.       return gameShell;
  190.    }
  191.    
  192.    // Stop widget
  193.    void stopWidget()
  194.    {
  195.       cleanup();
  196.    }
  197.    
  198.    // Close widget
  199.    void closeWidget()
  200.    {
  201.       cleanup();
  202.    }
  203. }

Running the widget

Before we examine what each section of this code does, let's try running the widget. We'll first need to include three preview image files within the paddle_game directory. These are included with some of the examples in the SDK, although you can also download them here: web_icon.png, web_maximized.png, web_minimized.png.

The wiki contains a full list of the SDK commands we need to compile and test widgets. Figure 5 shows the commands used to login to WidSets, upload the widget (for testing on a real handset) and run the widget in the emulator.

command line window

Figure 5. Testing the widget.

Figure 6 shows the widget running in the emulator.

testing the widget in the emulator

Figure 6. Minimised view (left), maximised view (centre), options menu (right).

When you run the widget in the emulator, you should be presented with a plain, white rectangle with the label "Paddle Game" in black on the dashboard (Figure 6 left). Selecting the widget expands the view to fill the mobile screen (Figure 6 centre). Selecting Options from the soft key menu will bring up a list of options (Figure 6 right).

Code discussion

Interfaces

Developing any kind of WidSets widget requires a certain amount of foundation code (framework). Typically, a widget will implement a common set of interfaces (system callback functions) defined in the Script section of the Apidocs. The following is a list of all the interfaces defined in our widget:

Fixed-point numbers

The current release of WidSets (v2.0.0) is designed to work on handsets supporting MIDP 2.0 and CLDC 1.0 – this allows WidSets to run on a large number of devices. However, support for floating-point numbers (floats and doubles) was only introduced with CLDC 1.1. Here lies the problem – we need a way to represent real numbers in order to scale our game field to fit different screen resolutions and to allow our game objects to move smoothly around the game area. This is where fixed-point arithmetic comes to the rescue. Using fixed-point maths, not only can we represent floating-point numbers but we can also ensure our real-time widget runs as fast as possible (without hardware support, floating-point operations are a lot more computationally expensive than fixed-point operations).

In order to implement fixed-point (FP) numbers, we'll simply use integers to represent our pseudo-float values. We have used 16:16 FP format in the code, this means that the first 16 bits* (of the integer's 32 bits) will represent the integer part of our FP number and the last 16 bits will represent the fractional part (i.e. a value between 0 and 1). We can shift from integers to FP and vice-versa using the binary shift operators >> and <<. Calculating the result of operations between FP numbers is not as straightforward as between integers or floating-point numbers, so we've created a number of helper functions. The following code snippet demonstrates these functions and also shows how we should treat mixed FP and integer calculations:

// Fixed point constants
const int SHIFT = 16; // 16:16 FP

// Multiply (input limit 6:16)
int ml(int x, int y) { return ((x >> 6) * (y >> 6)) >> 4; }

// Divide (input limit 6:16)
int dv(int x, int y) { return ((x << 6) / (y >> 6)) << 4; }

// Integer to FP
int i2f(int x) { return x << SHIFT; }

// FP to integer
int f2i(int x) { return x >> SHIFT; }

void foo()
{
  // The integer 1
  int one = 1;
  
  // The integer 1 represented as FP
  int one_f = i2f(1); // 1 << SHIFT
  
  // We can use direct addition on FP
  int two_f = one_f + one_f;
  
  // ...and subtraction
  int neg_one_f = one_f - two_f;
  
  // We can also use normal multiplication
  // and division IFF one of the terms is a
  // normal integer.
  int ten_f = 5 * two_f;
  
  // However, if both terms are FP we need
  // to use FP helper functions.
  int twenty_f = ml(ten_f, two_f);
  // In this case this is the same as...
  twenty_f = 10 * two_f;
  
  // Same for division
  int half_f = dv(one_f, two_f);
  // Again, in this case this is same as...
  half_f = one_f / 2; // one_f >> 1
  
  // This is where FP becomes useful...
  
  int third_f = one_f / 3;
  
  // 6.66 rec.
  int result_f = ml(twenty_f, third_f);
  
  printf("20 * (1 / 3) = " + result_f + " (in FP)");
  printf("20 * (1 / 3) = " + f2i(result_f) + " (rounded to integer)");
}

You don't need to add this code to your widget, just examine the sequence of execution and get a feel for working with FP. In order to distinguish between fixed point and normal integers, we have added _f to FP variable names (_F for constants). Note that the SHIFT constant and FP helper functions are defined on lines lines 15 and 49 in our widget's WSL source code file. In the next step we'll start using FP numbers extensively. The printed output from the code given above is as follows:

20 * (1 / 3) = 436480 (in FP)
20 * (1 / 3) = 6 (rounded to integer)

The number 436480 might not seem like the value 6.66 (rec.) on first inspection, but if you divide this number by 65536 (1 << 16 or 216) on a calculator, you should get the output 6.66015625 which is accurate to 2 d.p. Some accuracy is lost in our calculation due to the nature of our fast fixed-point multiplication and division functions – we may make these more accurate later (perhaps at a small cost to speed of execution).

These FP helper functions represent an overhead in our code, so we may inline these function calls when the widget is functionally complete. We'll avoid low-level optimisation as long as possible as this can result in the the code becoming less readable and/or maintainable.

* The first bit will actually be the sign-bit.

World units

There's one last section of code that needs explaining – the calculateViewport() method (line 78):

// Calculate viewport size (and determine scale factor)
void calculateViewport(int sw, int sh)
{
  screenWidth = sw;
  screenHeight = sh;
  
  // Viewport is square
  viewportSize = min(sw, sh);
  viewportSize -= viewportSize % f2i(WORLD_SIZE_F); // i.e. % 32
  
  // Recompute UI layout
  gameFlow.setPreferredSize(sw, sh);
  gameCanvas.setPreferredSize(viewportSize, viewportSize); // Square viewport
  gameShell.computeLayout();
  
  // Pixels per world unit
  scale = viewportSize / f2i(WORLD_SIZE_F);
  
  // How many pixels per world unit?
  setBubble(null, "Pixels per world unit = " + scale);
}

This function is called when we first open the widget (lines 187) and when we detect a change in the phone's display orientation (lines 119).

In order to allow our game field to scale to the resolutions of different mobile phone screens (see Figure 7), we need to introduce a unit that's independent of pixels, we'll call these world units. We can then define our game area in terms of this new unit. Figure 6 shows game units marked onto the canvas.

canvas showing world units

Figure 6. World units marked on the canvas.

We have divided the canvas into a grid of 32 x 32 world units. If we know how big the viewport is in pixels, we can work out the scale factor from pixels to world units by dividing the pixel size by the number of world units (line 93).

Figure 7. Manually changing the scale value.

The calculateViewport(int sw, int sh) function accepts two arguments, representing the screen dimensions excluding menu the bar i.e. getScreenSize(). Our viewport is square, so we first need to determine what the shortest display dimension is and assign this to viewportSize. Then we reduce viewportSize to ensure that it is an exact multiple of the number of world units. This guarantees that the number of pixels per world unit (scale) will be an integer. There are two reasons for doing this:

  1. This will speed up our calculations later on when we perform world unit to pixel conversion.
  2. This will ensure that objects are represented consistently on the game field. Without ensuring our scale factor represents an exact pixel amount, objects of specific world unit sizes may appear "out by one pixel" in terms of size and positioning on the canvas and we don't have the option of using anti-aliasing for visually representing inter-pixel positioning.

In this tutorial step we've developed the basic UI structure of our game widget. The majority of the code presented is common to all widgets (interfaces, menus etc.) so we have presented it without in-depth explanation. For a more detailed discussion on how to develop WidSets widgets, please consult the Getting started article on the developer wiki. In the next tutorial, we'll begin to add functionality that's specific to widget game development, so we'll approach things a little more slowly, adding functionality piece-by-piece.

Earlier in the tutorial, the scale variable was introduced to enable us to convert world units to pixel units. The following video demonstrates what happens when we adjust the value of scale manually (using a more complete implementation of the game):

Resources

Download the widget files for step 1

Next step

Step 2. Developing the game framework

Tutorial links