So you want to make your own Java games for java enabled mobile phones.
It's either that or you stumbled on this page by mistake.
Just like I said in the introductory post, we'll be using Netbeans IDE on the
tutorials that will be presented here.
Here's a list of things you need and where to get them:
← Java SE Development Kit (JDK) 6.0 | Download Site
← NetBeans IDE 5.5.1/6.0 Beta 2 | Download Site
← Netbeans Mobility Pack 5.5.1 for CLDC/MIDP (only for NetBeans 5.5.1) |
Download Site
Note that you can download the NetBeans IDE 5.5 bundled with the JDK 6.0
from either site. If you're downloading NetBeans 6.0 Beta 2, you can
get it bundled with Mobility pack.
The difference between NetBeans 5.5 and 6.0 is that, in NetBeans 6.0 you
get to try out the nifty Game Builder that helps you create maps, sprites, and
sprite animation sequences. You also get the new syntax highlighting and
other new stuff improved from the previous version.
If you downloaded the bundle:
← Install JDK-Netbeans IDE bundle.
← Install Netbeans Mobility Pack
If you downloaded them individually:
← Install JDK 6.0 Update 3.
← Install Netbeans IDE 5.5.1/6.0 Beta 2.
← Install Netbeans Mobility Pack for CLDC/Midp( only for NetBeans 5.5.1)
That's it! You can now start making J2me applications for your java enabled
mobile phone.
There's one more thing we have to add to our checklist - a way to transfer
your game on your mobile phone. This depends on the phone you're working
on. Here's some options you might be able to choose from :
← Bluetooth - Use this if your phone is Bluetooth capable.
← Infrared Cable - Use this if you don't have Bluetooth.
← USB/Serial Cable - Use this if you have neither Bluetooth nor Infrared
← Card Reader - Use this if your phone has a memory card and you can't
use any of the above.
Aside from the cable/dongle you want to use, you might also need the
software and drivers to make them work. You can get these from your
phone's manufacturer site or the CD-Rom that came with your phone. An
example of the kind of software you might need is the Nokia PC Suite for
Nokia cellphones.
Obviously, you will also need an image editor for creating the graphics you
will use in your game. But for starters you can also find resources on the
internet that offer sprites, tiles and other graphic content for you to practice
on. Some of which I have added to the links section.
Alright, I think that's about it for our checklist. If you have some questions
place a comment under this post
As I've said in the Getting Started tutorial, we will be using NetBeans IDE in
making our applications for Java enabled mobile phones. If you prefer using
the J2me Toolkit you can skip this tutorial as it's a bit about familiarizing the
user with the NetBeans IDE.
In this tutorial we will be creating our first, traditional "Hello World" MIDlet.
"So what's a MIDlet?", you say. Bah! Details...details. Who cares, right?
Seriously, in simple terms a MIDlet is a java program you can run on mobile
devices such as java enabled cellphones. A MIDlet is to your phone as an exe
is to pc (excuse the bad analogy heh).
If you haven't already, open the NetBeans IDE. You should see something
similar to this:
Choose "File" from the main menu and click on "New Project". Select
"Mobile" from the list of Categories and "Mobile Application" from the list of
Projects. Click on the "Next" button.
On the next screen, you get to name your project and choose it's location on
your hard drive. As an example I typed "MyMidlet" in that box. Be sure to
place a check on both "Set as Main Project" and "Create Hello MIDlet"
options before you hit the "Next " button.
Select an emulator platform. I recommend choosing the one with the highest
version. Leave the Device on "Default ColorPhone". Choose CLDC-1.0 from
Device Configuration and MIDP 2.0 for the Device Profile. There's more about
these settings later but for now hit the "Finish" button.
If all went well, NetBeans would have created the MIDlet for you including
the whole "Hello World" program. You can view the source code of the
application by expanding the items of the treeview in the Projects panel.
Click on the Run icon from the toolbar, it's the one with the yellow and green
rectangles (back-to-backarrows?), or you can just press F6 from your
keyboard.
After it has finished compiling and building the program the emulator window
will pop-up. You will see the list of MIDlets on the phone screen with your one
and only MIDlet already highlighted. You can click on the Select button or the
Softkey labeled with "Launch" to run the program or you can simply hit the
Enter key from your keyboard.
Painless, wasn't it? Now about those "Platform Settings" we chose earlier. It really depends on the phone you want your program to run on. Most phones
at the time of writing already supports MIDP 2.0 and this in turn allows us to use new libraries that were added to J2me specifically for game development. From here on, the rest of the tutorials you will find here will be focused on MIDP 2.0 phones.
The reason that I chose CLDC-1.0 is for compatibility. If you're targeting a specific phone model it's best to view the device specification for that phone from the manufacturers website. One of the main changes from CLDC-1.0 and CLDC-1.1 is that in the latter they have added support for real numbers or floats. Unless you really need to use floats in your code I suggest sticking to CLDC-1.0 for the time being. You can also find resources on the internet on how to deal with floats on the CLDC-1.0 platform.
Creating the Midlet
The last tutorial showed you how to use Netbeans IDE to create a basic MIDP
2.0 MIDlet, compile it, and preview your program in the emulator.
Unfortunately, the code generated for the Hello MIDlet is of little or no use
for making games. So in this tutorial you will be shown the basic code
needed for creating your games.
First off, open Netbeans and create a new mobile application project. What
we need is a blank project so remember to uncheck the "Create Hello MIDlet"
on the "Name and Location" screen.
Now we need to create the MIDlet ourselves. There are several ways to go
about this.
← By pressing CTRL+N on your keyboard.
← By choosing New File from the File Menu
← By right-clicking on the project panel treeview and selecting New, then
Midlet
For those of you who used one of the first 2 ways, select MIDP from the
categories and MIDlet from the list of file types then click on the "Next"
button.
On the next screen, type in midMain for MIDlet Name. The MIDlet Class
Name field will be automatically filled in for you. Leave the MIDlet icon blank
for now. As a rule of thumb, never create your files in the "default package".
Enter MyGame in the Package field. A folder with the same name will be
created inside the project source folder where all source code for your game
should go. Click on the "Finish" button.
Congratulations!! You have succeeded on creating an empty MIDlet that does absolutely nothing. Next stop, Creating the Canvas...
We need to make a canvas based on the new GameCanvas class. The canvas
is where we draw all our stuff like images, sprites, maps, scores so it can be
shown on the phones screen. It also let's us know what keys were pressed on
the phone so we can respond to them.
Choose New File from the File menu. Select Java Classes from the list of
categories and Java Class from the list of file types then click on the "Next"
button. Alternatively, you can right-click on the package name then choose
New and Java Class.
Type in clsCanvas for the Class Name. Make sure to set the Package field to
the package name we set earlier for our MIDlet in the first part of this
tutorial. Click on the "Finish" button when you're done.
You should see the source code of class file we just created in the editor
panel. If not, then navigate to the file from the project panel and double-click
on the filename. Except for the author's name and the file/class/package
name (if you chose your own), it should look similar to the code below.
** clsCanvas.java** Created on October 15, 2007, 7:13 PM** To change this template, choose Tools | Template Manager* and open the template in the editor.*/
package MyGame;
/**** @author devlin*/public class clsCanvas { /** Creates a new instance of clsCanvas */
public clsCanvas() { } }
It's now time to turn this class into a GameCanvas class.
Modify this part of the code:
public class clsCanvas {
...into this:
public class clsCanvas extends GameCanvas implements Runnable {
Your code should now look like this:
(I will strip the comments from the code from now on for readability.)
package MyGame;
public class clsCanvas extends GameCanvas implements Runnable { public clsCanvas() { }}
Press Shift+ALT+F (short cut for Fix Imports) to make sure that all import
statements our code requires are detected and added automatically. (Do this
periodically or whenever we use a new class or code.)
package MyGame;
import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable { public clsCanvas() { }}
Now it's time to get rid of those ugly red lines signifying errors in our code.
We first need to fulfill the requirements of the base class that we are
extending so modify the code further like so:
package MyGame;
import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable { public clsCanvas() { super(true); }
public void run() { }
}
Tada! No more errors!
The superclass GameCanvas requires us to implement it's constructor which
has one boolean parameter - suppressKeyEvents. Passing the value true
stops key events like keyPressed, keyRepeated and keyReleased from being
called and optimizing your code. Then how do we respond to user input
then? We use the function getKeyStates() to find out what keys were
pressed whenever we need them. More on this later.
Our canvas class also implements the Runnable class which allows our game
to run on a separate thread. This requires us to implement the run()
method. This will contain our main loop and most game related code.
Let's modify the code further to include the main loop for which we will be
using a while-loop construct. Modify your run() method like so:
public void run() { while(isRunning){
flushGraphics(); } }
Add this private variable under the main class declaration statement
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true;
One feature of the GameCanvas is an off-screen buffer where everything you
draw is first rendered. The contents of this buffer is transferred to the screen
after the flushGraphics() function is called. This eliminates flicker and
makes your animations smoother. So flushGraphics() is called every time
at the end of each loop after you have drawn what you want to be shown on
the screen.
With the isRunning variable set to true, the while loop will run forever. We
must add a way for the loop to terminate and our program to end. Let's allow
the user to end our program when the Fire or 5 key is pressed on the phone.
This is where the getKeyStates() function comes in. Modify the run()
method like so:
public void run() { int iKey = 0; while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){
isRunning = false; }
flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } }
We make the thread sleep or pause for 30 milliseconds after each loop. This
keeps the the thread from hogging the processing power of the phone and
the game from being unresponsive. Without it, the game will not be able to
respond to key presses at the moment it needs to. You can change this value
to what you want as long as it works. We will be adding frame limiting at
some point and that will dynamically adjust the sleep value and make the
frame rate of the game somewhat stable on different phones.
Let's add some drawing code so we can see what we've done later when we
run the application.
Add this code in our global variable declarations right under the isRunning
declaration:
private boolean isRunning = true; private Graphics g;
Add this code in the run() method just before the while loop:
g = getGraphics(); while(isRunning){
Add this code inside the run() method just before the flushGraphics()
function call:
//set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);
flushGraphics();
Add this code at the end of the run() method:
} g = null; }
Hit Shift+ALT+F to update the imports section and your code should
now look like this:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true; private Graphics g;
public clsCanvas() { super(true); } public void run() { int iKey = 0;
g = getGraphics(); while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } //set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);
flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; }}
Notice we declared a new Graphics object named g. The Graphics object contains the methods we must use to draw stuff on the GameCanvas. Just take note that the color passed when setColor() is called will apply to succeeding calls to drawing methods until setColor() is called again with a different color. The Graphics object is also declared as a global variable and is assigned to the actual object right before the main loop. This saves us from having to call the getGraphics() method of the GameCanvas over and over.
Displaying the GameCanvas
In this last part of the tutorial entitled Basic Game Template, we will
modify the code of our MIDlet and Canvas classes to make the both ends
meet and the application to run. It is recommended that you start with Part
1 of this tutorial if you haven't done so.
First, we'll need to add a new global variable to reference the MIDlet from
the canvas class. Let's call it fParent. So add this code to clsCanvas like so:
private Graphics g;private midMain fParent;
Modify the contructor to accept the MIDlet class as a parameter and assign
the value to our global variable:
public clsCanvas(midMain m) { super(true); fParent = m; }
The canvas needs a way to notify the MIDlet that the main loop has ended
and the program needs to terminate. Add this code at the end of our run()
method:
} g = null; fParent.destroyApp(false); fParent = null; }
We need a new method that the MIDlet can call to start the game thread.
Let's call it the start() method and add the code under the clsCanvas
constructor:
public void start(){ Thread runner = new Thread(this); runner.start(); }
You should press Shift+ALT+F after editing the code...just in case.
Here's the completed clsCanvas source code:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true; private Graphics g;private midMain fParent;
public clsCanvas(midMain m) { super(true); fParent = m; } public void start(){ Thread runner = new Thread(this); runner.start(); }
public void run() { int iKey = 0; g = getGraphics(); while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } //set drawing color to black
g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; fParent.destroyApp(false); fParent = null; }}
Now open the source code of our MIDlet. We will define a new global variable
called myCanvas that will allow the MIDlet to create and reference the
clsCanvas class. Place the code just under the midMain class declaration:
public class midMain extends MIDlet { clsCanvas myCanvas;
Next, we will modify the startApp() method of our midlet so that it creates a
new instance of the clsCanvas class, starts it in a new thread, and finally
making the the canvas the current displayed item. Add this code to the
startApp() method:
public void startApp() { Display d = Display.getDisplay(this); myCanvas = new clsCanvas(this); myCanvas.start();
d.setCurrent(myCanvas); }
The last code we will add is for making sure we release all the resources our
MIDlet has used and terminate gracefully when your game has ended. Place
this code in the destroyApp() method:
public void destroyApp(boolean unconditional) { myCanvas = null; notifyDestroyed(); }
Here's the completed midMain source code:
package MyGame;
import javax.microedition.midlet.*;import javax.microedition.lcdui.*;
public class midMain extends MIDlet { clsCanvas myCanvas; public void startApp() { Display d = Display.getDisplay(this); myCanvas = new clsCanvas(this); myCanvas.start(); d.setCurrent(myCanvas); } public void pauseApp() { } public void destroyApp(boolean unconditional) { myCanvas = null; notifyDestroyed(); }}
Yay! We're done! You can now test your by pressing the F6 key on your keyboard or clicking on the Run Main Project icon on the toolbar. That ends our tutorial on creating a basic template for your game. I will also be using this template for the rest of the tutorials that will be posted here. If you have some comments, suggestions or having problem with the source code, feel free to leave a comment or use the ShoutBox. Have fun.
Making a FullScreen Canvas
Updated : 10/24/2007
In this rather short tutorial, we're going to make our canvas take up the
whole screen area of the phone. Open the project you created in the tutorial
Basic MIDP 2.0 Game Template. Open the file clsCanvas.java then
insert setFullScreenMode(true); at the end of the contructor:
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true);}
That's all there is to it.
Update
As commented by our anonymous tipper, some of you might encounter a
bug while calling the setFullScreenMode() function from the constructor of
the GameCanvas class.
I decided to dig deeper on this and I was able to find a post at Sun's Java
Forums. One poster mentioned a problem with the SonyEricsson P900 and
P910 phones. You can read the whole thread here: Link to Thread.
I will try to find more information about bugs or other weird behaviors when
using the setFullScreenMode() function
Loading Images Into Your Game
Updated : 10/22/2007
The title says it all. We're gonna load some graphics into your game. We're
going to build upon the project we made in the tutorial Basic MIDP 2.0
Game Template. So better do that part first if you haven't already. I will be
referring to that project as "project".
Image Format
The preferred image format is the PNG format. This is a good thing since PNG
supports alpha transparency which means that the images can be anti-
aliased and smoother looking. The bad news is not all phones can display
images with alpha transparent pixels correctly. It depends on your target
phone. But it's still better to avoid alpha transparent pixels as much as
possible.
Color Depth
Take note that different phones have different color depths. The color depth
of the phones today ranges from 8-bit(256colors) to 24-bit color (16,777,216
colors). What does this all mean? While your hi-res imagery looks great on
phones with higher depth, they would get dithered on phones with lower
color depth making the images look pixelated. Further loss of color might
even make your images unrecognizable given the small screen size the
game will be viewed from.
There's also only a small amount of memory available on mobile phones for
you to work with and pictures that use more colors eats up more memory. So
as a rule of thumb use less colors for your graphics.
Introducing earth.png
For this tutorial I've prepared a quick-and-dirty 16x16 pixel version of your
home planet. W000t!! I drew a whole planet in less than 5 minutes!! It's a bit
large though coz' it used up 34 colors (502 bytes).
Actual Size of Earth (16x16)
Enlarged View of Earth (64x64)
Open an explorer window and navigate to the folder where you saved the
project and make a backup of the whole project folder just in case something
goes wrong (or for historical purposes). Better yet use a VCS like CVS or VSS.
NetBeans supports either (VSS via plug-in). I
After making a backup, go inside the the project folder and inside the src
folder.
So if the path to your project folder is:
c:\YourDocs\netbeans\BasicGameTemplate
...the path to the src folder should be:
c:\YourDocs\netbeans\BasicGameTemplate\src
Create a new folder inside the src folder named "images". The path to
which should look like this:
c:\YourDocs\netbeans\BasicGameTemplate\src\images
Firefox users can right-click on the earth.png image and choose Save
Image As from the context menu. IE users can right-click on the earth.png
image and choose Save Picture As from the context menu. Save the PNG
file in the images folder you just created. You can save any of the images
above as they are the same file.
An even easier way to create the "images" folder is to:
1. Open the project in NetBeans.
2. Right-click on the project name in the Projects panel.
3. From the pop-up menu, choose New and then Folder.
4. Type "images" as the Folder Name in the resulting dialog box.
5. Make sure src is selected as the Parent Folder.
6. Click on the "Finish" button.
Loading the Image
Open NetBeans and press CTRL+Shift+O. This will bring about the Open
Project dialog box where you can choose the project folder. You can also
click on the purple/violet colored folder icon in the main toolbar. Don't forget
to set it as the main project by selecting the Open as Main Project at the
right side of the dialog box. If that was the same project you had open when
you closed NetBeans, the project would be automatically loaded the next
time you open NetBeans.
Expand the nodes of the treeview in the Projects panel. You should now see
the images folder listed there and earth.png listed under that folder.
You can now open the class file clsCanvas.java so we can start coding.
First we need to make a global variable to hold our image. Let's name it
"imgEarth". Declare imgEarth right under the fParent declaration like so:
private midMain fParent;private Image imgEarth;
Now would be a good time to press Shift+ALT+F to update the imports
section.
Let's make a new method named load() where we will place all the code to
initialize our game. This is where we actually load the image file. Place it
right after the start() method like so:
public void start(){ Thread runner = new Thread(this); runner.start();}
public void load(){ try{ // try to load the image file imgEarth = Image.createImage("/images/earth.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; }}
The createImage() method of the Image class is used to load the image
and assign the resulting Image object to our imgEarth variable. We used
the method inside an exception handling block so you can catch the
exception error it generates when it fails to load the image. Just so you know,
NetBeans won't let you use it without exception handling.
Let's make an unload() method where we can place all the shutdown or
cleanup code our game needs. Place it after the newly created load()
method:
public void load(){ try{ // try to load the image file imgEarth = Image.createImage("/images/earth.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; }}
public void unload(){ // make sure the object get's destroyed imgEarth = null;}
Now it's time to call the new functions inside the run() method. Place the
load() method under the iKey variable declaration like so:
public void run() { int iKey = 0; load(); g = getGraphics();
...then call the unload() method after we assign null to the g variable:
} g = null; unload(); fParent.destroyApp(false);
When you're done, your clsCanvas code should look like this:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true;private Graphics g;private midMain fParent;private Image imgEarth;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true);}
public void start(){ Thread runner = new Thread(this); runner.start();}
public void load(){ try{ // try to load the image file imgEarth = Image.createImage("/images/earth.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; }}
public void unload(){ // make sure the object get's destroyed imgEarth = null;}
public void run() { int iKey = 0; load(); g = getGraphics(); while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } //set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null;}}
Drawing the Image
To draw the image on the screen insert this line of code inside the run()
method just before the flushGraphics() function call:
//draw the image g.drawImage(imgEarth, 50, 50, Graphics.TOP | Graphics.LEFT); flushGraphics();
The drawImage() method of the Graphics object g is used to draw the
image unto our canvas at the coordinates 50(X), 50(Y). The last parameter,
Graphics.Top | Graphics.Left, defines the anchor point or part of the
image that will reside at the given coordinates. In this example the top-left
part of the image will be positioned at coordinates 50,50 , X and Y
respectively.
The completed clsCanvas source code:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true; private Graphics g;private midMain fParent;private Image imgEarth;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); }
public void start(){ Thread runner = new Thread(this); runner.start(); }
public void load(){ try{ // try to load the image file imgEarth = Image.createImage("/images/earth.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } }
public void unload(){ // make sure the object get's destroyed imgEarth = null; }
public void run() { int iKey = 0; load(); g = getGraphics(); while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } //set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT); //draw the image g.drawImage(imgEarth, 50, 50, Graphics.TOP | Graphics.LEFT);
flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
Whenever your ready you can hit F6 on your keyboard and start the MIDlet
when the emulator pops up. You should see the something like the screeny
below.
Got an error? Something could've been done better? --> Post a comment.
Update
The code in this tutorial now runs in fullscreen mode. Details can be found
here : Making a FullScreen Canvas
Clipping Images or Displaying Only Parts of an Image
Sometimes you just need to display certain parts of an image. Like for
instance, when you want to show some animation, It would be better to
place all the animation frames inside a single image file rather than to store
them in several files. This will help save some precious phone memory since
you only have to load one image file into one Image object so there's no
extra overhead from using multiple Image object instances.
Once again we will just be using the project we last modified in the previous
tutorial : Loading Images Into Your Game.
I'm also writing this in preparation for the next tutorial which is about
graphical menus where clipping plays a major part.
Reintroducing Earth: Animated
In this tutorial we're going to show earth spinning on the phone screen like
the image displayed below:
Spinning Earth 16 x 16 pixels
Here is the image strip for that animation. Firefox users can right-click and
choose "Save Image As" from the context menu while IE users can choose
"Save Picture As" instead. Save the image inside the "images" folder of
the "src" folder of the project.
Spinning Earth animation with all frames in a single image.
Dimensions: 144 x 16 pixels
Frame Size: 16 x 16 pixels
Just in case you're wondering, both images were made from using a satellite
image of earth which you can see here:
...which is a bit of an overkill.
Juggling Frames
What we're actually going to do is to draw one frame at a time from the
image strip you just downloaded. All the while keeping the frame we want to
draw inside the clipping rectangle. An illustration would be best to
demonstrate what I'm trying to say:
The red rectangle represents the clipping rectangle and everything outside
that rectangle will not be visible on the screen. The dimmed portion of the
image strip represents the portion that will not be visible on the screen. The
frame number being displayed is the current frame that is inside the clipping
rectangle and is visible on the screen. We decrement the X of the image strip
by 16 pixels to skip to the beginning of each frame going from right to left.
Again the most important thing to remember about clipping rectangles is
that everything outside it is invisible and everything inside it is visible.
It's now time to start NetBeans and open the project. Upon opening the
project you should see the "earthstrip.png" file inside the images folder.
Navigate your way to clsCanvas.java so we can start editing some code. In
the load() method, change "earth.png" to "earthstrip.png".
// try to load the image file imgEarth = Image.createImage("/images/earthstrip.png");
Let's add some variables in the run() method under the iKey declaration:
public void run() { int iKey = 0;
int imgX = 50; // x coordinate of the image int frameIndex = 0; // current frame to be drawn long lDelay = 250; //time to pause between frames in milliseconds long lStart = 0; //time we last changed frames in milliseconds long lCurrTick = 0; // current system time in milliseconds;
Add this code inside the main loop after we check for key presses and before
we call the drawing methods:
if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; }
lCurrTick = System.currentTimeMillis(); if ((lCurrTick-lStart) >= lDelay){ lStart = lCurrTick; // save the current time if (frameIndex < 8) { frameIndex++; // skip to the next frame } else { frameIndex = 0; // go back to first frame } imgX = 50 - (frameIndex * 16); // compute x relative to clip rect } //restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight());
//set drawing color to black g.setColor(0x000000);
There's a lot happening there. First, we store the current system time in
milliseconds to the lCurrTick variable. We use that to check if the time that
has passed since the last time we changed the frame, lStart, is greater than
the delay we set, lDelay. If so, then it's time to change frames. And since we
are changing frames, we will set lStart to the current time for our delay to
work on the next loop.
We have a total of 9 frames in that animation which we index starting at 0.
That means the last frame would have an index of 8. So we check if the
current frame index which is stored in frameIndex is the last index. If not,
then we increment it by 1 frame. If it is the last frame, we then return to the
first frame.
After we have figured out if it's time to change frames and what frame to
display next, we can now compute the X coordinate of the image strip so the
frame we want to show will be drawn inside the clipping rectangle. Here's
the formula in pseudo code:
image_X = X_of_clipping_rectangle - (frame_index * frame_width);
setClip(), Not Scissors!!
You might have also noticed that we called a new method: setClip(). The
setClip() method of the Graphics class is what let's us define the position
and size of the clipping rectangle. The first two parameters defines the
position, x and y, and the last two parameters defines the width and height
of the clipping rectangle.
Let's draw some more.
Add this code in the main loop just before the call to drawImage():
g.drawString("Frame : " + Integer.toString(frameIndex), 2, 68, Graphics.TOP | Graphics.LEFT); g.drawString("X : " + Integer.toString(imgX), 2, 88, Graphics.TOP | Graphics.LEFT); //clip the drawing area to a single frame g.setClip(50, 50, 16, 16);
...and modify the drawImage() function call like so:
g.drawImage(imgEarth, imgX, 50, Graphics.TOP | Graphics.LEFT);
Notice that we placed two calls to the setClip() method of the Graphics
object g. The first call sets the clipping rectangles dimensions to match that
of the whole screen area of the phone. The second call sets the clipping
rectangle to where the animated globe will be drawn. This is because the
dimensions you pass to the setClip() method the last time you call it will be
in effect until you call setClip() again with a different set of parameters. This
means if you don't reset the clipping rectangle, you won't be able to draw
anywhere else on the screen. Anything you draw outside the clipping
rectangle will not show up on the phone screen. So it's good practice to call
the setClip() method with full screen dimensions at the top of your drawing
code.
Your clsCanvas source code should now look like this:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {private boolean isRunning = true; private Graphics g;private midMain fParent;private Image imgEarth;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); } public void load(){
try{ // try to load the image file imgEarth = Image.createImage("/images/earthstrip.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } } public void unload(){ // make sure the object get's destroyed imgEarth = null; }
public void run() { int iKey = 0;
int imgX = 50; // x coordinate of the image int frameIndex = 0; // current frame to be drawn long lDelay = 250; //time to pause between frames in milliseconds long lStart = 0; //time we last changed frames in milliseconds long lCurrTick = 0; // current system time in milliseconds; load(); g = getGraphics(); while(isRunning){ iKey = getKeyStates(); if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } lCurrTick = System.currentTimeMillis(); if ((lCurrTick-lStart) >= lDelay){ lStart = lCurrTick; // save the current time if (frameIndex < 8) { frameIndex++; // skip to the next frame } else { frameIndex = 0; // go back to first frame } imgX = 50 - (frameIndex * 16); // compute x
relative to clip rect } //restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight());
//set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);
g.drawString("Frame : " + Integer.toString(frameIndex), 2, 68, Graphics.TOP | Graphics.LEFT); g.drawString("X : " + Integer.toString(imgX), 2, 88, Graphics.TOP | Graphics.LEFT); //clip the drawing area to a single frame g.setClip(50, 50, 16, 16); //draw the image g.drawImage(imgEarth, imgX, 50, Graphics.TOP | Graphics.LEFT); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
You can start up the MIDlet now. You should see a spinning globe with the
current frame and x coordinate displayed below it. You can also change the
speed in which the globe rotates by increasing or decreasing the value of
iDelay in the run() method. Increasing iDelay will make the animation run
slower while setting it to a lower value will make the animation run faster.
That's it for clipping. If you have a question or would like to point out a
mistake, feel free to post a comment. Have fun!!!
Thursday, October 25, 2007
Input Handling : Keypress with Repeat Rate
There are a lot of ways to deal with user input in your game. This tutorial will
show one of the solutions that you can use on certain parts of the game. Like
when your prompting the user to exit the game or not, while in the main
menu, or when you want the user to choose from some options on the
screen. As a warning, we're going to limit the code to using the
getKeyStates() method only.
The Problem : It's Like The Keys Get Stuck..
Continues key presses are good while playing the game because the game
can respond at the heat of the moment and match the users super reflexes.
But not during selection screens. Why so? Let's say your game has a frame
rate of 15 frames per second. That's also the speed at which it can act every
time the user presses a key on the phone.
Imagine that in your game the second item from the top of the list is the
View Instructions item and the current selected or highlighted item is the
New Game item which is the first item on the menu. The user, trying to get
to the Instructions item, presses the down button and the selection moves
down from one item to the other at an alarming rate of 15 times in one
second. The confused user will not be able to accurately pick items from the
menu.
Better yet, imagine typing a document in a text editor and every time you hit
the keyboard you get 15 of each character you type in. It would be like
driving a car without breaks (No, I'm not drawing that for you!!!).
Making the Keys Toggle
In this tutorial we're going to add some interactivity to the spinning globe we
made in the last tutorial : Clipping Images or Displaying Only Parts of
an Image. We'll change the code so that the user can adjust the speed at
which the globe spins. So load the project in NetBeans and open the
clsCanvas code.
Let's declare some constants and variables. Insert this code under the
clsCanvas class declaration:
public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;
public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = {false, false, false, false, false};
//last time the key changed stateprivate long[] keyTick = {0, 0, 0, 0, 0};
//lookup table for key constants :Pprivate int[] keyValue = {GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED,GameCanvas.FIRE_PRESSED};
The value assigned to the constant keyDelay will determine how fast a key
state changes from being pressed to not pressed while holding down a key.
In other words, this will be the repeat rate. The higher the number, the
slower the repeat rate gets and vice versa.
The next five constants we declared are for the 5 standard game keys Up,
Left, Down, Right, and the Fire key. We just defined them so our code
won't become confusing, so instead of using numbers we can use
"meaningful words". Also, someone told me about 12-14 years ago that it
was good practice to define constants for numbers or other fixed values your
going to use repeatedly all over your code.
The boolean array isDown[] is going to hold the state for each of the
standard keys we've defined while the long array keyTick[] will hold the
time when a key last changed from being down to up.
The last array keyValue[], an integer array to hold the actual key codes for
each key. We defined this so we can loop through each key in our key
detection code which you will see in the next code section.
We'll add a new method to our clsCanvas class called checkKeys(). Add
the code above the run() method:
public void checkKeys(int iKey, long currTick){ long elapsedTick = 0;
//loop through the keys for (int i = 0; i < 5; i++){
// by default, key not pressed by user isDown[i] = false;
// is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i];
//is it time to toggle key state? if (elapsedTick >= keyDelay){
// save the current time keyTick[i] = currTick;
// toggle the state to down or pressed isDown[i] = true; } } } }
public void run() {
The checkKeys() method takes the value returned by the getKeyStates()
method in the iKey parameter and the current time in milliseconds in the
currTick parameter. It then loops through all the keys in the keyValue[]
array too see if a certain key is being pressed. It then toggles the value of
the appropriate element in the isDown[] array depending on how long it's
been since the key was in the pressed state. It also makes sure that the
isDown[] array is updated as to which keys are not being pressed.
Let's change our run() method so that when the user presses the left key
the spinning animation speeds up and when the user presses the right key
the animation slows down. First let's get rid of the old way we detect the fire
key so that it makes use of the new code we just added. So delete or
comment the following lines from the run() method, from inside the while
loop:
/* if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } */
Next, insert the following code under the line where we store the current
time in lCurrTick so we can pass that value to the checkKeys() method:
lCurrTick = System.currentTimeMillis(); checkKeys(iKey, lCurrTick); if (isDown[leftKey]){ if (lDelay > 0){ lDelay-=10; } } else if (isDown[rightKey]){ if (lDelay < 1000){ lDelay+=10; } } else if (isDown[fireKey]){ isRunning = false; }
Add this next code just before the second call to setClip() so we can display
some feedback when the keys are pressed:
g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT); if (isDown[upKey]) { g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[leftKey]) { g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[downKey]) { g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[rightKey]) { g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); }
//clip the drawing area to a single frame g.setClip(50, 50, 16, 16);
The last code we added displays the value of lDelay so we know if it's
actually getting changed. It also displays notification whenever one of the
directional keys are pressed. It doesn't display feedback for the Fire key
because when that is pressed the MIDlet is terminated.
Here's the completed clsCanvas source code:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};
//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};
//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;private Image imgEarth;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); } public void load(){ try{ // try to load the image file imgEarth =
Image.createImage("/images/earthstrip.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } } public void unload(){ // make sure the object get's destroyed imgEarth = null; } public void checkKeys(int iKey, long currTick){ long elapsedTick = 0;
//loop through the keys for (int i = 0; i < 5; i++){
// by default, key not pressed by user isDown[i] = false;
// is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i];
//is it time to toggle key state? if (elapsedTick >= keyDelay){
// save the current time keyTick[i] = currTick;
// toggle the state to down or pressed isDown[i] = true; } } } }
public void run() { int iKey = 0;
int imgX = 50; // x coordinate of the image int frameIndex = 0; // current frame to be drawn long lDelay = 250; //time to pause between frames in milliseconds long lStart = 0; //time we last changed frames in
milliseconds long lCurrTick = 0; // current system time in milliseconds; load(); g = getGraphics(); while(isRunning){ iKey = getKeyStates(); /* if ((iKey & GameCanvas.FIRE_PRESSED) != 0){ isRunning = false; } */ lCurrTick = System.currentTimeMillis(); checkKeys(iKey, lCurrTick); if (isDown[leftKey]){ if (lDelay > 0){ lDelay-=10; } } else if (isDown[rightKey]){ if (lDelay < 1000){ lDelay+=10; } } else if (isDown[fireKey]){ isRunning = false; } if ((lCurrTick-lStart) >= lDelay){ lStart = lCurrTick; // save the current time if (frameIndex < 8) { frameIndex++; // skip to the next frame } else { frameIndex = 0; // go back to first frame } imgX = 50 - (frameIndex * 16); // compute x relative to clip rect } //restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight()); //set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight());
// set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);
g.drawString("Frame : " + Integer.toString(frameIndex), 2, 68, Graphics.TOP | Graphics.LEFT); g.drawString("X : " + Integer.toString(imgX), 2, 88, Graphics.TOP | Graphics.LEFT); g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT); if (isDown[upKey]) { g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[leftKey]) { g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[downKey]) { g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } else if (isDown[rightKey]) { g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT); } //clip the drawing area to a single frame g.setClip(50, 50, 16, 16); //draw the image g.drawImage(imgEarth, imgX, 50, Graphics.TOP | Graphics.LEFT); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null;
}}
Great! Now you can hit F6 and view the result of your work.
Ponts to Ponder
There are a few more things you can do to the sample code presented in this
tutorial. For instance, you can add support for the other game keys or move
the checkKeys() method in it's own class along with the supporting
variables and even make it a static method. You can also change the value
of keyDelay to that which suits you. Finally, with a few modification you can
use the same technique when handling key codes from keyPressed() and
keyReleased() call back methods.
Although the sample code used here is quite usable, it doesn't mean you
can't do it in your own way. What matters most is that you have a general
understanding of the problem and how to solve it in a simple but effective
manner.
Something is not working? I got it totally wrong? You got a question? Feel
free to post a comment. XD
Adding an Icon to Your MIDlet or Game
I was looking at my blog's search statistics at Google Webmaster Tools
when I found a few users using key words like "j2me icon MIDlet", "MIDP
icon bit depth", "MIDlet icon transparency". So I decided to write this
tutorial just for those people...how ever late it might seem to be.
If you we're following the series of tutorials presented here, you would recall
(or not) that in first part of the tutorial Creating a Basic MIDP 2.0 Game
Template you were prompted for an icon to associate with your MIDlet.
We chose to ignore the field because it seemed trivial at the time. But I bear
great news to you! Because in NetBeans you can still add an icon to your
MIDlet through the Project Properties window!!! Oh, so you knew that
already. Well, for those of you who don't, you can access the Project
Properties from the File Menu and choosing the menu item labeled with
"Your-Project-Name" Properties, with the double quotes.
You can also reach the Project Properties by right-clicking on the project
name from the Projects Panel treeview and selecting Properties at the
bottom of the context menu.
Before you go adding those uber cool icons one thing you have to remember
is that size does matter when it comes to MIDlet icons. Meaning the width
and height of the icon. If you also read the tutorial Loading Images Into
Your Game, it was mentioned that the number of colors used should be
kept at a minimum, preferably at the 8-bit depth of 256 colors max. You
should also avoid anti-aliased icons because some phones don't support
them and will display the anti-aliased icon with "dirty" edges or with random
pixels around the edges that are supposed to be semi-transparent. The PNG
format is also the preferred and widely used file format for a MIDlet icon.
Here are some icon sizes you may consider:
Phone/Model
Resolution128x128
128x160
176x208
208x176
208x208
240x320
320x240
352x416
416x352
BlackBerry
45x45
Motorola
15x1516x1632x32
Nokia S40
16x16 18x1824x2429x29
n/a n/a 46x46 46x4842x29
n/a n/a n/a
Nokia S601st/2nd Edition
n/a n/a 42x2929x29
n/a n/a n/a n/a 76x76 n/a
Nokia S603rd Edition
n/a n/a 31x3142x29
37x37 37x37 53x5355x5564x64
52x5254x54
64x64 76x7684x58
Sagem 18x18Samsung
16x1629x2932x32
Sanyo 24x24Sharp 27x27Siemens
18x18
Sony Ericsson
16x1632x32
Note that most of the icon sizes for the Nokia mobile phones came from
the Using Icons in Midlets technical note at Forum Nokia. Some of the
additional sizes came from personally testing them on various phone models.
The icon sizes for the other brands of mobile phones came from other
developers experiences.
Mobile phones behave differently from one another on the way they try to
display an incorrectly sized icon. Some phones will scale your icons to the
right size. Other phones will display the icon as a small glyph at the upper-
left corner of the area where the full sized icon should appear. The glyph
displayed is so small you can barely see it. If that's the case, then you can
try the next larger icon size. Some phones will also clip the icon to fit the
area reserved for it like in the case of using 42x29 pixel icons on older Series
60 phones with a resolution of 176x208.
There are several ways to find out what icon size you can use. One sure way
to find the size that fits your target phone is by extracting the contents of the
jar file of a game or application designed for that phone. Somehow that
didn't quite come out right...
If your phone supports themes or allows you to customize the skin, you can
usually download a theme editor or supplemental documents for creating
themes/skin for your phone and from there you can gather the information
you need.
Here are some tools you can use to make the PNG images:
← GIMP - Link to Site
← Paint.Net - Link to Site
← Adobe Fireworks - Link to Site
← Adobe Photoshop - Link to Site
← You can find a lot more tools you can use on the internet.
I won't go into details as to how to use those applications as such is beyond
the scope of this tutorial. I'll just give you a screenshot of the settings I use
in Fireworks. It should provide some insight on the settings you can use in
other applications.
These are the settings I use on icons with a transparent background. Notice
that there's only one transparent color in the pallet:
These are the settings I use on icons with no transparency:
I have prepared a transparent icon for you to use in this tutorial. You can use
your own work of art if you want to.
42x29 pixels
Firefox users can right-click on the image and choose Save Image As
from the context menu while IE users can choose Save Picture As from the
context menu. Save the PNG file inside the images folder inside the projects
src. If you can't find the images folder then make one.
You can always just place the file inside the src folder and it will be listed
under the Default package. But then again I find it easier to work on the
game when all the images are kept inside a folder of their own.
Open the Project Properties in NetBeans and navigate your way to the
MIDlets section which is listed under Application Descriptor. You will find
your MIDlets listed here. Double-click on the MIDlet or select the MIDlet
and click on the Edit button to the right of the list.
By now, NetBeans would have detected the new image you have saved
earlier and you can simply choose it from the images listed in the combo box
of the Edit MIDlet dialog box.
Click the "Ok" button on the Edit MIDlet dialog to confirm your selection
then click the "Ok" button again in the Project Properties window to
commit your changes. Press F6 to test your MIDlet in the emulator.
When you run the emulator you will see the MIDlet brandishing your new
icon. Hold on a second?!! Why does our 42x29 icon look squashed and
squarish? That's only how the emulator displays your icon. You will only find
out what it actually looks like once you have tested your MIDlet on a real
phone.
I hope this tutorial have at least given you a clue of how to find your way
through the murky world of MIDlet icons.
Some humble words from Forrest Grumpy:
My momma always said, "MIDlet Icons was like a box of chocolates.
You never know what you're gonna get."
No Commands: Delicious Graphical Menus
Aesthetics plays an important part in the games that you make. Making the
main menu look like it fits the game is equally as important. If done right, it
can set the mood for the player even before actually playing the game.
Let's face it. Using Command Listeners and objects for your menus just
doesn't feel right. Command objects just don't look like they're part of the
game. On some phones, the game is hidden when the menu is activated.
Phones don't even display the label for the keys you have to press to
activate the menu when the game is in fullscreen mode. Graphical menus
solves these problems and makes the game look cool too.
Clipping is used extensively in this tutorial. If you need a quick study, turn to
this tutorial : Clipping Images or Displaying Only Parts of an Image.
Loading the Images
In this tutorial we will make a simple vertical menu in the middle of the
canvas. The MIDlet will be designed for mobile phones with 176x208 screen
resolution.
I have prepared a clean project for you to start with. It includes the basic
game template and the input handling code from the tutorial Input
Handling : Keypress with Repeat Rate. It also includes the images we're
going to use for the menu. Download the one for the version of NetBeans you
will use:
← BasicGameTemplate4NB5.zip (39.04 KB) - project code for
NetBeans 5.5
← BasicGameTemplate4NB6.zip (45.03 KB) - project code for
NetBeans 6.0
When you open the project in NetBeans, you should see these 2 files inside
the images folder:
logo.png - 176x208 pixels
menuitems.png - 82x80 pixels
First we'll have to load the images. So open the clsCanvas code and add
these variable declarations above the constructor:
private midMain fParent;
private Image imgBG;private Image imgMenu;
public clsCanvas(midMain m) {
Add the actual image loading code in the load() method. Make sure they are
inside the try..catch or exception handling code:
public void load(){ try{ // load the images here imgBG = Image.createImage("/images/logo.png"); imgMenu = Image.createImage("/images/menuitems.png");
}catch(Exception ex){
Add these lines inside the unload() method to make sure the resources
reserved for those objects will get freed:
public void unload(){ // make sure the object get's destroyed imgMenu = null; imgBG = null; }
The logo can now replace the call to fillRect() that we used to clear the
screen since the logo takes up the whole screen. Remove or comment the
lines from the main loop as indicated below:
//restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight()); /* start - delete lines //set drawing color to black g.setColor(0x000000); //fill the whole screen g.fillRect(0, 0, getWidth(), getHeight()); */ end - delete lines // set drawing color to white g.setColor(0xffffff);
Now we can draw the logo. Insert this code under the call to setClip() inside
the main loop :
//restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight());
g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT);
Drawing the Menu
We'll need a variable to store the currently focused menu item. Let's call it
menuIndex and declare it under the other variables we declared earlier:
private Image imgMenu;
private int menuIndex = 0;
public clsCanvas(midMain m) {
The drawing code for the menu will be placed on a new method name
drawMenu(). Add this code above the run() method:
public void drawMenu(Graphics g){ int cy = 0; for (int i = 0; i < 5; i++){ //compute the Y position of the menu item cy = 64 + (i * 22); //set the clipping rectangle to where the item will be drawn g.setClip(47, cy, 82, 20); if (menuIndex == i){ //draw the light button if the item is focused g.drawImage(imgMenu, 47, cy - 20, Graphics.TOP | Graphics.LEFT); } else { //draw the dark button if the item is not focused g.drawImage(imgMenu, 47, cy, Graphics.TOP | Graphics.LEFT); } //offset of the label is 6 pixels from the top of the button cy += 6; //set the clipping rectangle to where the label will be drawn g.setClip(47, cy, 82, 8);
//draw the label so that it is inside the clipping rectangle g.drawImage(imgMenu, 47, cy - (40 + (i * 8)), Graphics.TOP | Graphics.LEFT); } }
public void run() {
The menu will be drawn 64 pixels from the top of the screen and the menu
items are drawn at 22 pixel intervals and since each of the menu items is
only 20 pixels in height, a 2 pixel space will be left between the menu
items acting as a margin. Depending on the value of menuIndex, either a
light-blue or a dark-blue button will be drawn. Finally, the label of each menu
item is drawn 6 pixels lower than the menu items position.
To show the menu on the screen, simply insert the following lines of code at
the point after which the logo has been drawn:
//restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight()); //draw the logo g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT); //draw the menu drawMenu(g); //restore the clipping rectangle to full screen again g.setClip(0, 0, getWidth(), getHeight()); // set drawing color to white
Notice that we now have 2 calls to setClip() where the clipping rectangle is
reset to fullscreen. The second call is necessary because when the
drawMenu() is finished, the method would have resized and positioned the
clipping rectangle to that of the last label in the menu. Reseting the clipping
rectangle after drawMenu() is called allows us to draw more stuff in other
parts of the screen.
The only thing left to do now is to make the menu respond to keypresses.
Modify the conditional statement under the call to the checkKeys() method
inside the main loop like so:
checkKeys(iKey, lCurrTick);
if (isDown[upKey]){ //move focus up if (menuIndex > 0){ menuIndex--; } else { menuIndex = 4; } } else if (isDown[downKey]){ //move focus down if (menuIndex < 4){ menuIndex++; } else { menuIndex = 0; } } else if (isDown[fireKey]){ //do action depending on the menu item selected if (menuIndex == 4){ isRunning = false; } }
The last code allows us to move the highlight or focus from one menu item to
the other. Pressing the UP key should now move the focus one menu item
higher in the list. Pressing the DOWN key should move the focus one menu
item lower in the list. Pressing the FIRE key should select the focused menu
item and initiate an "action". So far, there is only one "action" defined in this
menu and that is for exiting the MIDlet from the EXIT menu item.
Here's the completed clsCanvas code so you can check if you missed
something:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private Image imgBG;private Image imgMenu;//stores the focused menu itemprivate int menuIndex = 0;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true);
}
public void start(){ Thread runner = new Thread(this); runner.start(); }
public void load(){ try{ // load the images here imgBG = Image.createImage("/images/logo.png"); imgMenu = Image.createImage("/images/menuitems.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } }
public void unload(){ // make sure the object get's destroyed imgMenu = null; imgBG = null; }
public void checkKeys(int iKey, long currTick){ long elapsedTick = 0; //loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } }
public void drawMenu(Graphics g){
int cy = 0; for (int i = 0; i < 5; i++){ //compute the Y position of the menu item cy = 64 + (i * 22); //set the clipping rectangle to where the item will be drawn g.setClip(47, cy, 82, 20); if (menuIndex == i){ //draw the light button if the item is selected g.drawImage(imgMenu, 47, cy - 20, Graphics.TOP | Graphics.LEFT); } else { //draw the dark button if the item is not selected g.drawImage(imgMenu, 47, cy, Graphics.TOP | Graphics.LEFT); } //offset of the label is 6 pixels from the top of the button cy += 6; //set the clipping rectangle to where the label will be drawn g.setClip(47, cy, 82, 8); //draw the label so that it is inside the clipping rectangle g.drawImage(imgMenu, 47, cy - (40 + (i * 8)), Graphics.TOP | Graphics.LEFT); } }
public void run() { int iKey = 0; long lCurrTick = 0; // current system time in milliseconds; load(); g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates(); checkKeys(iKey, lCurrTick); if (isDown[upKey]){ //move focus up if (menuIndex > 0){ menuIndex--; } else {
menuIndex = 4; } } else if (isDown[downKey]){ //move focus down if (menuIndex < 4){ menuIndex++; } else { menuIndex = 0; } } else if (isDown[fireKey]){ //do action depending on the menu item selected if (menuIndex == 4){ isRunning = false; } } //restore the clipping rectangle to full screen g.setClip(0, 0, getWidth(), getHeight());
//draw the logo g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT); //draw the menu drawMenu(g); //restore the clipping rectangle to full screen again g.setClip(0, 0, getWidth(), getHeight()); // set drawing color to white g.setColor(0xffffff); //display the key code last pressed g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
Press the F6 key in NetBeans to see if it actually works.
Here's a link to a low-res flash video of the MIDlet running on an N70:
Click to View Video
The technique used here to draw the vertical menu is pretty old school. I've
been using it in DOS based games written in Turbo Pascal, DirectX games
written in VisualBasic 6.0, and in C# using XNA Game Studio Express. One
thing to remember is that you're not just limited to using vertical menus in
your game. I just used a vertical menu as an example because they are very
user friendly and easy to make. You can change the shape of the menu items
to anything you want, knowing that you can use transparent images or
change the colors to match your game. Here's a screenshot of a character
selection screen for a demo quiz game:
Character sprites came from the MMORPG Trickster.
Using the graphics methods to make the menus for your game let's you use
your creativity and the design is only limited by your imagination. Just
remember that however unlimited your imagination may seem to be, the
phone on the other hand, has limited capabilities.
~John Constantime:
"There's always a catch...damn cellphones!"
Wednesday, November 7, 2007
Using Custom Fonts or Bitmap Fonts in MIDP 2.0 Part I
Jump to part: 1 | 2
Have you ever wondered how those other games display scores and text
with different styles and colors? You probably found out you can't do that
with drawString() and setFont() alone. The truth is that those numbers
and letters were drawn using images or what we call Bitmap Fonts.
Ok, so that's really old news. But I'll have you know that about a decade and
a half ago, it was the only decent way to draw numbers and text on your
game screen. I think that makes it sound even much older...moving on...
Bitmap fonts also solves some portability issues in your game as different
phones sometimes have different font sizes and can really screw up your
design if your relying only on the drawString() method. Using bitmap fonts
ensure that those scores and text will look exactly the same on your target
phones.
In this tutorial you will be shown how to use bitmap fonts with a little help
from the setClip() method and type casting. If you need a jump starter on
clipping, you can follow the tutorial on Clipping Images or Displaying
Only Parts of an Image before you continue with this tutorial.
We'll be using the new project templates which you can find here:
Clean MIDP 2.0 Game Templates
The ASCII Table
Like I mentioned earlier, images are used to draw those numbers and letters.
Actually, it's an image strip where each frame contains one character. Here's
an example of a bitmap font image:
Bitmap font sample zoomed in 2x.
Frame size: 9x10 pixels.
Click on the image to see what it actually looks like.
Since it's an image, you have a higher degree of freedom when it comes to
customizing the way the font looks. Just remember that you can cause
someones eyes to bleed and ultimately delete your game from their phones
if you choose a repulsive set of colors and a barely readable font size. What
am I saying?!!
The characters on the bitmap font image are arranged based on the
standard ASCII table :
This makes it easy for us to determine the location of a character on the
bitmap font image using this formula:
positionX = ((int)theCharacter) * frameWidth;
Type casting a char data type to an int gives you the ordinal value of the
character, meaning it's Decimal value in the ASCII table.
You may have noticed that there are black and gray rectangles at the
beginning of the image. I placed them there as markers for non-printable
characters. Because the first 32 characters in the ASCII table are non-
printable, meaning they can't be drawn on the screen. Even the last
character in the table, the Delete character, cannot be drawn. So only the
characters whose Decimal value is in-between 31 and 127 can be drawn.
Creating the Bimap Font Writer : the clsFont Class
You can now open the project templates you just downloaded in NetBeans.
We will start by making a new class called "clsFont". A demo in how to
create a new class can be found in part II of the tutorial Basic MIDP 2.0
Game Template : Creating the GameCanvas. When your done, you
should now see something like this:
package MyGame;
public class clsFont { /** Creates a new instance of clsFont */ public clsFont() { } }
Next, we'll declare some global variables under the class declaration:
public class clsFont {
// additional space between characters public int charS = 0; // max clipping area
public int screenW = 176; public int screenH = 208; // flag: set to true to use the Graphics.drawString() method // this is just used as a fail-safe public boolean useDefault = false; // height of characters public int charH = 10; // lookup table for character widths public int[] charW = { // first 32 characters 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // space 9, // everything else :P 3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 7, 3, 7, 3, 9, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 4, 6, 3, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 5, 3, 5, 4, // delete character 9}; // the bitmap font image public Image imgFont;
Press ALT+Shift+F or choose "Fix Imports" from the "Source" menu so
NetBeans can automagically add those missing import statements.
The variable charS is used to define additional space between characters.
You can change it if you feel that the characters are too close together and
hard to read. It can also be useful if you want to do a spring effect animation
and make the characters bounce sideways.
The screenW and screenH defines the maximum screen area we can draw
on. They are used to make sure that the clipping rectangle always falls in the
area defined. You will know more about that later.
Next we have the useDefault variable. This is purely optional and we'll use
that to signal if the image file failed to load. More on this later too.
The charH variable stores the maximum height of the characters and is used
to adjust the height of the clipping rectangle.
The integer array charW[] holds the pre-calculated widths of each
character. We use this to calculate the exact place where each character in a
given string should be drawn and how much space each of them should
occupy. This way, the string displayed will look more natural. It also helps
save some screen space since the space used by each character is adjusted
to only how much space is needed. Unlike monoblock style fonts where each
character uses the same width even if the characters themselves only take
up half as much space. One thing to note about the values stored in
charW[] is that they already include adequate character spacing. But you
can still use the charS variable to adjust the spacing if necessary.
The last variable imgFont will hold the actual bitmap font image.
We will now add the load() method for loading the image and the unload()
method for cleaning up when we're done using the class. Add these lines
after the class constructor:
/** Creates a new instance of clsFont */ public clsFont() { } public boolean load(String imagePath){ useDefault = false; try{ // load the bitmap font if (imgFont != null){
imgFont = null; } imgFont = Image.createImage(imagePath); } catch (Exception ex){ // oohh we got an error then use the fail-safe useDefault = true; } return (!useDefault); } public void unload(){ // make sure the object get's destroyed imgFont = null; }
The load() method takes the string parameter imagePath which defines
the path to the bitmap font image that we want to load. If it fails to do so, the
useDefault variable will be set to true. You can check this variable in your
game so you know if the bitmap font got loaded or not and act accordingly.
The load() method also returns the inverse value of useDefault so you can
use the method in your condition statement to load the bitmap font and at
the same time check if it was loaded as in the example below:
if (!myFont.load("/images/fonts.png")){ /* ...do something to handle the error when the image fails to load... ... ... */ }
Next we will add the drawChar() method which will be used to draw a single
character on the screen. Add it beneath the unload() method:
public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){ // non printable characters don't need to be drawn if (cIndex < 33){ return; }
// neither does the delete character if (cIndex > 126){ return; }
// get the characters position int cx = cIndex * 9;
// reset the clipping rectangle g.setClip(0, 0, screenW, screenH);
// resize and reposition the clipping rectangle // to where the character must be drawn g.clipRect(x, y, w, h);
// draw the character inside the clipping rectangle g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT); }
The drawChar() method takes a few parameters:
← Graphics g - the graphics object used to draw the image
← int cIndex - the ordinal value of the character to be drawn
← int x - the horizontal position of the character and clipping rectangle
on the screen
← int y - the vertical position of the character and clipping rectangle on
the screen
← int w - the width of the character and clipping rectangle
← int h - the height of the character and clipping rectangle
The drawChar() method checks to see if the character supposed to be
drawn is a non-printable character by checking the value of cIndex. It then
computes the position of the character on the image and stores it in for later
use. The method resets the clipping rectangle to fullscreen using the
setClip() method and adjusts the clipping rectangle to exactly where the
character should be drawn and matches it's size by using the clipRect()
method. It then draws the character on the screen and inside the clipping
rectangle.
Using setClip() in conjunction with clipRect() is a good way of ensuring that
the resulting clipping rectangle is within the phones screen boundaries so
that the drawing methods will not draw anything outside the screen. Some
phones behave erratically when you try draw stuff outside the screen and
produce garbled output.
Let's add the final method for the clsFont class, the drawString() method.
This is the method we will call in our game to draw the strings, scores, and
other text. Add it under the drawChar() method:
public void drawString(Graphics g, String sTxt, int x, int y){
// get the strings length int len = sTxt.length();
// set the starting position int cx = x; // if nothing to draw return if (len == 0) { return; } // our fail-safe if (useDefault){ g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT); return; }
// loop through all the characters in the string for (int i = 0; i < len; i++){
// get current character char c = sTxt.charAt(i);
// get ordinal value or ASCII equivalent int cIndex = (int)c;
// lookup the width of the character int w = charW[cIndex];
// draw the character drawChar(g, cIndex, cx, y, w, charH);
// go to the next drawing position cx += (w + charS); } }
The drawString() method parameters:
← Graphics g - the Graphics object that will be used to draw the string
← String sTxt - the string to be drawn
← int x - the horizontal starting position of the string on the screen
← int y - the vertical position of the string on the screen
The drawString() method first gets the length of the string and the X
position where the first character will be drawn. It then checks the length of
the string to see if the string is not empty, otherwise it exits the method.
Now you will get to see what the useDefault is used for. If it's set to true,
meaning the bitmap font image wasn't loaded, the regular drawString()
method of the Graphics object is used to draw the string instead of using
the bitmap fonts.
The drawString() method loops through each character of the string and
gets the ordinal value of each character. The ordinal values are used to get
each of the characters widths from the lookup table charW[]. It is also
passed to the drawChar() method to define the width of the clipping
rectangle. After the current character has been drawn, the width is added to
the current drawing position and the method proceeds with the next
character. If you put a value other than 0 in charS, it will also be added to
the current drawing position.
Here's the completed clsFont source code so you can check your work:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;
public class clsFont { // additional space between characters public int charS = 0; // max clipping area public int screenW = 176; public int screenH = 208; // flag: set to true to use the Graphics.drawString() method // this is just used as a fail-safe public boolean useDefault = false; // height of characters public int charH = 10; // lookup table for character widths public int[] charW = { // first 32 characters 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // space 9, // everything else XD 3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 7, 3, 7, 3, 9, 6, 4, 6, 6, 6,
6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 4, 6, 3, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 5, 3, 5, 4, // delete 9}; // the bitmap font image private Image imgFont; /** Creates a new instance of clsFont */ public clsFont() { } public boolean load(String imagePath){ useDefault = false; try{ // load the bitmap font if (imgFont != null){ imgFont = null; } imgFont = Image.createImage(imagePath); } catch (Exception ex){ // oohh we got an error then use the fail-safe useDefault = true; } return (!useDefault); } public void unload(){ // make sure the object get's destroyed imgFont = null; } public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){ // non printable characters don't need to be drawn if (cIndex < 33){ return; }
// neither does the delete character if (cIndex > 126){
return; }
// get the characters position int cx = cIndex * 9;
// reset the clipping rectangle g.setClip(0, 0, screenW, screenH);
// resize and reposition the clipping rectangle // to where the character must be drawn g.clipRect(x, y, w, h);
// draw the character inside the clipping rectangle g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT); } public void drawString(Graphics g, String sTxt, int x, int y){ // get the strings length int len = sTxt.length();
// set the starting position int cx = x; // if nothing to draw return if (len == 0) { return; } // our fail-safe if (useDefault){ g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT); return; }
// loop through all the characters in the string for (int i = 0; i < len; i++){
// get current character char c = sTxt.charAt(i);
// get ordinal value or ASCII equivalent int cIndex = (int)c;
// lookup the width of the character int w = charW[cIndex];
// draw the character drawChar(g, cIndex, cx, y, w, charH);
// go to the next drawing position cx += (w + charS); } } }
Press Shift+F11 or choose "Clean and Build Main Project" from the
"Build" menu to test-build your project and to see if anything is missing. You
can also choose to click on the toolbar icon with the broom glyph to clean
and build your project. If everything went well, we can move on to the next
step: Word-Wrap and More.
Using Custom Fonts or Bitmap Fonts Part II
Jump to part: 1 | 2
On the previous part of this tutorial, we created a clsFont class for drawing
bitmap fonts. Now we will use the class to see if it actualy works. So if you
haven't read the first part of this tutorial just click on this link: Using
Custom Fonts or Bitmap Fonts Part I. I have also added more methods
to the clsFont class that you may find useful so stick around a bit.
Using the Bitmap Font Writer
Open the clsCanvas code so we can begin. We will start by adding a new
global variable to hold an instance of the clsFont class. Let's call it
"myFont" and add it under the other variable declarations like so:
private midMain fParent;
private clsFont myFont;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) {
If you expand the images folder on the Projects panel, you should see the
image file "fonts.png" listed there. This is the bitmap font we're going to
pass to the clsFont.load() method. Add these lines in the end of the
clsCanvas.load():
} myFont = new clsFont(); myFont.load("/images/fonts.png");
}
We will also call the clsFont.unload() method inside the
clsCanvas.unload() method like so:
public void unload(){ // make sure the object get's destroyed
myFont.unload(); myFont = null;
}
Lastly, we will use the clsFont.drawString() method to draw some text on
the screen. Add these lines above the call to flushGraphics() in the main
loop:
g.fillRect(0, 0, screenW, screenH); myFont.drawString(g, "Hello Neo...", 10, 50); myFont.drawString(g, "1234567890", 10, 70); myFont.drawString(g, "ABCDEFG abcdefg", 10, 90); flushGraphics();
Test your MIDlet by pressing F6 on your keyboard and you should see
something like this:
I almost forgot to post the completed clsCanvas source code:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};
//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};
//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private clsFont myFont;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); }
public void load(){ try{ // load the images here }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } myFont = new clsFont(); myFont.load("/images/fonts.png");
} public void unload(){ // make sure the object gets destroyed
myFont.unload(); myFont = null;
} public void checkKeys(int iKey, long currTick){ long elapsedTick = 0; //loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } }
public void run() { int iKey = 0; int screenW = getWidth(); int screenH = getHeight();
long lCurrTick = 0; // current system time in milliseconds; load(); g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates(); checkKeys(iKey, lCurrTick); if (isDown[fireKey]){ isRunning = false; } //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); //set drawing color to black g.setColor(0x000000); //fill the screen with blackness g.fillRect(0, 0, screenW, screenH); myFont.drawString(g, "Hello Neo...", 10, 50); myFont.drawString(g, "1234567890", 10, 70); myFont.drawString(g, "ABCDEFG abcdefg", 10, 90); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
Things to Consider
You could also consider adding method wrappers in the clsFont class to
simplify drawing integer types like so:
public void drawInt(Graphics g, int num, int x, int y){ drawString(g, Integer.toString(num), x, y); }
public void drawLong(Graphics g, long num, int x, int y){ drawString(g, Long.toString(num), x, y); }
A variant of the drawString() that draws the string aligned to the right
could easily be made like the following lines:
//draws string from right to left starting at x,y public void drawStringRev(Graphics g, String sTxt, int x, int y){ // get the strings length int len = sTxt.length();
// set the starting position int cx = x; // if nothing to draw return if (len == 0) { return; } // our fail-safe if (useDefault){ g.drawString(sTxt, x, y, Graphics.TOP | Graphics.RIGHT); return; }
// loop through all the characters in the string for (int i = (len - 1); i >= 0; i--){
// get current character char c = sTxt.charAt(i);
// get ordinal value or ASCII equivalent int cIndex = (int)c;
// lookup the width of the character int w = charW[cIndex];
// go to the next drawing position cx -= (w + charS);
// draw the character drawChar(g, cIndex, cx, y, w, charH); } }
Then you can add more method wrappers like this:
public void drawIntRev(Graphics g, int num, int x, int y){ drawStringRev(g, Integer.toString(num), x, y); }
public void drawLongRev(Graphics g, long num, int x, int y){ drawStringRev(g, Long.toString(num), x, y); }
Those additional methods could be used in drawing scores and numbers that
are aligned to the right. Very useful if you want the score to grow leftwards
like in score boards.
Bitmap Fonts With Word Wrap
Here is an additional method that you can play with and use to emulate word
wrapping when using bitmap fonts:
// space between lines in pixels public int lineS = 2;
// draws words that wrap between x and x1 public void drawStringWrap(Graphics g, String s, int x, int y, int x1){ int len = s.length(); // current x int tx = x; // current y int ty = y; /* word buffer contents width - I just thought it would be faster than calling the String.length() method */ int ww = 0; // word buffer String sWord = ""; for (int i = 0; i < len; i++){ char c = s.charAt(i); int cIndex = (int)c; int cw = charW[cIndex]; if ((cIndex > 32) && (cIndex < 127)){ //if not a space and the character is printable //add the character to the buffer sWord += String.valueOf(c); //compute the length of the current word ww += cw; } else { //if space or non-printable character // check if there is a word in the buffer if (ww > 0) { //check if the word goes past the right margin if ((tx + ww) > x1){ // carrage return tx = x; // line feed ty += (charH + lineS);
} // draw the contents of the word buffer drawString(g, sWord, tx, ty); } //move to the next position tx += (ww + cw); // clear the word buffer sWord = ""; // word buffer width to zero ww = 0; } }
// if there is a word remaining in the buffer then draw it if (ww > 0) { if ((tx + ww) > x1){ tx = x; ty += (charH + lineS); } drawString(g, sWord, tx, ty); } }
The drawStringWrap() method parameters:
← Graphics g - the Graphics object used to draw the bitmapfont
← String s - the string to be drawn
← int x - the left margin and horizontal starting position
← int y - the vertical position where the first line is drawn
← int x1 - the right margin where the words will be wrapped
The drawStringWrap() method makes use of the drawString() method to
draw each word. The method considers the space and non-printable
characters as a word delimiter. You can change the line spacing by changing
the value of lineS.
To test it, add this line before the call to flushGraphics() in main loop in
clsCanvas:
myFont.drawString(g, "ABCDEFG abcdefg", 10, 90);
myFont.drawStringWrap(g, "blah blah blah blah blah blah " + "blah blah blah blah blah blah blah blah blah " + "blah blah blah blah blah blah blah blah blah " + "blah blah blah blah blah blah blah blah blah " + "blah blah blah blah blah blah blah blah blah " + "blah ", 10, 100, 166);
flushGraphics();
You should see this when you run it:
I hope the comments in the code will make the methods inner workings clear
enough for you. If not, just post your questions in the comments. Here is the
completed clsFont class with the extended methods:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;
public class clsFont { // additional space between characters public int charS = 0; // max clipping area public int screenW = 176; public int screenH = 208; // flag: set to true to use the Graphics.drawString() method // this is just used as a fail-safe public boolean useDefault = false; // height of characters public int charH = 10; // lookup table for character widths public int[] charW = { // first 32 characters 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // space 9, // everything else XD 3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 7, 3, 7, 3, 9, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 4, 6, 3, 9, 6, 6, 6,
6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 5, 3, 5, 4, // delete 9}; // the bitmap font image private Image imgFont; public clsFont() { } public boolean load(String imagePath){ useDefault = false; try{ // load the bitmap font if (imgFont != null){ imgFont = null; } imgFont = Image.createImage(imagePath); } catch (Exception ex){ // oohh we got an error then use the fail-safe useDefault = true; } return (!useDefault); } public void unload(){ // make sure the object gets destroyed imgFont = null; } public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){ // non printable characters don't need to be drawn if (cIndex < 33){ return; }
// neither does the delete character if (cIndex > 126){ return; }
// get the characters position int cx = cIndex * 9;
// reset the clipping rectangle
g.setClip(0, 0, screenW, screenH);
// resize and reposition the clipping rectangle // to where the character must be drawn g.clipRect(x, y, w, h);
// draw the character inside the clipping rectangle g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT); } public void drawString(Graphics g, String sTxt, int x, int y){ // get the strings length int len = sTxt.length();
// set the starting position int cx = x; // if nothing to draw return if (len == 0) { return; } // our fail-safe if (useDefault){ g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT); return; }
// loop through all the characters in the string for (int i = 0; i < len; i++){
// get current character char c = sTxt.charAt(i);
// get ordinal value or ASCII equivalent int cIndex = (int)c;
// lookup the width of the character int w = charW[cIndex];
// draw the character drawChar(g, cIndex, cx, y, w, charH);
// go to the next drawing position
cx += (w + charS); } } // extended methods ***************************************
public void drawInt(Graphics g, int num, int x, int y){ drawString(g, Integer.toString(num), x, y); }
public void drawLong(Graphics g, long num, int x, int y){ drawString(g, Long.toString(num), x, y); } // Right align methods **************************************** //draws string from right to left starting at x,y public void drawStringRev(Graphics g, String sTxt, int x, int y){ // get the strings length int len = sTxt.length();
// set the starting position int cx = x; // if nothing to draw return if (len == 0) { return; } // our fail-safe if (useDefault){ g.drawString(sTxt, x, y, Graphics.TOP | Graphics.RIGHT); return; }
// loop through all the characters in the string for (int i = (len - 1); i >= 0; i--){
// get current character char c = sTxt.charAt(i);
// get ordinal value or ASCII equivalent int cIndex = (int)c;
// lookup the width of the character int w = charW[cIndex];
// go to the next drawing position cx -= (w + charS);
// draw the character drawChar(g, cIndex, cx, y, w, charH);
} }
public void drawIntRev(Graphics g, int num, int x, int y){ drawString(g, Integer.toString(num), x, y); }
public void drawLongRev(Graphics g, long num, int x, int y){ drawString(g, Long.toString(num), x, y); }
// Word wrap method **************************************** // space between lines in pixels public int lineS = 2; // draws words that wrap between x and x1 public void drawStringWrap(Graphics g, String s, int x, int y, int x1){ int len = s.length(); // current x int tx = x; // current y int ty = y; /* word buffer contents width - I just thought it would be faster than calling the String.length() method */ int ww = 0; // word buffer String sWord = ""; for (int i = 0; i < len; i++){
char c = s.charAt(i); int cIndex = (int)c; int cw = charW[cIndex]; if ((cIndex > 32) && (cIndex < 127)){ //if not a space and the character is printable //add the character to the buffer sWord += String.valueOf(c); //compute the length of the current word ww += cw; } else { //if space or non-printable character // check if there is a word in the buffer if (ww > 0) { //check if it goes past the right margin if ((tx + ww) > x1){ // carrage return tx = x; // line feed ty += (charH + lineS); } // draw the contents of the word buffer drawString(g, sWord, tx, ty); } //move to the next position tx += (ww + cw); // clear the word buffer sWord = ""; // word buffer width to zero ww = 0; } } // if there is a word remaining in the buffer then draw it if (ww > 0) { if ((tx + ww) > x1){ tx = x;
ty += (charH + lineS); } drawString(g, sWord, tx, ty); } }
}
There is a lot that could be done to make the clsFont class more functional.
For instance, you can add justification or optimize the already existing
methods. You can also add another set of characters to the bitmap with a
different style and modify the code so you can select which style to use.
Since all the building blocks are in place, I think I'll just leave the missing
parts to you.
Using the Sprite Class and Sprite Movement in MIDP 2.0
This short tutorial will show you how to use the built-in Sprite class and how
to move it about on the screen. This may come a bit late but I just thought I
should throw it in for variety.
We will be using the project templates which you can get from this post:
Clean Project Templates. Download and extract them to a folder of your
choice.
I have prepared a new image strip for us to play with. It's an 18x18 pixel
white van drawn in 4 directions from frame 0 to frame 3, UP, DOWN, LEFT,
and RIGHT.
White Van Image
Frame Size:18x18 pixels
I know the drawing is not politically correct so please bear with me on this
one XD. Firefox users can right-click on the image and choose "Save Image
As" from the context menu and IE users can choose "Save Picture As"
instead. Save it inside the images folder, inside the src folder of the project
template you downloaded.
Fire up NetBeans to open your project and navigate your way to the
clsCanvas source code.
Let's add a couple of variables, one for the image and one for the sprite. Add
these lines above the clsCanvas constructor:
private Image imgVan;private Sprite Van;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) {
Now we'll load the image and initialize the Sprite object so modify the
load() method like so:
public void load(){ try{ // load the images here imgVan = Image.createImage("/images/van.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } // initialize the Sprite object Van = new Sprite(imgVan, 18, 18); // show the frame 1 - the second frame Van.setFrame(1); // move to 50, 50 (X, Y)
Van.setPosition(50, 50); }
The Sprite() constructor we're using takes in 3 parameters:
1. Image image - The Image object that contains the frames/pictures
2. int frameWidth - the width of each frame in the image
3. int frameHeight - the height of each frame in the image
You must make sure that the width and height of the picture assigned to
the Image object must be exactly divisible by the frameWidth and
frameHeight that you defined. Otherwise, you will get an exception error at
runtime. In this example, each frame of the image is 18 pixels wide and 18
pixels high.
After initializing the sprite we use the setFrame() method of the Sprite
object to set the current visible frame to 1, which is the second frame
counting from left to right and starting with 0. We also set the initial position
to 50, 50 (X, Y) on the screen.
We will also add some code to the unload() method of clsCanvas to make
sure the objects we made gets destroyed:
public void unload(){ // make sure the object gets destroyed
Van = null; imgVan = null;
}
The next line of code must be placed before the call to flushGraphics():
// draw the sprite Van.paint(g); flushGraphics();
To test the MIDlet, press F6 on your keyboard then press Enter when the
emulator shows up. You should see something like this:
For our next trick, we're going to make the van move in all 4 directions: Up,
Down, Left, Right. We'll begin by getting the current position of the sprite
and storing them in two variables. Add these lines in the run() method just
before the call to setClip():
// get the current position of the van int cx = Van.getX(); int cy = Van.getY(); //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH);
We will change the value of those variables depending on what keys are
being pressed. Add the following lines beneath the lines you just added:
// get the current position of the van int cx = Van.getX(); int cy = Van.getY(); if ((iKey & GameCanvas.UP_PRESSED) != 0){ // show the van facing up Van.setFrame(0); // move the van upwards cy--; } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){ // show the van facing down Van.setFrame(1); // move the van downwards cy++; } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){ // show the van facing left Van.setFrame(2); // move the van to the left cx--; } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){ // show the van facing right Van.setFrame(3); // move the van to the right cx++; } // update the vans position Van.setPosition(cx, cy);
//restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH);
The code above checks which key is being pressed and changes the current
frame the sprite is showing to match the direction of it's movement. It then
computes for the next location the sprite should move to and updates the
sprites position.
As a reference for beginners, here is a list of operations to do to move an
object in different directions:
← UP - decrease the value of the Y coordinate
← Down - increase the value of the Y coordinate
← Left - decrease the value of the X coordinate
← Right - increase the value of the X coordinate
If you run the project now, you should be able to control the van by pressing
the arrow/directional keys.
Here's the completed clsCanvas source code:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;import javax.microedition.lcdui.game.Sprite;
public class clsCanvas extends GameCanvas implements Runnable {// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};
//last time the key changed stateprivate long[] keyTick = {
0, 0, 0, 0, 0};
//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private Image imgVan;private Sprite Van;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); } public void load(){ try{ // load the images here imgVan = Image.createImage("/images/van.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } // initialize the Sprite object Van = new Sprite(imgVan, 18, 18); // show the frame 1 - the second frame Van.setFrame(1); // move to 50, 50 (X, Y) Van.setPosition(50, 50);
} public void unload(){ // make sure the object gets destroyed Van = null; imgVan = null; } public void checkKeys(int iKey, long currTick){ long elapsedTick = 0; //loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } }
public void run() { int iKey = 0; int screenW = getWidth(); int screenH = getHeight(); long lCurrTick = 0; // current system time in milliseconds; load(); g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates(); checkKeys(iKey, lCurrTick); if (isDown[fireKey]){ isRunning = false; }
// get the current position of the van int cx = Van.getX(); int cy = Van.getY(); if ((iKey & GameCanvas.UP_PRESSED) != 0){ // show the van facing up Van.setFrame(0); // move the van upwards cy--; } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){ // show the van facing down Van.setFrame(1); // move the van downwards cy++; } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){ // show the van facing left Van.setFrame(2); // move the van to the left cx--; } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){ // show the van facing right Van.setFrame(3); // move the van to the right cx++; } // update the vans position Van.setPosition(cx, cy); //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); //set drawing color to black g.setColor(0x000000); //fill the screen with blackness g.fillRect(0, 0, screenW, screenH); // draw the sprite Van.paint(g); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ }
} g = null; unload(); fParent.destroyApp(false); fParent = null; }}
The code presented in this tutorial also shows the basics of how to make a
user controlled sprite or character. I hope it was of some help to you
Using the TiledLayer Class to Display Tile Maps
A tile map is the map/landscape drawn in the background that the character
walks on. Tile maps are usually seen in games like Diablo, Fallout, Command
& Conquer, StarCraft, Final Fantasy, Pokemon, most 2D RPGs/MMORPGs and
strategy games. Here's an example of a game where a player controlled
character and some monsters are standing on a tile map:
Screenshot of the game Herbarrio
It's called a tile map because the map is actually made out of smaller images
called tiles. The tiles are drawn repeatedly and mix-matched with one
another to form the whole map. The collection of different tiles is also called
a tileset.
The whole point of using a tile map is so the game doesn't have to load a
huge image but instead loads a smaller image containing the tileset and
recreates the map in the game. The tilesets are usually made so you can use
them to make several different maps using the same tile images. This saves
a us lot of memory and makes the game load faster.
In this tutorial, we're going to make use of the project created in a previous
tutorial Using the Sprite Class and Sprite Movement. We're going to
place a map for the van to run on and place obstacles that blocks the vans
path. We're also going to take advantage of the TiledLayer class in MIDP
2.0 which was made specifically for drawing tile maps.
The map we'll be making will take up the whole screen of a 176x208 pixel
display. Here's an illustration to make you more confused:
The large green rectangle is the tile map and the dark green grid inside it
represents the tiles that makes up the map. Tile map dimensions are usually
measured by the number of tile columns and tile rows. In the example
above, the tile map is 11x13 tiles, columns and rows respectivly, and the
tiles themselves are 16x16 pixels. This makes a perfect fit for a 176x208
pixel display screen.
Creating and Displaying the TiledLayer
I prepared a tileset image for us to use in drawing the map. Firefox users can
right-click on the image and choose "Save Image As" from the context
menu, while IE users can choose "Save Picture As" instead. Save the image
in the images folder, inside the src folder of the project.
Tileset Image
Dimensions: 160x16 pixels
Tile Size: 16x16 pixels
Here's a larger view:
Tileset Image zoomed in at 2x
If you're done saving the image, you can now open the project in NetBeans
and navigate your way to the clsCanvas code. We'll add some variables to
hold our TiledLayer objects and the tileset image before the clsCanvas
constructor:
private Image imgTileset;private TiledLayer roadMap;private TiledLayer blockMap;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) {
You can press ALT+SHIFT+F now to add the missing export statements
automatically.
The imgTileset will be used to hold the tileset image you downloaded. The
roadMap TiledLayer will be used to display the road which the van can run
on. The blockMap TileLayer will be used to display the blocks/obstacles
and will also be used to check for collision.
Next, we'll add a loadRoadMap() method for initializing the roadMap
TiledLayer. Add the following lines beneath the start() method:
public void loadRoadMap(){ //initialize the roadMap roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);
// Create a new Random for randomizing numbers Random Rand = new Random(); //loop through all the map cells for (int y = 0; y < 13; y++){ for (int x = 0; x < 11; x++){ // get a random tile index between 2 and 5 int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;
// set the tile index for the current cell roadMap.setCell(x, y, index); } } // mark Rand for clean up Rand = null; }
Again, you can press ALT+SHIFT+F now to add the missing export
statements automatically.
The loadRoadMap() method creates a new TiledLayer object and defines
the number of columns, number of rows, the tileset image to be used, the
tile width, and the tile height. It then assigns a random tile index from 2-5 to
each cell of the map using the setCell() method.
Take note that the tile index of the tiles starts at 1 instead of 0. You can also
leave a cell empty by assigning 0 to the cell, meaning that cell will not be
drawn.
We'll do the same for the blockMap TiledLayer and add a
loadBlockMap() method. Insert the following lines beneath the
loadRoadMap() method:
public void loadBlockMap(){ // define the tile indexes to be used for each map cell byte[][] blockData = { {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 }, {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 }, {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 }, {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 }, {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 }, {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 } }; //initialize blockMap blockMap = new TiledLayer(11, 13, imgTileset, 16, 16); //loop through all the map cells for (int y = 0; y < 13; y++){ for (int x = 0; x < 11; x++){ // set the tile index for the current cell // take note of the reversed indexes for blockData blockMap.setCell(x, y, blockData[y][x]); } } blockData = null; }
The loadBlockMap() method does pretty much the same thing as what the
loadRoadMap() method does. Except this time, the tile index for each cell
is predefined in a byte array before being assigned to each map cell.
Now, we'll modify the load() method so it will load the tileset image and call
the methods we previously added for initializing the TiledLayer objects.
We'll also change the initial position of the van so that it's not already hitting
a block when the game starts. So modify the load() method like this:
public void load(){ try{ // load the images here imgVan = Image.createImage("/images/van.png"); // load the tileset imgTileset = Image.createImage("/images/tileset1.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } // initialize the Sprite object Van = new Sprite(imgVan, 18, 18); // show the frame 1 - the second frame Van.setFrame(1); // move to 16, 16 (X, Y) Van.setPosition(16, 16); //initialize the TiledLayers loadRoadMap(); loadBlockMap(); }
Let's also add some clean up code to the unload() method:
public void unload(){ // make sure the object gets destroyed blockMap = null; roadMap = null; Van = null; imgTileset = null; imgVan = null; }
We can now draw the maps on the screen. Add these lines inside the run()
method before the van is drawn:
//draw the road roadMap.paint(g); //draw the blocks blockMap.paint(g); // draw the sprite Van.paint(g); flushGraphics();
Collision Detection
If you run the project now, you will notice that the van doesn't really stop
when you hit a block. The van just runs over them. Lucky for us, the
TiledLayer and Sprite objects already have methods for collision detection.
First, let's make it so it's easier to change the speed of the van. Add the
global variable vanSpeed under the Van Sprite declaration:
private Sprite Van;
private int vanSpeed = 2;
private Image imgTileset;
Then, modify the run() method to make use of the vanSpeed variable like
so:
if ((iKey & GameCanvas.UP_PRESSED) != 0){ // show the van facing up Van.setFrame(0);
// move the van upwards cy -= vanSpeed; } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){ // show the van facing down Van.setFrame(1);
// move the van downwards cy += vanSpeed; } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){ // show the van facing left Van.setFrame(2);
// move the van to the left cx -= vanSpeed; } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){ // show the van facing right Van.setFrame(3);
// move the van to the right cx += vanSpeed; }
Now for the collision detection part. We'll first store the current position of
the van in new variables, tx and ty, so we can reset the vans position to
where it was before it collided with a block:
// get the current position of the van
int cx = Van.getX(); int cy = Van.getY(); // save the current position in temporary vars // so we can restore it when it hits a block int tx = cx; int ty = cy; if ((iKey & GameCanvas.UP_PRESSED) != 0){
The following lines of code checks if the van hits a block and resets the vans
position to where it was before it hit the block. Add the code after the vans
position has been updated:
// update the vans position Van.setPosition(cx, cy); //check if the van hits a block if (Van.collidesWith(blockMap, true)){ //reset the van to the original position Van.setPosition(tx, ty); } //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH);
The Sprite.collidesWith() method is used to check if the van collides with
any part of the blockMap TiledLayer. The second parameter we passed,
the boolean value "true", indicates that it should check for collision at the
"pixel level". This means that the collidesWith() method will only return
true if the non-transparent parts of the vans image overlaps with non-
transparent parts of the blockMap TiledLayer. You can also use the
collidesWith() method to check for collision between Sprite objects.
Btw, since the map takes up the whole screen, it would probably be better to
comment out or remove the code for filling the screen with blackness from
the run() method:
//restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); /* comment out or remove this code set drawing color to black g.setColor(0x000000); fill the screen with blackness g.fillRect(0, 0, screenW, screenH);
*/ //draw the road roadMap.paint(g);
When you run the project, you should see something like this:
Here's the completed clsCanvas code so you can check your work:
package MyGame;
import java.util.Random;import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;import javax.microedition.lcdui.game.Sprite;import javax.microedition.lcdui.game.TiledLayer;
public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};
//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};
//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private Image imgVan;private Sprite Van;
private int vanSpeed = 2;
private Image imgTileset;private TiledLayer roadMap;private TiledLayer blockMap;
public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); } public void loadRoadMap(){ //initialize the roadMap roadMap = new TiledLayer(11, 13, imgTileset, 16, 16); // Create a new Random for randomizing numbers Random Rand = new Random(); //loop through all the map cells for (int y = 0; y < 13; y++){ for (int x = 0; x < 11; x++){ // get a random tile index between 2 and 5 int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2; // set the tile index for the current cell roadMap.setCell(x, y, index); } } Rand = null; } public void loadBlockMap(){ // define the tile indexes to be used for each map cell byte[][] blockData = { {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },
{6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 }, {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 }, {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 }, {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 }, {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 } }; //initialize blockMap blockMap = new TiledLayer(11, 13, imgTileset, 16, 16); //loop through all the map cells for (int y = 0; y < 13; y++){ for (int x = 0; x < 11; x++){ // set the tile index for the current cell // take note of the reversed indexes for blockData blockMap.setCell(x, y, blockData[y][x]); } } blockData = null; } public void load(){ try{ // load the images here imgVan = Image.createImage("/images/van.png");
imgTileset = Image.createImage("/images/tileset1.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } // initialize the Sprite object Van = new Sprite(imgVan, 18, 18); // show the frame 1 - the second frame
Van.setFrame(1); // move to 16, 16 (X, Y) Van.setPosition(16, 16); loadRoadMap(); loadBlockMap(); } public void unload(){ // make sure the object gets destroyed blockMap = null; roadMap = null; Van = null; imgTileset = null; imgVan = null; } public void checkKeys(int iKey, long currTick){ long elapsedTick = 0; //loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } }
public void run() { int iKey = 0; int screenW = getWidth(); int screenH = getHeight(); long lCurrTick = 0; // current system time in milliseconds; load();
g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates(); checkKeys(iKey, lCurrTick); if (isDown[fireKey]){ isRunning = false; } // get the current position of the van int cx = Van.getX(); int cy = Van.getY(); // save the current position in temporary vars // so we can restore it when we hit a block int tx = cx; int ty = cy; if ((iKey & GameCanvas.UP_PRESSED) != 0){ // show the van facing up Van.setFrame(0); // move the van upwards cy -= vanSpeed; } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){ // show the van facing down Van.setFrame(1); // move the van downwards cy += vanSpeed; } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){ // show the van facing left Van.setFrame(2); // move the van to the left cx -= vanSpeed; } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){ // show the van facing right Van.setFrame(3); // move the van to the right cx += vanSpeed; }
// update the vans position Van.setPosition(cx, cy); //check if the van hits a block if (Van.collidesWith(blockMap, true)){ //reset the van to the original position Van.setPosition(tx, ty); } //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); /* comment out or remove this code set drawing color to black g.setColor(0x000000); fill the screen with blackness g.fillRect(0, 0, screenW, screenH);
*/
//draw the road roadMap.paint(g); //draw the blocks blockMap.paint(g); // draw the sprite Van.paint(g); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
The sample code presented here was made to show a very simple way to
use the TiledLayer class, how to draw a Tile Map and check for collision. It
also shows a way to randomize numbers by using the Random class.
Finally, you can find a lot of information about tile maps on the internet.
Although most of them aren't related to MIDP 2.0 or Java, the concept is still
the same and can be useful to your game project.
Loading Tile Map Data From a File
In case you haven't seen it yet, there's a push-puzzle demo game in the
"Various Games"/"Various Games for MIDP 2.0" sample mobile project
included with The NetBeans Mobility Pack. The push-puzzle game demo also
shows how to load map data from text files. But the one thing I remember
most about that sample game is that the code confused me a lot, heh.
Here's my attempt to make things a bit easier for you so you won't have to
go through all that code jumping. But instead of loading map data from a
text file, we're going to pull the map data from a binary file.
In this tutorial, we're going to make use of the project code from the tutorial
Using the TiledLayer Class to Display Tile Maps. So head over there
first to get the code and to have a better understanding of what's going on.
That tutorial displays a user controlled sprite on a map with obstacles. We're
going to modify the code so that the obstacle layer data is loaded from a
map file.
You can get the map file here in SMP format:
samplemap.smp (155 bytes) - Download Link
Create a new folder called "maps" in the "src" folder of the project and save
the map file there.
You can view or edit the contents of the map with the map editor which you
can get here: Simple Tile Map Editor Info. and Download Page.
The link includes instructions and some more info.
To preview the map in the map editor, Click on the "Open" icon and choose
the "samplemap.smp" file you saved earlier. In the next dialog, select the
"tileset1.png" image file inside the "images" folder within the "src" folder
of the project. You should see something like this:
When you're ready, open the project in NetBeans and navigate your way to
the clsCanvas code. Add the following code below the loadBlockMap()
method:
public TiledLayer getMap(String fpath, Image ftiles){ TiledLayer tMap = null; try { // open the file InputStream is = this.getClass().getResourceAsStream(fpath); DataInputStream ds = new DataInputStream(is); try {
// skip the descriptor ds.skipBytes(8); // read map width int mW = ds.readByte(); // read map height int mH = ds.readByte(); // read tile width int tW = ds.readByte(); // read tile height int tH = ds.readByte(); // create a new tiled layer tMap = new TiledLayer(mW, mH, ftiles, tW, tH); // loop through the map data for (int rCtr = 0; rCtr < mH; rCtr++){ for (int cCtr = 0; cCtr < mW; cCtr++){ // read a tile index byte nB = ds.readByte(); // if tile index is non-zero // tile index 0 is usually a blank tile if (nB > 0) { // assign (tile index + 1) to the current cell // TiledLayer objects start tile index at 1 // instead of 0 tMap.setCell(cCtr, rCtr, nB + 1); } } }
} catch (Exception ex) { tMap = null; System.err.println("map loading error : " + ex.getMessage()); } // close the file ds.close(); ds = null; is = null; } catch (Exception ex) {
tMap = null; System.err.println("map loading error : " + ex.getMessage()); } // return the newly created map or null if loading failed return tMap; }
Make sure to press ALT+Shift+F so NetBeans can add the missing import
statements.
The code we just added is the same getMap() method you will find the
Simple Tile Map Editor page. We will use it here to serve as an example of
it's usage.
We'll make a new method called loadBlockMapFile() that uses the
getMap() method to load the map data and initialize the blockMap
TiledLayer. This will let you preserve the previous loadBlockMap() method
code for future reference. Add the following code below the
loadBlockMap() method:
public void loadBlockMapFile(){ //initialize blockMap blockMap = getMap("/maps/samplemap.smp", imgTileset); }
We now have to replace the call to loadBlockMap() inside the load()
method to call loadBlockMapFile() instead:
loadRoadMap(); loadBlockMapFile(); }
You should see something like this when you run the project:
There's not much difference between the output of the previous tutorial and
this one except for the position of a few blocks and their colors.
Here's the completed clsCanvas source code so you can check your work:
package MyGame;
import java.io.DataInputStream;import java.io.InputStream;import java.util.Random;import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.Image;import javax.microedition.lcdui.game.GameCanvas;import javax.microedition.lcdui.game.Sprite;import javax.microedition.lcdui.game.TiledLayer;
public class clsCanvas extends GameCanvas implements Runnable {// key repeat rate in milliseconds
public static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private Image imgVan;private Sprite Van;
private int vanSpeed = 2;
private Image imgTileset;private TiledLayer roadMap;private TiledLayer blockMap;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start();
} public void loadRoadMap(){ //initialize the roadMap roadMap = new TiledLayer(11, 13, imgTileset, 16, 16); // Create a new Random for randomizing numbers Random Rand = new Random(); //loop through all the map cells for (int y = 0; y < 13; y++){ for (int x = 0; x < 11; x++){ // get a random tile index between 2 and 5 int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2; // set the tile index for the current cell roadMap.setCell(x, y, index); } } Rand = null; } public void loadBlockMap(){ // define the tile indexes to be used for each map cell byte[][] blockData = { {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 }, {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 }, {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 }, {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 }, {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 }, {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10}, {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 }, {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 }, {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 } }; //initialize blockMap blockMap = new TiledLayer(11, 13, imgTileset, 16, 16); //loop through all the map cells for (int y = 0; y < 13; y++){
for (int x = 0; x < 11; x++){ // set the tile index for the current cell // take note of the reversed indexes for blockData blockMap.setCell(x, y, blockData[y][x]); } } blockData = null; } public void loadBlockMapFile(){ //initialize blockMap from a binary file blockMap = getMap("/maps/samplemap.smp", imgTileset); }
public TiledLayer getMap(String fpath, Image ftiles){ TiledLayer tMap = null; try { // open the file InputStream is = this.getClass().getResourceAsStream(fpath); DataInputStream ds = new DataInputStream(is); try { // skip the descriptor ds.skipBytes(8); // read map width int mW = ds.readByte(); // read map height int mH = ds.readByte(); // read tile width int tW = ds.readByte(); // read tile height int tH = ds.readByte(); // create a new tiled layer tMap = new TiledLayer(mW, mH, ftiles, tW, tH); // loop through the map data for (int rCtr = 0; rCtr < mH; rCtr++){ for (int cCtr = 0; cCtr < mW; cCtr++){ // read a tile index byte nB = ds.readByte();
// if tile index is non-zero // tile index 0 is usually a blank tile if (nB > 0) { //assign (tile index + 1) to the current cell tMap.setCell(cCtr, rCtr, nB + 1); } } }
} catch (Exception ex) { tMap = null; System.err.println("map loading error : " + ex.getMessage()); } // close the file ds.close(); ds = null; is = null; } catch (Exception ex) { tMap = null; System.err.println("map loading error : " + ex.getMessage()); } // return the newly created map or null if loading failed return tMap; } public void load(){ try{ // load the images here imgVan = Image.createImage("/images/van.png"); imgTileset = Image.createImage("/images/tileset1.png"); }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } // initialize the Sprite object Van = new Sprite(imgVan, 18, 18); // show the frame 1 - the second frame
Van.setFrame(1); // move to 50, 50 (X, Y) Van.setPosition(16, 16); loadRoadMap(); loadBlockMapFile(); } public void unload(){ // make sure the object gets destroyed blockMap = null; roadMap = null; Van = null; imgTileset = null; imgVan = null; } public void checkKeys(int iKey, long currTick){ long elapsedTick = 0; //loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } }
public void run() { int iKey = 0; int screenW = getWidth(); int screenH = getHeight(); long lCurrTick = 0; // current system time in milliseconds; load();
g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates(); checkKeys(iKey, lCurrTick); if (isDown[fireKey]){ isRunning = false; } // get the current position of the van int cx = Van.getX(); int cy = Van.getY(); // save the current position in temporary vars // so we can restore it when we hit a block int tx = cx; int ty = cy; if ((iKey & GameCanvas.UP_PRESSED) != 0){ // show the van facing up Van.setFrame(0); // move the van upwards cy -= vanSpeed; } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){ // show the van facing down Van.setFrame(1); // move the van downwards cy += vanSpeed; } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){ // show the van facing left Van.setFrame(2); // move the van to the left cx -= vanSpeed; } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){ // show the van facing right Van.setFrame(3); // move the van to the right cx += vanSpeed; }
// update the vans position Van.setPosition(cx, cy); //check if the van hits a block if (Van.collidesWith(blockMap, true)){ //reset the van to the original position Van.setPosition(tx, ty); } //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); /* comment out or remove this code set drawing color to black g.setColor(0x000000); fill the screen with blackness g.fillRect(0, 0, screenW, screenH);
*/ //draw the road roadMap.paint(g); //draw the blocks blockMap.paint(g); // draw the sprite Van.paint(g); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
The way you define the map data depends entirely on you. I chose to use a
binary file because of it's size and extracting the data is pretty straight
forward. No need for custom parsers and such.
You can also find a bunch of tile map editors online with more powerful
features like saving the map data to an XML file and more.
Links to some 2d Map Editors:
← Tiled - Link
← Tile Studio - Link
← Mappy - Link
← Tat Tile Map Editor - Link
Finally, you can also have a look at the new Game Builder that comes with
the latest NetBeans 6 and Mobility bundle. Here's a preview from a tutorial
in the NetBeans Community Docs: Using Game Builder for Java ME
development.
Good luck and have fun!
Handling Text Input or Accepting Character/Player Names
Your game would be more interesting if players can enter their name. It
would make them feel like they really are part of the game. For instance, you
can make certain dialog text address the player's name so it would seem like
the game is "talking" to the player. It would also allow the player to associate
his name with his score as proof of his victory and bragging rights.
What's the fuzz all about? You can always use a TextBox, right? No, my
young padawan. Well, you could probably get away with it. But there's a
couple of reasons why you should avoid using a TextBox in a fullscreen
game. In most phones, you're game is hidden while the TextBox is displayed.
It is usually drawn using the phones U.I. theme and would not look like it's
part of the game. Once again, it's all about aesthetics.
We will be using the code from the tutorial Using Custom Fonts or Bitmap
Fonts to display the characters on the screen. So head over there if you
haven't already.
If you prefer, you can still use the normal Graphics.drawChar() method in
place of the clsFont.drawChar() method. But you at least need to have the
project code which you can download from : Clean MIDP 2.0 Game
Templates.
Lack of Buttons
In this tutorial, we're going to mimic the text input used in arcade consoles
of old. Those coin/token-eating machines only had a 4 way joystick, 6
function buttons, a start button, and a reset button. But these were sufficient
enough to let the player input names or initials for the scoreboard. While a
typical mobile phone has more buttons for us to play with, not all of them
map to the same key codes on different phones.
Fortunately, the GameCanvas class gives us a few standard keys which
more or less works on most phones.
Standard Keys:
← 8 key or UP Button
← 2 key or DOWN Button
← 4 key or LEFT Button
← 6 key or RIGHT Button
← 5 key or FIRE Button
We're going to assign functions to these keys as follows:
← LEFT - select the character to the left of the current selected character
in the string.
← RIGHT - select the character to the right of the current selected
character in the string.
← UP - Scroll through the characters upwards.
← DOWN - Scroll through the characters downwards.
← FIRE - commit changes
The Variables
Let's begin by opening the project from NetBeans and navigate your way to
the clsCanvas code. Add the following global variables before the
clsCanvas constructor:
private clsFont myFont;
// this will hold the resulting textprivate String prText = "AAAAAA";
// the currently selected characterprivate int prSelected = 0;
// width and height of the charactersprivate int prWidth = 9;private int prHeight = 10;
// character spacing - includes width of characterprivate int prSpacing = 12;
// vars for timing the blinking cursorprivate long prStart = 0;private long prDelay = 100;private boolean prShow = true;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) {
The String prText will contain the text entered by the player and at the
same time provide feed back of the letters the player has chosen. The length
of the string assign to prText will also determine the maximum length of the
text the player can enter and will also be the first characters displayed on
the screen.
The integer prSelected will hold the index of the currently selected
character from the string assigned to prText.
The variables prWidth and prHeight holds the width and height of a
character. These variables will be passed to the clsFont.drawChar()
method for resizing the clipping rectangle.
The integer prSpacing defines the interval or space in pixels between each
characters drawing position (X coordinate). The value assigned to
prSpacing should be the sum of the width of a character and the space
between each character.
Replacing a Single Character
Next, we'll add a new method called replaceCharAt() that will let us change
a single character in a given string. Add the new method above the run()
method:
public String replaceCharAt(String s, int pos, char c) { if (pos < (s.length() - 1)){ return s.substring(0, pos) + c + s.substring(pos + 1); } else { return s.substring(0, pos) + c; } }
public void run() {
The replaceCharAt() method requires 3 parameters:
← String s - the original string
← int pos - the position of the character to be replaced
← Char c - the replacement character
User Controls
We also need a new method for detecting/responding to key presses and
toggle the cursors visibility when needed. Add the updatePompt() method
above the run() method:
public void updatePrompt(long currTick){ // get text length int len = prText.length(); // get current selected characters ASCII value int prOrd = (int)prText.charAt(prSelected); // is it time to change the cursors visibility if ((currTick - prStart) >= prDelay){ // update starting time prStart = currTick; // toggle cursor visibility prShow = !prShow; } if (isDown[leftKey]){ // if not first character if (prSelected > 0){ // select previous character in string prSelected--; } } else if (isDown[rightKey]){ // if not last character if (prSelected < (len - 1)){ // select next character in string prSelected++; } } else if (isDown[upKey]){
if (prOrd == 97){ // small leter a prOrd = 90; // jump to capital letter Z } else if (prOrd == 65){ // capital letter A prOrd = 32; // jump to space character } else if (prOrd == 32){ // space character prOrd = 122; // jump to small leter z } else if ((prOrd > 97) || (prOrd > 65)){ prOrd--; // previous character } // replace the selected character with the new character prText = replaceCharAt(prText, prSelected, (char)prOrd); } else if (isDown[downKey]){ if (prOrd == 32){ // space character prOrd = 65; // jump to capilat letter A } else if (prOrd == 90){ // capital letter Z prOrd = 97; // jump to small letter a } else if (prOrd == 122){ // small letter z prOrd = 32; // jump to space character } else if ((prOrd < 90) || (prOrd < 122)){ prOrd++; // next character } // replace the selected character with the new character prText = replaceCharAt(prText, prSelected, (char)prOrd); } }
public void run() {
The updatePrompt() method takes a single Long parameter currTick
which is the current time in milliseconds. The value from the parameter is
just used to check if the time that passed between prStart and the currTick
is greater than or equal to the delay defined in prDelay. If so, the boolean
prShow which controls if the cursor is show or hidden is toggled between
true and false.
Notice that the updatePrompt() method allows you to select capital letters,
small letters, and the space character. It wouldn't be too hard to add
numbers there too or even the entire displayable character set. But you
would usually only use capital letters.
You might want to keep the space character though, because the player can
use it as placeholders for unused spaces if the length of your prompt is too
long. This way you can trim the spaces from the value of prText when the
player is finished entering his name.
Displaying the Text
Finally, we'll add a method for drawing the text and the cursor. Again, add
the drawPrompt() method above the run() method:
public void drawPrompt(Graphics g, int x, int y){ // get length of the string int len = prText.length(); // loop through the characters for (int i = 0; i < len; i++){ // get ASCII value int cIndex = (int)prText.charAt(i); // compute next drawing position - X int cx = x + (i * prSpacing); // draw the character myFont.drawChar(g, cIndex, cx, y, prWidth, prHeight); // compute cusor position - Y int cy = y + 6;
// if current char is selected if (i == prSelected){ // if cursor shoud be visible if (prShow) { //draw the cursor using char 95 or underscore character myFont.drawChar(g, 95, cx, cy, prWidth, prHeight); } } else { //draw the cursor using char 95 or underscore
character myFont.drawChar(g, 95, cx, cy, prWidth, prHeight); } } }
public void run() {
The drawPrompt() method draws each character in the prText string and
cursor underneath each character. The cursor under the selected character
blinks to let the player know which character he is currently editing. The
markers/cursors under the non-selected characters will show the user how
many characters he can enter.
The drawPrompt() method takes 3 parameters:
← Graphics g - the Graphics object used for drawing. It will be passed
to the clsFont.drawChar() method
← int x - the horizontal starting drawing position
← int y - the vertical drawing position
Putting It All Together
Let's modify the code in our run() method to make use of the new methods
we just added. First, add the call to the updatePrompt() method below the
call to checkKeys() like so:
checkKeys(iKey, lCurrTick); updatePrompt(lCurrTick); if (isDown[fireKey]){
Then, add the following lines above the call to flushGraphics() like so:
g.fillRect(0, 0, screenW, screenH); myFont.drawString(g, "Enter your name:", 10, 20);
drawPrompt(g, 10, 40);
myFont.drawString(g, "LEFT/RIGHT - move cursor", 10, 80); myFont.drawString(g, "UP/DOWN - change letter", 10, 100);
myFont.drawString(g, "Hello " + prText.trim() + "!", 10, 140); flushGraphics();
You can now run the project and should see something like this:
Here's the completed clsCanvas source code so you can check your work:
package MyGame;
import javax.microedition.lcdui.Graphics;import javax.microedition.lcdui.game.GameCanvas;
public class clsCanvas extends GameCanvas implements Runnable {// key repeat rate in millisecondspublic static final int keyDelay = 250;
//key constantspublic static final int upKey = 0;public static final int leftKey = 1;public static final int downKey = 2;public static final int rightKey = 3;public static final int fireKey = 4;
//key states for up, left, down, right, and fire keyprivate boolean[] isDown = { false, false, false, false, false};//last time the key changed stateprivate long[] keyTick = { 0, 0, 0, 0, 0};//lookup table for key constants :Pprivate int[] keyValue = { GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED, GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, GameCanvas.FIRE_PRESSED};
private boolean isRunning = true; private Graphics g;private midMain fParent;
private clsFont myFont;
// this will hold the resulting textprivate String prText = "AAAAAA";
// the currently selected character
private int prSelected = 0;
// width and height of the charactersprivate int prWidth = 9;private int prHeight = 10;
// character spacing - includes width of characterprivate int prSpacing = 12;
// vars for timing the blinking cursorprivate long prStart = 0;private long prDelay = 100;private boolean prShow = true;
/** Creates a new instance of clsCanvas */ public clsCanvas(midMain m) { super(true); fParent = m; setFullScreenMode(true); } public void start(){ Thread runner = new Thread(this); runner.start(); } public void load(){ try{ // load the images here }catch(Exception ex){ // exit the app if it fails to load the image isRunning = false; return; } myFont = new clsFont(); myFont.load("/images/fonts.png"); } public void unload(){ // make sure the object gets destroyed myFont.unload(); myFont = null; } public void checkKeys(int iKey, long currTick){ long elapsedTick = 0;
//loop through the keys for (int i = 0; i < 5; i++){ // by default, key not pressed by user isDown[i] = false; // is user pressing the key if ((iKey & keyValue[i]) != 0){ elapsedTick = currTick - keyTick[i]; //is it time to toggle key state? if (elapsedTick >= keyDelay){ // save the current time keyTick[i] = currTick; // toggle the state to down or pressed isDown[i] = true; } } } } public String replaceCharAt(String s, int pos, char c) { if (pos < (s.length() - 1)){ return s.substring(0, pos) + c + s.substring(pos + 1); } else { return s.substring(0, pos) + c; } } public void updatePrompt(long currTick){ // get text length int len = prText.length(); // get current selected characters ASCII value int prOrd = (int)prText.charAt(prSelected); // is it time to change to cursors visibility if ((currTick - prStart) >= prDelay){ // update starting time prStart = currTick; // toggle cursor visibility prShow = !prShow; } if (isDown[leftKey]){ // if not first character if (prSelected > 0){ // select previous character in string prSelected--; } } else if (isDown[rightKey]){
// if not last character if (prSelected < (len - 1)){ // select next character in string prSelected++; } } else if (isDown[upKey]){ if (prOrd == 97){ // small leter a prOrd = 90; // jump to capital letter Z } else if (prOrd == 65){ // capital letter A prOrd = 32; // jump to space character } else if (prOrd == 32){ // space character prOrd = 122; // jump to small leter z } else if ((prOrd > 97) || (prOrd > 65)){ prOrd--; // previous character } // replace the selected character with the new character prText = replaceCharAt(prText, prSelected, (char)prOrd); } else if (isDown[downKey]){ if (prOrd == 32){ // space character prOrd = 65; // jump to capilat letter A } else if (prOrd == 90){ // capital letter Z prOrd = 97; // jump to small letter a } else if (prOrd == 122){ // small letter z prOrd = 32; // jump to space character } else if ((prOrd < 90) || (prOrd < 122)){ prOrd++; // next character } // replace the selected character with the new character prText = replaceCharAt(prText, prSelected, (char)prOrd); } } public void drawPrompt(Graphics g, int x, int y){ // get length of the string int len = prText.length(); // loop through the characters for (int i = 0; i < len; i++){ // get ASCII value int cIndex = (int)prText.charAt(i); // compute next drawing position - X int cx = x + (i * prSpacing);
// draw the character myFont.drawChar(g, cIndex, cx, y, prWidth, prHeight); // compute cusor position - Y int cy = y + 6;
// if current char is selected if (i == prSelected){ // if cursor shoud be visible if (prShow) { //draw the cursor using char 95 or underscore character myFont.drawChar(g, 95, cx, cy, prWidth, prHeight); } } else { //draw the cursor using char 95 or underscore character myFont.drawChar(g, 95, cx, cy, prWidth, prHeight); } } }
public void run() { int iKey = 0; int screenW = getWidth(); int screenH = getHeight(); long lCurrTick = 0; // current system time in milliseconds; String sName = ""; load(); g = getGraphics(); while(isRunning){ lCurrTick = System.currentTimeMillis(); iKey = getKeyStates();
checkKeys(iKey, lCurrTick);
updatePrompt(lCurrTick); if (isDown[fireKey]){ isRunning = false;
} //restore the clipping rectangle to full screen g.setClip(0, 0, screenW, screenH); //set drawing color to black g.setColor(0x000000); //fill the screen with blackness g.fillRect(0, 0, screenW, screenH); myFont.drawString(g, "Enter your name:", 10, 20);
drawPrompt(g, 10, 40);
myFont.drawString(g, "LEFT/RIGHT - move cursor", 10, 80); myFont.drawString(g, "UP/DOWN - change letter", 10, 100);
myFont.drawString(g, "Hello " + prText.trim() + "!", 10, 140); flushGraphics(); try{ Thread.sleep(30); } catch (Exception ex){ } } g = null; unload(); fParent.destroyApp(false); fParent = null; }}
The code presented in this tutorial show a very simple way for your game to
accept text input from the user. Using only directional keys/buttons, the
player is able to enter his name which is useful for game customization and
recording scores. One thing to remember is that you can adjust the
maximum length of text the user can enter simply by assigning a string of
the required length to the prText variable. You can even use a bunch of
spaces to initialize it.
If you have ever played games on other hand-held devices with a minimal
set of controls like the GameBoy® or even console systems like the
PlayStation®, you have probably run across other techniques to get text
input from the player. Those techniques have been tried and tested so it
wouldn't hurt to adapt them for you own use. Just remember to keep it
simple
Top Related