Table of ContentsDevice-Specific LibrariesSiemens

Nokia

As you saw in Chapter 3, Nokia has an impressive range of J2ME-compatible MIDs on the market. In fact, they provide a significant portion of the installed user base you'll target with your games. Fortunately, Nokia has backed their J2ME hardware with solid support for Java developersespecially game developers.

Using the Nokia SDKs, you will be able to add the following features to your games:

  • Device control for flashing the lights or making the device vibrate

  • Expanded sound support

  • Full-screen drawing (beyond Canvas)

  • Extra graphics, including triangle and polygon drawing, transparent image blitting, rotation, alpha channels, and direct pixel access

Nokia provides two distinct tools to help you develop J2ME games for their range of MIDsthe NDS (Nokia Developer's Suite) for J2ME and Java SDKs for individual devices. You'll download and install both of these in the next section.

Installing the Tools

The NDS provides a basic Nokia phone emulator, audio tools, and a simple development environment somewhat similar to Sun's J2ME Wireless Toolkit. (Actually, the NDS is a little better in some ways.) I'm not going to go into detail about using the NDS as an IDE, but feel free to play around with it. Although it's pretty cute, it still doesn't serve as a fullscale IDE.

However, the NDS is a great way to manage and run the various Nokia emulators (in the form of phone SDKs). It also has support for JBuilder and Sun ONE Studio integration, so download it from the Nokia Web site and let's take a tour. To download the NDS, you need to visit Forum Nokia at http://www.forum.nokia.com.

Navigate to the Java section and then to the Tools and SDKs page. (There's a ton of cool stuff around there, so don't get distracted now!) What you're after is the Nokia Developer's Suite for J2ME. You need to become a member of the developer forum (free of charge) to download these tools. As you can see in Figure 6.1, after you download the NDS, install it as you normally would and select the IDE integration you prefer.

Figure 6.1. The Nokia Developers Suite 2.0 installation options for integration with an existing development environment.

graphic/06fig01.gif


Next, set the installation directory to be under your J2ME working directory like that in Figure 6.2 (feel free to use your own directory location). Nokia 175

Figure 6.2. Select the directory in which to install the NDS.

graphic/06fig02.gif


The Nokia UI API

Nokia makes additional MIDP features available through the Nokia UI API. You will find a version of the API, in the form of documentation and class files (classes.zip), packaged with most of the device SDKs (such as C:\J2ME\Nokia\Devices\Nokia_7210_MIDP_SDK_v1_0) Each device comes packaged with a complete copy of the Nokia UI, however they are all the same thing.

Life on planet Nokia consists of the classes and interfaces highlighted in Figure 6.3.

Figure 6.3. The Nokia UI classes

graphic/06fig03.gif


To see a Nokia emulator in action you can just run the executable (such as 7210.exe) found in the bin directory for each device (C:\J2ME\Nokia\Devices\Nokia_7210_MIDP_ SDK_v1_0\bin) and then use the file menu to open a JAR file containing your classes.

NOTE

Tip

I would stay away from using the NDS to build your project; use your IDE or the command line to compile the project and then use the NDS device emulators to view it.Also, you only need to include the Nokia UI specific classes (classes.zip) on your classpath if you're using Nokia UI features.

To get things working, you need to add lib/classes.zip to your build class path. Then you will be able to use the classes in the com.nokia.mid.* package. Take a look at the details of these features.

Device Control

You can control some of the extra physical features of Nokia devices using the com.nokia.mid.ui.DeviceControl class. (Table 6.1 shows the methods available.)

Table 6.1. com.nokia.mid.ui.DeviceControl

Method

Description

static void flashLights(long duration)

Flashes the lights.

static void setLights(int num, int level)

Turns the lights on and off.

static void startVibra(int freq, long duration)

Vibrates for a given frequency and time.

static void stopVibra()

Stops vibrating.


The two device elements you can control are the lights and vibration. To temporarily flash the lights on and off, use the flashLights method. For example:

import com.nokia.mid.ui.DeviceControl;
…
    DeviceControl.flashLights(5000);

This code will cause the lights (such as the LEDs) to flash. If there is no support for this feature, then nothing will happen (duh). The integer value specifies the length of time (in milliseconds) to keep the lights on, although the device might override this value if you specify a number that is too large.

The other method relating to lights, setLights, allows you to control any of the lights on the device individually, such as the backlight or the LEDs …in theory. In reality, Nokia only gives you the ability to control the device's backlight (if it has one). To do this, call the method with the light number (the first integer) set to 0 for the backlight and the level integer set to a number between 0 and 100 (where 0 is off and 100 is the brightest level). For devices that don't support graded backlighting, all values between 1 and 100 just translate to on. Here's an example of setLights in action:

DeviceControl.setLights(0, 100);

NOTE

Tip

A word of warning about playing with the backlight: There is no method to determine the current backlighting level; therefore, you have no way of restoring the lighting to the level the user previously had. Be careful using this function. A user won't be impressed if, while playing in a dark place, you turn the lights out to reward them for completing a level.

If you really want to add this function to your game, consider making it an option the player can enable or disable. This applies to the vibration function as well.

Controlling the vibration of the phone is one of those cool things you really want to do. Crashing into a side railing or taking shield damage feels much cooler if you give the phone a jiggle when it happens. To start the phone vibrating, use

DeviceControl.startVibra(10, 1000);

The first parameter, the frequency, is an integer in the range of 0 to 100. This number represents how violent the shaking should be. The second integer is the duration of the vibration in milliseconds. You can vary these numbers depending on what's happening in the game. For example, you might make the phone vibrate more violently for bigger shield hits or for a longer duration as the shield weakens.

A call to startVibra will return immediately, regardless of the duration of the call. If the device does not support vibration it will throw an IllegalStateException. This can happen even on a device that supports vibration if, for example, it's docked in a cradle.

To immediately stop any current vibration, use the stopVibra method.

Sound

The Nokia UI also includes the ability to play sounds on compatible MIDs (Table 6.2 lists the available methods). The most basic support you can rely on is playing simple tones as well as Nokia ring-tone format (RTPL) tunes. More advanced MIDs can also play digitized audio using the WAV format.

Table 6.2. com.nokia.mid.sound.Sound

Method

Description

Sound(byte[] data, int type)

Creates a sound object using the byte array data.

Sound(int freq, long duration)

Creates a sound object using a frequency and a duration.

void init(byte[] data, int type)

Initializes a sound using a byte array.

void init(int freq, long duration)

Initializes a sound using a frequency and a duration.

static int getConcurrentSoundCount(int type)

Gets the maximum number of simultaneous sounds the device supports.

static int[] getSupportedFormats()

Gets the supported formats.

void release()

Releases any audio resources owned by the sound.

int getGain()

Gets the volume of the sound.

int getState()

Gets the state of the sound.

void setGain(int gain)

Sets the volume.

void setSoundListener(SoundListener listener)

Registers a sound listener for state notifications.

void stop()

Stops playing the sound.

void resume()

Starts playing the sound again from where you last stopped.

void play(int loop)

Starts playing the sound.


Playing Tones

The first thing to playing sounds is determining the support for the device. The getSupportedFormats method will return an array of integers containing either FORMAT_WAV or FORMAT_TONE.

You can create a simple tone sound and play it using

Sound s = new Sound(440, 1000L);
s.play(1);

This code constructs a 440-Hz tone with a duration of 1000 milliseconds. The play method starts the sound playing and immediately returns; your MIDlet continues with the sounds playing in the background. The sound will stop either when the duration expires or when you ask it to stop by calling the stop method.

NOTE

Tip

All Nokia MIDs at least support frequencies of 400 Hz and 3951 Hz. You should consult the Nokia UI API javadoc for details on the other supported frequencies.

Some MIDs support multiple sounds played simultaneously. You can determine how many sounds are supported using the getConcurrentSoundCount method. If a call to play a sound exceeds the maximum number supported, the last sound playing will stop and the new sound will start.

You can also use the API to play simple tunes, such as the ring tones included with the phone. This functionality is available through Nokia's RTPL (Ring Tone Programming Language), which is part of the SMS (Smart Messaging Specification). The simplest way to generate RTPL music is to use the Nokia PC suite software.

NOTE

Tip

Support for more advanced multimedia (including video) on newer MIDs is available through Sun's MMAPI (Mobile Media API). For more information, visit http://java.sun.com/products/mmapi.

Listening In

When you're playing sounds, it isn't always easy to determine when a particular sound or RTPL tune has finished playing. For simple sounds (such as weapon fire) you probably don't care, but for background tunes or level-win rewards you need to know when the sound (or song) has finished, by either moving on in the game or starting another song. To determine when a sound or song has ended you use the SoundListener interface.

To set up a listener, create a class (or use your existing MIDlet) that implements the SoundListener interface, and then implement the soundStateChanged method. For example:

import com.nokia.mid.sound.Sound;
import com.nokia.mid.sound.SoundListener;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.lcdui.*;

/**
 * A demonstration of the Nokia UI Sound API.
 * @author Martin J. Wells
 */

public class NokiaSound extends MIDlet implements CommandListener, SoundListener
{
   private Sound sound;
   private Form form;
   private Command quit;
   private Command play;

   /**
    * MIDlet constructor creates a form and appends two commands. It then
    * instantiates a Nokia Sound class ready to play sounds for us in response
    * to the play command (see the commandAction method).
    */ 
public NokiaSound()
{
   form = new Form("Nokia Sound");

   form.setCommandListener(this);
   play = new Command("Play", Command.SCREEN, 1);
   form.addCommand(play);
   quit = new Command("Quit", Command.EXIT, 2);
   form.addCommand(quit);

   // Initialize a sound object we'll use later. We create the object here
   // with meaningless values becuase we'll init it later with proper
   // numbers. A sound listener is then set.
   sound = new Sound(0, 1L);
   sound.setSoundListener(this);
}

/**
 * The Nokia SoundListener interface method which is called when a sound
 * changes state.
 * @param sound The sound that changed state
 * @param state The state it changed to
 */
public void soundStateChanged(Sound sound, int state)
{
   if (state == Sound.SOUND_UNINITIALIZED)
      form.append("Sound uninitialized ");

   if (state == Sound.SOUND_PLAYING)
      form.append("Sound playing ");

   if (state == Sound.SOUND_STOPPED)
      form.append("Sound stopped ");
}


/**
 * Called by the Application Manager when the MIDlet is starting or resuming
 * after being paused. In this example it acquires the current Display object
 * and uses it to set the Form object created in the MIDlet constructor as
 * the active Screen to display.
 * @throws MIDletStateChangeException
 */ 
protected void startApp() throws MIDletStateChangeException
{
   Display.getDisplay(this).setCurrent(form);
}

/**
 * Called by the MID's Application Manager to pause the MIDlet. A good
 * example of this is when the user receives an incoming phone call whilst
 * playing your game. When they're done the Application Manager will call
 * startApp to resume. For this example we don't need to do anything.
 */
protected void pauseApp()
{
}

/**
 * Called by the MID's Application Manager when the MIDlet is about to
 * be destroyed (removed from memory). You should take this as an opportunity
 * to clear up any resources and save the game. For this example we don't
 * need to do anything.
 * @param unconditional if false you have the option of throwing a
 *  MIDletStateChangeException to abort the destruction process.
 * @throws javax.microedition.midlet.MIDletStateChangeException
 */
protected void destroyApp(boolean unconditional)
         throws MIDletStateChangeException
{
}

/**
 * The CommandListener interface method called when the user executes a
 *  Command, in this case it can only be the quit command we created in the
 *  constructor and added to the Form.
 *  @param command the command that was triggered
 * @param displayable the displayable on which the event occurred
 */
public void commandAction(Command command, Displayable displayable)
{
   // check for our quit command and act accordingly
   try
   {
      if (command == play) 
         {
            // initliaze the parameters of the sound (440Hz playing for 2
            // seconds - 2000 ms)
            sound.init(440, 2000L);
            sound.play(1);
         }

         if (command == quit)
         {
            destroyApp(true);

            // tell the Application Manager we're exiting
            notifyDestroyed();
         }
      }

      // we catch this even though there's no chance it will be thrown
      // since we called destroyApp with unconditional set to true.
      catch (MIDletStateChangeException me)
      { }
   }

}

NOTE

Tip

The Nokia NDS contains a great sound playing example in the C:\j2me\Nokia\Tools\Nokia_ Developers_Suite_for_J2ME\midp_examples\Tones directory.

NOTE

Tip

You can control the playback volume using the Sound.setGain method with a number ranging from 0 to 255.

Full-Screen Drawing

Even when you are using the MIDP low-level UI's Canvas class, you still don't get access to the entire screen. This is inconvenient because of the relatively significant space you lose at the top and bottom of the screen that could be put to good use. (For example, you could use the space to display the number of lives the player has left.) As you can see in Figure 6.4, Nokia's FullCanvas is an extension of the MIDP Canvas class that provides full-screen access. You can see the lone method for the class in Table 6.3.

Figure 6.4. Nokia's FullCanvas class gives you access to the entire display.

graphic/06fig04.gif


Table 6.3. com.nokia.mid.ui.FullCanvas

Method

Description

FullCanvas()

Constructs a new FullCanvas.


The price you pay is the loss of the command system. You need to implement this yourself using keystroke responses. (The addCommand and setCommandListener methods will throw an IllegalStateException.) In exchange, you will gain access to the keys previously reserved for commands, as follows:

  • KEY_UP_ARROW

  • KEY_DOWN_ARROW

  • KEY_LEFT_ARROW

  • KEY_RIGHT_ARROW

  • KEY_SEND

  • KEY_END

  • KEY_SOFTKEY1

  • KEY_SOFTKEY2

  • KEY_SOFTKEY3

In the following example, you will draw some random-colored rectangles extending out as far as the canvas will allow. As you can see from the results in Figure 6.1, you can gain a sizeable amount of rendering space when you use FullCanvas.

import com.nokia.mid.ui.FullCanvas;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*; 
import java.util.Random;

/**
 * An example that demonstrates the use of the Nokia UI FullCanvas class.
 * You must have the Nokia UI class files on your classpath and run this example
 * on a Nokia emulator (such as the 7210).
 * @author Martin J. Wells
 */
public class FullCanvasTest extends MIDlet
{
   private MyCanvas myCanvas;

   /**
    * An inner class which extends from the com.nokia.mid.ui.FullCanvas and
    * implements a random rectangle drawer.
    */
   class MyCanvas extends FullCanvas
   {
      private Random randomizer = new Random();

      /**
       * A method to obtain a random number between a miniumum and maximum
       * range.
       * @param min The minimum number of the range
       * @param max The maximum number of the range
       * @return
       */
      private int getRand(int min, int max)
      {
         int r = Math.abs(randomizer.nextInt());
         return (r % (max - min)) + min;
      }

      /**
       * Canvas class paint implementation where we draw the some random
       * rectangles.
       * @param graphics The graphics context to draw to
       */
      protected void paint(Graphics graphics)
      {
         for (int i = 10; i > 0; i--)
         {
            graphics.setColor(getRand(1, 254), getRand(1, 254), getRand(1, 254)); 
            graphics.fillRect(0, 0, i * (getWidth() / 10), i * (getHeight() / 10));
         }
      }
   }

   /**
    * MIDlet class which constructs an instance of our custom FullCanvas.
    */
   public FullCanvasTest()
   {
      myCanvas = new MyCanvas();
   }

   /**
    * Called by the Application Manager when the MIDlet is starting or resuming
    * after being paused. In this example it acquires the current Display object
    * and uses it to set the Canvas object created in the MIDlet constructor as
    * the active Screen to display.
    * @throws MIDletStateChangeException
    */
   protected void startApp() throws MIDletStateChangeException
   {
      Display.getDisplay(this).setCurrent(myCanvas);
   }

   /**
    * Called by the MID's Application Manager to pause the MIDlet. A good
    * example of this is when the user receives an incoming phone call whilst
    * playing your game. When they're done the Application Manager will call
    * startApp to resume. For this example we don't need to do anything.
    */
   protected void pauseApp()
   {
   }

   /**
    * Called by the MID's Application Manager when the MIDlet is about to
    * be destroyed (removed from memory). You should take this as an opportunity
    * to clear up any resources and save the game. For this example we don't
    * need to do anything.
    * @param unconditional if false you have the option of throwing a
    * MIDletStateChangeException to abort the destruction process.
    * @throws MIDletStateChangeException 
    */
   protected void destroyApp(boolean unconditional) throws MIDletStateChangeException
   {
   }
}

Direct Graphics

Now you get to the real fun of the Nokia UIgraphics. And let me tell you, what you get is downright cooltransparency, image rotation, triangle and polygon drawing, and monkeys …yeah, monkeys …little pink, fluffy monkeys will jump out of the device and dance naked for you! Um, all right. There are no monkeys, but seriously, this is where the Nokia UI really comes into its own. Features such as image rotation and transparency are more than just a little bit important for making great games.

These graphics functions are available through the com.nokia.mid.ui.DirectGraphics class (the full API is listed in Table 6.5), which you acquire using the com.nokia.mid.ui. DirectUtils.getDirectGraphics static method (the DirectUtils API is shown in Table 6.6). For example:

Table 6.5. com.nokia.mid.ui.DirectGraphics

Method

Description

void drawImage(Image img, int x, int y, int anchor, int manipulation)

Draws an image.

void drawPixels(byte[] pixels, byte[] transparencyMask, int offset, int scanlength, int x, int y, int width, int height, int manipulation, int format)

Draws pixel data directly with a transparency mask.

void drawPixels(int[] pixels, boolean transparency, int offset, int scanlength, int x, int y, int width, int height, int manipulation, int format)

Draws pixel data directly with optional transparency encoding in the pixel data format.

void drawPixels(short[] pixels, boolean transparency, int offset, int scanlength, int x, int y, int width, int height, int manipulation, int format)

Draws pixel data (short data type version).

void drawPolygon(int[] xPoints, int xOffset, int[] yPoints, int yOffset, int nPoints, int argbColor)

Draws a polygon (a closed, many-sided shape) based on the x and y arrays of points.

void drawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, int argbColor)

