Step 1. Creating a widget
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):
<?xml version="1.0" encoding="UTF-8"?>
<widget spec_version="2.1">
<info>
<name>Paddle Game</name>
<version>0.1</version>
<author>Your Name Here</author>
<clientversion>2.0</clientversion>
<shortdescription>Paddle Game</shortdescription>
<longdescription>Simple paddle game</longdescription>
<tags>fun retro action arcade pong paddle game</tags>
</info>
<parameters>
<parameter
type="string"
name="widgetname"
description="Name of widget"
editable="no">
Paddle Game
</parameter>
</parameters>
<resources>
<code src="paddle_game.he"/>
<stylesheet src="style.css"/>
</resources>
<layout minimizedheight="3em">
<view id="viewMini" class="mini">
<label class="label">${widgetname}</label>
</view>
</layout>
</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):
mini
{
background: solid white;
}
label
{
align: hcenter vcenter;
color: black;
}
gameFlow
{
align: hcenter vcenter;
background: solid green;
}
gameCanvas
{
background: solid black;
}
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):
class
{
/*------------
| Constants |
------------*/
// General commands
const int CMD_BACK = 1;
const int CMD_NEW_GAME = 10;
const int CMD_HELP = 11;
const int CMD_ABOUT = 12;
// Fixed point constants
const int SHIFT = 16; // 16:16 FP
const int ONE_F = 1 << SHIFT;
const int WORLD_SIZE_F = 32 << SHIFT; // 32x32 units
// Menu items
final MenuItem MENU_BACK = new MenuItem(CMD_BACK, "Back");
final MenuItem MENU_OPTIONS = new MenuItem(OPEN_MENU, "Options");
// Game options menu
Menu MENU = new Menu()
.add(CMD_NEW_GAME, "New game")
.add(CMD_HELP, "Help")
.add(CMD_ABOUT, "About");
/*------------
| Variables |
------------*/
// UI
Shell gameShell;
Flow gameFlow;
Canvas gameCanvas;
int screenWidth;
int screenHeight;
int viewportSize;
int scale; // Pixels per world unit
/*------------------------
| Fixed-point functions |
------------------------*/
// 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;
}
// Fixed-point to integer
int f2i(int x)
{
return x >> SHIFT;
}
/*------------
| Functions |
------------*/
// 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);
// 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);
}
// Cleanup (stop timers, free resources etc.)
void cleanup()
{
gameCanvas = null;
gameFlow = null;
gameShell = null;
}
/*-------------------
| System callbacks |
-------------------*/
// Paint the game canvas
void paint(Component c, Graphics g, Style s, int width, int height)
{
// Determine if orientation has changed?
// E.g. landscape <-> portrait
int sw, int sh = getScreenSize();
if(sw != screenWidth)
calculateViewport(sw, sh);
// Display a simple message
g.setColor(0xFFFFFF);
g.drawString("Paddle Game", width / 2, height / 2, BOTTOM | HCENTER);
}
// Softkey mapping
MenuItem getSoftKey(Shell shell, Component focused, int key)
{
if(key == SOFTKEY_BACK)
return MENU_BACK;
else if(key == SOFTKEY_OK)
return MENU_OPTIONS;
return null;
}
// Get the current menu
Menu getMenu(Shell shell, Component source)
{
return MENU;
}
// Key event handler
boolean keyAction(Component source, int op, int code)
{
return false;
}
// Action event handler
void actionPerformed(Shell shell, Component source, int action)
{
switch(action)
{
case CMD_NEW_GAME:
setBubble(null, "New game...");
break;
case CMD_HELP:
setBubble(null, "Help...");
break;
case CMD_ABOUT:
setBubble(null, "Paddle Game");
break;
case CMD_BACK: // Exit
popShell(shell);
break;
}
}
// Start widget (create minimised view)
void startWidget()
{
Flow flow = createView("viewMini", getStyle("default"));
setMinimizedView(flow);
}
// Open widget (create maximised view)
Shell openWidget()
{
// Setup UI
gameCanvas = new Canvas(getStyle("gameCanvas"));
gameFlow = new Flow(getStyle("gameFlow"));
gameFlow.add(gameCanvas);
gameShell = new Shell(gameFlow);
// Determine viewport size and game unit scaling
calculateViewport(getScreenSize());
return gameShell;
}
// Stop widget
void stopWidget()
{
cleanup();
}
// Close widget
void closeWidget()
{
cleanup();
}
}
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.
Figure 6 shows the widget running in the emulator.
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:
-
startWidget() and stopWidget() interfaces (lines 171 and 192).
These interfaces handle the widget's lifecycle. When the user starts WidSets, the startWidget() function is called for each widget on the dashboard. In our code, we create the dashboard (minimised) view (viewMini) in the startWidget() function. The stopWidget() function is called whenever the widget is removed, reloaded or when the WidSets client is closed. We call the user-defined function cleanup() here to free up widget resources.
-
openWidget() and closeWidget() interfaces (lines 178 and 199).
The openWidget() function is called when the widget is selected on the dashboard. Typically, this is where you create your maximised view of the widget. In our code we create a Canvas (gameCanvas), and then add this to a Flow (gameFlow). We then add the gameFlow to a Shell (gameShell) which represents a single user interface screen (see Figure 6 centre). Finally, we return the gameShell, which the WidSets client then pushes onto the shell stack. There is also a call to the user-defined function calculateViewport() – we'll discuss the purpose of this function later on in the tutorial. The closeWidget() function is called when we exit from the maximised view i.e. when the softkey labelled "Back" is selected in the maximised view (as shown in Figure 6 centre). As with the stopWidget() function, we call our cleanup() method to free resources (stop timers, null references etc.).
-
getSoftKey() and getMenu() interfaces (lines 128 and 139).
As the name suggests, the getSoftKey() function is called when a softkey has been pressed. Here, we map the menu item MENU_OPTIONS to the "OK" softkey and the menu item MENU_BACK to the "Back" softkey. When we declared and initialised our MENU_OPTIONS variable (line 22), we used the system defined constant OPEN_MENU as the action id. As a consequence, the getMenu() function is invoked when the user selects the "OK" softkey. The getMenu() function then returns our options menu (MENU) which results in the menu being shown to the user (Figure 6 right). To gain a greater understanding of this process, consult the Handling menu options section on the developer wiki.
-
actionPerformed() and keyAction() interfaces (lines 151 and 145).
The actionPerformed() function is called as a result of interaction with softkeys and menus. When the "Back" softkey is pressed, we pop the gameShell from the screen and return to the dashboard with a call to popShell(gameShell). The keyAction() function is called when the user clicks on an alpha-numeric key (including the navigation keys).
-
paint() interface (line 112).
In order to display our game objects, we need a user interface component that we can draw on dynamically. We achieve this by creating the Canvas instance gameCanvas (respresented by the large black centered area shown in Figure 6 centre). A WidSets Canvas is very similar to a Canvas in J2ME – we can draw text, images and primitives (triangles, rectangles, lines, arcs etc.) of various sizes to pixel coordinates on the surface. However, unlike J2ME, a WidSets Canvas does not necessarily need to fill the whole screen (we could therefore have multiple canvas objects shown on a single screen). The paint() function is called by the WidSets client when our canvas object is in need of a redraw. In our paint() function we draw the text "Paddle Game" to the centre of the canvas in white (line 122). We can force an update of the canvas by adding our gameCanvas to the repaint queue then flushing the screen (we'll use this method in the next tutorial when we setup the game loop).
On some handsets it is possible to switch the screen orientation from portrait to landscape (and vice-versa). Although WidSets internally handles this change by resizing UI components, there's no developer interface for receiving a notification of this event i.e. no equivelant of the sizeChanged(int newWidth, int newHeight) method defined in J2ME. To overcome this limitation we check each time the paint() function is called whether the canvas has changed size and act accordingly (line 117).
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.
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).
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:
-
This will speed up our calculations later on when we perform world unit to pixel conversion.
-
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
Next step
Tutorial links