The point to be demonstrated here is that
the MIDP Game API was developed for the feature phone market. In this
market, there are limits on heap size, thread pools, JAR size, few or no
optional JSRs and minimal multimedia capabilities. Given all of that it
is amazing what can be done in a very short space of time. So let's
hook straight in with a simple game that we can throw away just as a
quick demonstration of how much the MIDP 2.0 Game API does for you.
Figure 1 shows a screen shot for a demo game, called GhostPong in honor of the original arcade game Pong.It's less than 150 lines in length and took just over an hour to write
(it should have taken less but I kept making stupid typing mistakes...).
The point of the game is that
basically the little ghost sprites fall from the top of the screen and
you have to smash them out of existence with the brick paddle at the
bottom which moves left and right. You get 10 points for each one you
get and you lose 10 points for each ghost you miss. And as for the name –
well there's no excuse for that.
What this shows is
that you don't need complex architectures and reusable libraries in
order to build effective games. With MIDP 2.0 and the Game API you can
jump straight in and just get on with it. The game is only 6 KB in size
and has no external requirements other than a dependency on MIDP 2.0.
This means that it would quite possibly run today on almost a billion
mobile phones.
The code below shows the game canvas – for the full application please refer to the code on the book's website.
1 public class GhostPongCanvas extends GameCanvas implements Runnable{
2
3 GhostPongMIDlet midlet;
4 boolean running;
5 final Graphics graphics;
6 int pongX, pongY, pongWidth, pongHeight;
7 Image ghostImage;
8 Sprite pong;
9 LayerManager layerManager;
10 Random random = new Random();
11 long startTime, lastGenTime;
12 int points = 0;
13 Font font;
14 Player tonePlayer;
15 ToneControl toneControl;
16 byte G = (byte)(ToneControl.C4 + 7);
17 byte[] soundSeq = {
ToneControl.VERSION,1,ToneControl.TEMPO,127,G,8,G,8};
18
19 public GhostPongCanvas(GhostPongMIDlet midlet) throws Exception{
20 super(true);
21 setFullScreenMode(true);
22 this.midlet = midlet;
23 graphics = getGraphics();
24 font = Font.getFont(Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD,...SIZE_LARGE);
25 ghostImage = Image.createImage("/ghost.png");
26 pong = new Sprite(Image.createImage("/pong.png"));
27 pongWidth = pong.getWidth();
28 pongHeight = pong.getHeight();
29 pongX = (getWidth() - pongWidth) >> 1;
30 pongY = getHeight() - 20 - pongHeight;
31 pong.setPosition(pongX, pongY);
32 tonePlayer = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
33 tonePlayer.realize();
34 toneControl = (ToneControl) tonePlayer.getControl("ToneControl");
35 toneControl.setSequence(soundSeq);
36 layerManager = new LayerManager();
37 layerManager.append(pong);
38 }
39
40 public void start(){
41 Thread thread = new Thread(this);
42 thread.start();
43 running = true;
44 startTime = System.currentTimeMillis();
45 lastGenTime = startTime;
46 }
47
48 public void run(){ // the game loop
49 while(running){
50 try{
51 generateGhosts();
52 movePong();
53 moveGhosts();
54 draw();
55 flushGraphics();
56 Thread.sleep(50);
57 }
58 catch(Exception e){}
59 }
60 }
61
62 private void draw(){
63 graphics.setColor(0x000000);
64 graphics.fillRect(0, 0, getWidth(), getHeight());
65 graphics.setColor(0xff0000);
66 graphics.setFont(font);
67 graphics.drawString("" + points, getWidth()>>1, 10,
Graphics.HCENTER | Graphics.TOP);
68 layerManager.paint(graphics, 0, 0);
69 }
70
71 // every second there's an 80% chance of generating new ghosts
72 private void generateGhosts(){
73 long elapsed = System.currentTimeMillis() - lastGenTime;
74 if(elapsed > 1000L){
75 int i = genRandom(1, 10);
76 if(i <= 8){
77 int numGhosts = genRandom(1, 4);
78 for(int g = 0; g < numGhosts; g++){
79 Sprite ghost = new Sprite(ghostImage);
80 int ghostX = genRandom(0, getWidth() - ghost.getWidth());
81 ghost.setPosition(ghostX, 0);
82 if(i < 4) // occasionally flip the sprite for variety
83 ghost.setTransform(Sprite.TRANS_MIRROR);
84 layerManager.append(ghost);
85 }
86 }
87 lastGenTime = System.currentTimeMillis();
88 }
89 }
90
91 private void moveGhosts(){
92 int fallSpeed = 10;
93 int numGhosts = layerManager.getSize() − 1; // exclude the pong
94 for(int i = numGhosts; i >= 1; i--){
95 Sprite ghost = (Sprite) layerManager.getLayerAt(i);
96 ghost.move(0, fallSpeed);
97 if(ghost.collidesWith(pong, true)){
98 points += 10;
99 layerManager.remove(ghost);
100 try {
101 tonePlayer.start();
102 }
103 catch(Exception e){
104 e.printStackTrace();
105 }
106 }
107 else if(ghost.getY() > this.getHeight()){
108 points -= 10;
109 layerManager.remove(ghost);
110 }
111 }
112 }
113
114 private void movePong(){
115 int keyState = getKeyStates();
116 int dx = 15;
117 if( (keyState & GameCanvas.LEFT_PRESSED) != 0){
118 pongX -= dx;
119 if(pongX < 0) pongX = 0;
120 }
121 else if( (keyState & GameCanvas.RIGHT_PRESSED) != 0){
122 pongX += dx;
123 if(pongX > (getWidth() - pongWidth))
124 pongX = getWidth() - pongWidth;
125 }
126 pong.setPosition(pongX, pongY);
127 }
128
129 // shut down on right softkey (S60 & UIQ)
130 public void keyPressed(int keyCode){
131 if(keyCode == −7 || keyCode == −20){
132 midlet.die();
133 }
134 }
135
136 public int genRandom(int min, int max){
137 return (Math.abs(random.nextInt()) % max) + min;
138 }
139 }
Now we are going to have a quick look at what we have managed to cover in under 150 lines of Java ME code.
1. What Has It Got?
No argument – this game won't be on any best-seller list but let's see some of the things this simple game demonstrates:
Loading and displaying an image from a JAR file (lines 25 and 26)
Creating a Sprite instance from an image file (line 26)
A basic game loop using a thread (lines 48–60)
Using a full screen GameCanvas (lines 20 and 21)
Examining key states to respond to user input (lines 115, 117 and 121)
Moving and rotating Sprites (lines 79–83)
A simple way to generate random numbers over a fixed interval (lines 136–138)
Basic layer management using the LayerManager class (lines 95, 99–109)
Basic MIDP 2.0 Media API tone sequences (lines 14–17, 32–35 and 100–105)
Responding to softkey events (lines 130–134)
Collision detection between Sprites (line 97).
It has to be said that, once
you get used to the Game API, you will see that almost all of these
features came for free. The bulk of this game is made up of simple code
snippets that glue together pre-packaged MIDP 2.0 game functionality.
2. What Has It Not?
At first glance it isn't too bad
for a first pass. However there are a number of things that this game
lacks. Some of the missing features can be classed as 'nice to have' but
quite a few are core and represent common errors made in mobile game
development.
Obviously it doesn't have
menus, a splash screen, score tracking or advanced media. It doesn't use
any particular architecture, would be hard to maintain and is a little
casual with its memory management. Here are some other problems:
It does not handle pausing of the game in any way.
It has no handle to the game thread so this cannot be explicitly managed.
The flag variable running is set after the call to Thread.start()
– so if the game thread starts and is immediately pre-empted by another
thread before line 43 executes, the application is left in an
inconsistent state.
Access to the flag variable running is not synchronized across the GUI and game loop threads (e.g., by using the Java keyword volatile in its declaration).
It
creates a large number of new object instances (Sprites in this case)
instead of re-using a pool of them. The amount of heap in use at any
time depends completely on how often the garbage collector runs.
No
effort is made to manage the frame rate. The thread simply sleeps for
50 ms each time the game loop executes. Changing the sleep period speeds
up or slows down the movement of the ghost sprites.
There is no way to turn off the tone sequence used for sounds. This is very annoying.
There is no concept of 'game over' – it just runs forever with no challenge.
It
uses magic numbers (line 131) for special key codes instead of defining
them as constants. Unfortunately, you also can't use the Canvas.getGameAction()
method to handle softkey events because these are not part of the
standard MIDP key mappings and are specific to each manufacturer.
3. Debrief Summary
So far we have discovered two important concepts regarding game development with Java ME:
This is not all that
surprising. The vast majority of mobile phones in the mass market at the
time MIDP 2.0 was designed had a number of built-in limitations. So
MIDlets did not have much memory, little local storage space and were
rarely paused. Few mobile-phone operating systems are multitasking and
MIDlets that lose focus are often terminated outright. A direct
consequence is that there is a large code base of Java ME sample code in
existence that takes little or no heed of the pause–resume cycle. It is
common to see (as above) a start() method on the GameCanvas that is called whenever startApp()
is called. In almost all cases, this starts a new game thread without
checking for an existing game thread or storing a reference to the game
thread at class scope so that it can be shut down in a well-defined
manner.
Always keep a class-scoped reference to the game thread to control its behavior.
|
|
To help clarify these issues, try the following exercises:
Use the Sun WTK (or whatever IDE you prefer) to start the GhostPong
MIDlet and let it run for a few minutes. You can quickly see the
degradation in performance as more and more Sprite instances are
created. Even though all references to these are removed, there are
simply too many allocated too quickly for the garbage collection thread
to keep up.
Install
the MIDlet on a Symbian OS phone (by cable, Bluetooth transfer or PC
Suite software). Start the game and then move it into the background.
Assuming your phone is not in silent mode, you can hear the game
continuing to execute in the background, happily consuming battery
power, CPU cycles, RAM and delivering a bizarre user experience. Moving
the MIDlet back into the foreground in fact triggers a new game thread
instance and very quickly you have a process with a number of unmanaged
threads.
Where possible, try to reuse objects by using a managed pool of instances.
|
|
Symbian OS is a fully
multitasking operating system which means that Java ME MIDlets need to
be designed to acquire and release resources as they are moved in and
out of the foreground.