Draws a closed triangle.

void fillPolygon(int[] xPoints, int xOffset, int[] yPoints, int yOffset, int nPoints, int argbColor)

Draws a filled polygon based on the x and y arrays of points.

void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3, int argbColor)

Draws a filled, closed triangle.

int getAlphaComponent()

Gets the alpha component of the current color.

int getNativePixelFormat()

Returns the native pixel format of an implementation.

void getPixels(byte[] pixels, byte[] transparencyMask, int offset, int scanlength, int x, int y, int width, int height, int format)

Copies the pixel values (including any transparency mask) of the graphics context from a specific location to an array of byte values.

void getPixels(int[] pixels, int offset, int scanlength, int x, int y, int width, int height, int format)

Copies the pixel values of the graphics context from a specific location to an array of int values.

void getPixels(short[] pixels, int offset, int scanlength, int x, int y, int width, int height, int format)

Copies the pixel values of the graphics context from a specific location to an array of short values.

void setARGBColor(int argbColor)

Sets the current color (and alpha) to the specified ARGB value (0xAARRGGBB).


Table 6.6. com.nokia.mid.ui.DirectUtils

Method

Description

static Image createImage(byte[] imageData, int imageOffset, int imageLength)

Constructs a mutable image from the given byte array.

static Image createImage(int width, int height, int ARGBcolor)

Constructs a mutable image with a specified width, height, and ARGB color.

static DirectGraphics getDirectGraphics (javax.microedition.lcdui.Graphics g)

Gets a DirectGraphics object using a javax.microedition.lcdui.Graphics instance.



protected void paint(Graphics graphics)

{

   // Get the Nokia DirectGraphics object

   DirectGraphics dg = DirectUtils.getDirectGraphics(graphics);


   // Call DirectGraphics methods
   …
}

The DirectGraphics object returned in the method call in this example is not the same object as the original Graphics context, nor is DirectGraphics a super of Graphics.However, the two contexts are inherently connected; changes to one will affect the other. This is not only important, it also makes for some great bonus features when you combine the two.

Once you have a DirectGraphics object, any call on the original Graphics object's method to change color (setColor or setGrayScale), clipping (setClip or clipRect), stroke (setStrokeStyle), or translation (translate) will affect the output of any future drawing on the linked DirectGraphics object. Keep in mind that at this point I'm not saying you call these methods on the DirectGraphics object itself. (They aren't there anyway.) You call them on the original Graphics object. The link between the two will result in the change affecting the DirectGraphics object as well. Keep this in mind because I'll get to an example soon.

On the flip side, the DirectGraphics object's setARGBColor method will change the current rendering color on the original Graphics context. What's cool about that? Notice the "A" in the color method call? Yep, that's an alpha channel! You can set a variable level of trans-parency when drawing graphics. Not only can you do it using the DirectGraphics calls, but you can also do it using the Graphics call. For example, in the following code you'll draw two rectangles using Graphics and DirectGraphics to set an alpha channel color. Both rectangles will have a high level of transparency.

protected void paint(Graphics graphics)
{
   // Get the Nokia DirectGraphics object
   DirectGraphics dg = DirectUtils.getDirectGraphics(graphics);

   // Draw a transparent rectangle in the center of the screen
   dg.setARGBColor(0xFFFF0000);
   graphics.fillRect(50, 50, getWidth()-100, getHeight()-100);

   // Draw a rectangle that fills the screen (no transparency)
   dg.setARGBColor(0xFF0000FF);
   graphics.fillRect(0, 0, getWidth(), getHeight());
}

The call to setARGBColor uses a four-byte integer value (Quad 8) to represent the alpha-, red-, green-, and blue-channel intensity. For the alpha channel, 00 represents completely transparent and FF (or 255) is fully opaque. Table 6.4 lists a few examples.

Table 6.4. ARGB Examples

Value

Description

0xFF000000

Black (No transparency)

0xFFFF0000

Red (No transparency)

0x000000FF

Blue (Fully transparentresults in nothing being rendered)

0x80FFFF00

Yellow (Half transparent)

0x2000FF00

Green (Slightly transparent)


The code example you just saw will draw a big blue rectangle. Because the last setARGBColor is fully opaque, none of the underlying smaller red rectangle will show through. If you adjusted the color of the rectangle to have an alpha level of 55 (0x550000FF), you would see a mixture of both colors displayed. If the alpha level were further dampened down to 00 (0x000000FF), only the underlying red rectangle would show. Figure 6.5 shows an example of all three results.

Figure 6.5. Three examples of drawing using Nokia alpha channels.

graphic/06fig05.gif


Notice the transitional color of the red rectangle. In the second image, you see a blending of both rectangles on the display at the same time. Remember this trick; it's one of the coolest you've got.

Triangles and Polygons

MIDP limits geometric drawing to just lines and rectangles. Although you can use lines to draw almost any shape, there's no way to fill the compound shapeand it's about as much fun as watching paint dry. The Nokia UI adds the capability to draw outlined or filled shapes with three or more sides (triangles and polygons).

The method calls to draw geometry are relatively simple so an example should give you the general idea. The following code draws a triangle and a rectangle.

protected void paint(Graphics graphics)
{
   // Get the Nokia DirectGraphics object
   DirectGraphics dg = DirectUtils.getDirectGraphics(graphics);

   // Draw a colored triangle
   dg.fillTriangle(getWidth() / 2, 10, getWidth(), getHeight(), 0, getHeight(),
                    0xFF00FF00); 
   // Poly wants a display
   int[] xPoints = {25, 50, 25, 10};
   int[] yPoints = {10, 25, 50, 25};
   dg.drawPolygon(xPoints, 0, yPoints, 0, xPoints.length, 0x55FF0000);
}

Reflecting and Rotating

DirectGraphics provides an enhanced drawImage method capable of rendering an image using rotation and reflection. Given that graphics are commonly drawn in multiple directions in a game (such as to represent a sprite moving from left and right), you'll find that without reflection or rotation multiple versions (sometimes quite a few) of the same images are required to represent these different directions. A combination of reflection and rotation lets you reuse the same graphics, thus freeing up space in your JAR for yet more graphics.

To render an image using reflection or rotation, use the DirectGraphics.drawImage method. Table 6.7 lists the available options. You can combine flipping and rotating.

Table 6.7. Reflection and Rotation Types

Type

Description

FLIP_VERTICAL

Flips the image vertically.

FLIP_HORIZONTAL

Flips the image horizontally.

ROTATE_90

Turns the image 90 degrees counterclockwise.

ROTATE_180

Turns the image 180 degrees counterclockwise.

ROTATE_270

Turns the image 270 degrees counterclockwise.


In the following code, you will use drawImage to render an image both rotated and reflected.

class MyCanvas extends FullCanvas
{
   private Image alienHeadImage = null;

   /**
    * A custom FullCanvas that loads up a sample image (make sure the
    * image file is in the JAR file!)
    */ 
   public MyCanvas()
   {
      try
      {
         alienHeadImage = Image.createImage("/alienhead.png");
      }
      catch (IOException ioe)
      {
         System.out.println("unable to load image");
      }
   }

   /**
    * The overriden Canvas paint method draws the previously loaded image
    * onto the canvas using the passed in graphics context.
    */
   protected void paint(Graphics graphics)
   {
      // Get the Nokia DirectGraphics object
      DirectGraphics dg = DirectUtils.getDirectGraphics(graphics);

      // Draw the alien head rotated by 270 degrees
      dg.drawImage(alienHeadImage, getWidth()/2, getHeight()/2,
                    Graphics.HCENTER | Graphics.VCENTER,
                    DirectGraphics.ROTATE_270);

      // Draw the alien head upside down
      dg.drawImage(alienHeadImage, getWidth() / 2, (getHeight() / 2) + 20,
                    Graphics.HCENTER | Graphics.VCENTER,
                    DirectGraphics.FLIP_VERTICAL);
   }
}

Image Transparency

At the start of this section on DirectGraphics, you saw how you could set transparency, or alpha channels, for graphics rendering. Well here's a news flash: This also extends to the PNG image format! Yep, that's right; you can render images using the full alpha channel in PNG files exported directly from your favorite graphics application.

The most significant use of this feature in a game is when you render a non-rectangular image onto a background. Without transparency support, you cannot draw the image seamlessly over a background imageyou'll have ugly bordering. In Figure 6.6, you can see a good example of an image without transparency (top) and with transparency (bottom).

Figure 6.6. An example drawing a transparent and non-transparent PNG image.

graphic/06fig06.gif


The following code renders these images. As you can see, there is basically no difference when you load or display a transparent image.

protected void paint(Graphics graphics)
{
   // Get the Nokia DirectGraphics object
   DirectGraphics dg =
DirectUtils.getDirectGraphics(graphics);

   // Fill the screen with pastel green
   graphics.setColor(50, 200, 50);
   graphics.fillRect(0, 0, getWidth(), getHeight());

   // Draw the alien head
   dg.drawImage(alienHeadImage, getWidth()/2, getHeight()/2,
                 Graphics.HCENTER | Graphics.VCENTER, 0);

   // Draw the transparent alien head.
   // Note that I'm assuming you've previously loaded a transparent
   // image file into an Image object named transAlienHeadImage.
   dg.drawImage(transAlienHeadImage, getWidth() / 2, (getHeight() / 2) + 20,
                 Graphics.HCENTER | Graphics.VCENTER, 0);
}

Pixel Data Access

Last but not least, DirectGraphics gives you access to the nuts and bolts of the native pixel data with which a Nokia MID stores images. You can extract pixel data, modify it, and then render your modified version. Uses of direct pixel manipulation are pretty much endless because this function allows your program to modify the image. For example, you can turn one range of colored pixels into another color (or level of transparency). What's good about that? Well imagine the case of a space shooter. At the start of the game you could let the player choose his ship color, and then use pixel manipulation to change one color (such as the go-fast stripes along the wings) into another color of his choice. This could even include different levels of depth that match the original paint job. This wouldn't normally be practical because of the extra space different-colored ships would take up in the JAR file.

This all sounds great, but unfortunately it's not that easy to use. First, different Nokia models store image data internally in different formats. (Table 6.8 lists the various formats.) You need to determine the native storage format using DirectGraphics.getNativePixelFormat. Once you have the format, you can get the image data in the form of an array of integers, shorts, or bytes, depending on the native image format you're requesting. For example, the Nokia 7210 (which you'll use for all the following examples) has a native pixel format of TYPE_USHORT_444_RGB. Quite a mouthful, huh? This format uses the Java short type to store pixel data. With all this in mind, take a look at the code to grab the pixel data.

Table 6.8. Pixel Data Types

Type

Description

TYPE_BYTE_1_GRAY

One-bit format, two distinct color values (on/off), stored as a byte. Eight pixel values in a single byte, packed as closely as possible.

TYPE_BYTE_1_GRAY_VERTICAL

One-bit format, two distinct color values (on/off), stored as a byte. Eight pixel values are stored in a single byte.

TYPE_BYTE_2_GRAY

Two-bit format, four grayscale colors.

TYPE_BYTE_332_RGB

Three bits for red, three bits for green, and two bits for blue component in a pixel, stored as a byte.

TYPE_BYTE_4_GRAY

Four-bit format, 16 grayscale colors.

TYPE_BYTE_8_GRAY

Eight-bit format, 256 grayscale colors.

TYPE_INT_888_RGB

Eight bits for red, green, and blue component in a pixel (0x00RRGGBB).

TYPE_INT_8888_ARGB

Eight bits for alpha, red, green, and blue component in a pixel (0xAARRGGBB).

TYPE_USHORT_1555_ARGB

One bit for alpha, five bits for red, green, and blue component in a pixel.

TYPE_USHORT_444_RGB

Four bits for red, green, and blue component in a pixel, stored as a short (0x0RGB).

TYPE_USHORT_4444_ARGB

Four bits for alpha, red, green, and blue component in a pixel, stored as a short (0xARGB).

TYPE_USHORT_555_RGB

Five bits for red, green, and blue component in a pixel.

TYPE_USHORT_565_RGB

Five bits for red, six bits for green, and five bits for blue component in a pixel.


int pixFormat = d g.getNativePixelFormat();

// make sure we have a pixel format we know how to handle properly
if (pixFormat == DirectGraphics.TYPE_USHORT_444_RGB)
{
   // Create an array large enough to hold the pixels we grab (20 x 50)
   short pixels[] = new short[20*50]; 
   dg.getPixels(pixels, 0, 20, 0, 0, 20, 50, DirectGraphics.TYPE_USHORT_444_RGB);
   ...

The first thing you do in this code is to determine the pixel format of the data you're going to grab. To keep things simple, I've just hard-coded a test to make sure the format is what you expect (TYPE_USHORT_444_RGB). Adding support for other formats is a relatively painless process, but it doesn't make for concise examples at this stage.

Before I move on, I want to talk about exactly what the TYPE_USHORT_444_RGB format means. First, the USHORT indicates that a Java short type (2 bytes, or 16 bits) represents each pixel in the image. To understand all this, you can think of a short as being four sets of 4 bits, commonly written as 4444.

As you can see in Figure 6.7, the 444_RGBaka 12-bit colorindicates 4 bits for each of the red, green, and blue components of the color. Now because 4 bits can only represent 16 possible combinations, this means there is a maximum of 16 reds, 16 greens, and 16 blues. If you combine all these, you have a maximum of 4096 (16 * 16 * 16) colors on the device. This is the capability of the 7210 display; other MIDs have different capabilities.

Figure 6.7. The byte layout for a 444_RGB color pixel.

graphic/06fig07.gif


One thing you might notice here: Since the Java short type has 2 bytes (16 bits) and the combination of the RGB components is only 12 bits (4 + 4 + 4), what is the purpose of the other 4 bits? The answer is nothingthis format doesn't require them.

All right, now that you have a little background on the 444_RGB format, take a look at things in action. The following code draws an image with a go-fast red stripe down the center. Using the Nokia UI direct pixel access, you're going to change just the red components of the image to green. A free paint job that doesn't waste any precious JAR space!

protected void paint(Graphics graphics)
{
   // Get the Nokia DirectGraphics object
   DirectGraphics dg = DirectUtils.getDirectGraphics(graphics);

   // Paint the entire canvas black
   graphics.setColor(0, 0, 0);
   graphics.fillRect(0, 0, getWidth(), getHeight());

   // Draw the original red-striped image
   dg.drawImage(redStripeImage, 0, 0, Graphics.TOP|Graphics.LEFT, 0); 
   // Get the native pixel format
   int pixFormat = dg.getNativePixelFormat();

   // Make sure we have a pixel format we know how to handle properly
   if (pixFormat == DirectGraphics.TYPE_USHORT_444_RGB)
   {
      int imgWidth = redStripeImage.getWidth();
      int imgHeight = redStripeImage.getHeight();

      // Create an array big enough to hold the pixels
      short pixels[] = new short[imgWidth*imgHeight];

      // Grab them from the graphics context in the array
      dg.getPixels(pixels, 0, imgWidth, 0, 0, imgWidth, imgHeight,
                    DirectGraphics.TYPE_USHORT_444_RGB);

      // Loop through the contents of the array looking for the right color
      for (int y=0; y < imgHeight; y++)
      {
         for (int x = 0; x < imgWidth; x++)
         {
            // Pixels are stored in one long array so we need to get an
            // index relative to our x and y
            int a = (y * imgWidth) + x;
            short pixel = pixels[a];

            // Check to see if the pixel has all the RED bits on
            if (pixel == 0x0F00)
               pixels[a] = (short)0x00F0; //
         }
      }
      dg.drawPixels(pixels, false, 0, imgWidth, 60, 0, imgWidth, imgHeight,
                     0, DirectGraphics.TYPE_USHORT_444_RGB);
   }
}

NOTE

Tip

You can see a complete working example of this on the CD in the Chapter 6 source code directory under the name NokiaGraphicsTest.

You can recreate this example yourself by creating a PNG image file with some of the pixels set to maximum solid red. The preceding code will loop through the image and replace any pixels matching this color with solid green pixels. You can see the results in Figure 6.8.

Figure 6.8. The results of turning all the red pixels to green using RGB pixel manipulation.

graphic/06fig08.gif


This is a simple example of pixel manipulation directly replacing one pixel with another. Using other techniques, the sky really is the limit to what you can do. For now, here are some ideas of what's possible:

  • Color shifting. As you just saw in the example, you can change any set of colors into another. Using a more advanced version of this function, you can also reflect the shade of the original color. This results in much sexier go-fast stripes.

  • Translucent shadows. You can create a darkened version of a sprite by lowering the pixel intensity. You can then draw this version underneath the sprite to create a translucent shadow effect that saps the light as it passes over the background.

  • Light blasts. You can create some excellent explosive effects by dramatically increasing the brightness (the opposite of the shadow effect just mentioned) of an expanding area of pixels. This can give the illusion of an impact or an explosion with no images required. (You can use images as maps to create outlines for these effects if you want.) The end results can be spectacular.

  • Pulsing. You can range through pixels' colors to create blinking and other effects.

  • Weather effects. You can create daylight, nighttime, and fog effects.

  • Image scaling. You can change the size of images, which is useful for height or size illusions.

NOTE

Tip

One thing to keep in mind: Modifying pixels is a relatively slow process, so avoid (as much as possible) doing it on every call to paint. If the effect you're using still works, you can just pre-render modified versions (such as the alternative player-colored ships) as new images and then display those during the paint method. This has very little cost in terms of rendering speed.

Some effects require that you check pixels on every paint pass (such as the dynamic lighting explosions), but doing too much of this will kill the MID's CPU in no time. Give the effect a try (on the real phone) to see how it works. And above all, make these effects optional extras that the player can disable.

    Table of ContentsDevice-Specific LibrariesSiemens