If you don't like C and want to stay as far away from it as possible, the next two chapters will show you how simple and fun writing a Java-only game can be. We start by taking a look at the differences between Android and the Java Micro Edition (Java ME), which is the standard for mobile development. Next, we take a look at some basic gaming techniques such as sprite rendering and sound manipulation, as an appetizer to the main course—building a real game called Space Blaster. In this fun game, you must shoot or duck incoming asteroids that threaten to destroy your space ship. A shield will also help you in your quest for a high score. Let's get started.
If you have written many Android applications, perhaps you have noticed how different the Android Java API is from the Java SE and Java ME APIs, the standards for Java and mobile development respectively. As a matter of fact, according to Wikipedia, this is a major source of criticism about Android (http://en.wikipedia.org/wiki/Android_(operating_system)
).
Android does not use a conventional Linux kernel. It does not have a native X Window system, nor does it support the standard GNU libraries, making reusing existing Linux applications or libraries difficult.
Android does not use established Java standards (Java SE and Java ME). This prevents compatibility among Java applications. It does not provide the full set of Java SE or ME classes, libraries, and APIs.
Android does not officially allow applications to be installed on or run from an SD card. This is a serious mistake as many advanced 3D games use big files to store graphics and sound. Storing these files in the main file system will quickly drain precious disk space.
These shortcomings are evident just by looking at the Android library within your IDE. Consider Figure 3-1, which displays the classes for the android.jar
library. On the right side, you will notice that most of the packages of Java SE are included, which is good. However, some useful classes for games, such as java.awt.Polygon
and java.awt.Dimension
, are still missing. Although in version 1.5 of the SDK Google has added the important Java Bean property change support (see the java.beans
package). The middle of the figure shows where Android and Java SE are two different animals—none of the classes in the Android package are part of the Java SE and ME standards. Finally, in the left view, you can see that Google is reusing some neat libraries from the Apache Software Foundation (Commons HTTP and HTTP client) and the W3C XML APIs (under org.xml
).
The Android API breaks compatibility with Java SE/ME and makes it tough, but not impossible, to reuse Java code from standard Java games or applets. You'll find out how to reuse your Java code throughout this chapter.
It's time for the real thing. This section presents the pure Java game Space Blaster. Even though this is an Android game, some code has been reused from a Java applet.
Figure 3-2 shows the game in action. The objective of the game is to navigate a space ship through a field of meteors, shooting them up as you go. The ship has a laser weapon and a defensive shield. You can choose to shoot the meteor, dodge it by dragging the ship with your fingertips, or let the shield protect you, but whatever you choose, the power diminishes. As time passes, the power will replenish itself, and you will get points the longer you stay in the game. You can maneuver the ship with the keypad arrows or drag it across the field with your fingers. Tapping the background will fire the laser, pressing E will end the game, and pressing Q will terminate the application.
Let's take a look at the class hierarchy for this application.
Space Blaster is a relatively simple game. It has three main classes:
SpaceBlaster.java
: This is the main activity that bonds the game code with the Android platform. Its job is to load the game layout and process Activity
events.
SpaceBlasterGame.java
: This is where all the meat resides. This class has all the game logic, and it processes key and touch events. SpaceBlasterGame
extends ArcadeGame
.
ArcadeGame.java
: This abstract class encapsulates common functionality. It also uses a standard Java TimerTask
to define a game loop, which will repeat itself infinitely until the user quits the application.
These classes interact with each other and local resources as shown in Figure 3-3.
The main activity class loads the XML UI from the file main.xml
. The XML UI defines a basic Android LinearLayout
that draws all the sprites on the game. The position of a sprite is controlled using X and Y coordinates within the LinearLayout::onDraw()
method. Note that SpaceBlasterGame
extends ArcadeGame
, which in turn extends LinearLayout
. This inheritance hierarchy allows you to define your own class to use as a LinearLayout
, where you can draw by simply overloading the onDraw()
method (see Listing 3-1).
Example 3-1. User-Defined LinearLayout (main.xml)
<?xml version="1.0" encoding="utf-8"?> <!— User Defined Linear Layout —> <ch03.game.sb.SpaceBlasterGame xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ll_absolute" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FF000000"> </ch03.game.sb.SpaceBlasterGame>
The XML in Listing 3-1 creates a LinearLayout
with the ID ll_absolute
whose width and height will fill the screen (fill_parent
). It has a vertical orientation, so child controls will be aligned vertically. Other utility classes in the project follow:
AudioClip.java
: This sound handler class deals with the device's media player.
Tools.java
: This simple class has utility methods to display messages to the user.
To get started, you will need the chapter source that comes along with this book, which contains all the resources for the next sections.
Create a project to host the game. Here is how:
Click New Android Project from the toolbar, and enter the following information (see Figure 3-4):
Name: ch03.SpaceBlaster
Build Target: Android 1.1 or Android 1.5
Application Name: Space Blaster
Package Name: ch03.game.sb
Activity: SpaceBlaster
Min SDK version: 3 for 1.5, 2 for 1.1
Click Finish.
Listing 3-2 shows the game activity class, ch03.game.sb.SpaceBlaster
. This is the core class that will be executed when the game starts. When an instance of the game is created, the onCreate()
method will be called once. This method loads the user-defined layout described in Listing 3-1.
Example 3-2. Space Blaster's Main Activity Class
package ch03.game.sb; import android.app.Activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; public class SpaceBlaster extends Activity { private View view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LayoutInflater factory = LayoutInflater.from(this); // Set game layout view = factory.inflate(R.layout.main, null); setContentView(view); // Enable view key events view.setFocusable(true); view.setFocusableInTouchMode(true); } @Override protected void onStop() { super.onStop(); ((ArcadeGame) view).halt(); } @Override protected void onPause() { super.onPause(); onStop(); } @Override protected void onRestart() {
super.onRestart(); ((ArcadeGame) view).resume(); } }
The class keeps a reference to the layout (using the view
class variable). This variable is used to halt the game when the user exists or restart it, as shown in the onStop
and onRestart
methods. To manually load the user-defined layout, we use the LayoutInfalter
class:
LayoutInflater factory = LayoutInflater.from(this); // Set game layout view = factory.inflate(R.layout.main, null); setContentView(view);
Two important lines are used to cascade key and touch events to the inner layout:
view.setFocusable(true); view.setFocusableInTouchMode(true);
setFocusable
tells the view (the user-defined layout) that it can receive focus and thus events in key or touch modes. These two lines are critical; if they are commented out, the game will not receive events.
As mentioned before, the class SpaceBlasterGame
extends ArcadeGame
, which in turn extends the Android layout LinearLayout
. In this way, we can define a game thread to update the state of the game and simply draw sprites in the onDraw
method of LinearLayout
, thus gaining a finer control over the drawing process. SpaceBlasterGame
performs the drawing, and it has all the game logic. This class is relatively complex compared to the others, but before we take a look at it, let's see how the game thread is handled by ArcadeGame
.
ArcadeGame
is the abstract base class for SpaceBlasterGame
, and it deals with the game loop. This loop is infinite and invalidates the view to force a redraw of the display (see Listing 3-3).
Example 3-3. Abstract Class ArcadeGame
package ch03.game.sb; import java.util.Timer; import java.util.TimerTask; import ch03.common.AudioClip; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.AttributeSet; import android.widget.LinearLayout; /**
* Base class for all games. Extends {@link LinearLayout} and uses a * {@link TimerTask} to invalidate the view * * @author V. Silva * */ public abstract class ArcadeGame extends LinearLayout { // App context private Context mContex; // Update timer used to invalidate the view private Timer mUpdateTimer; // Timer period private long mPeriod = 1000; /** * C * * @param context */ public ArcadeGame(Context context) { super(context); mContex = context; } public ArcadeGame(Context context, AttributeSet attrs) { super(context, attrs); mContex = context; } /** * Fires on layout */ protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); try { // Init game initialize(); /** * start update task. Which will fire onDraw in the future */ startUpdateTimer(); } catch (Exception e) { // bug e.printStackTrace(); } } /**
* Set the update period * * @param period */ public void setUpdatePeriod(long period) { mPeriod = period; } /** * A timer is used to move the sprite around */ protected void startUpdateTimer() { mUpdateTimer = new Timer(); mUpdateTimer.schedule(new UpdateTask(), 0, mPeriod); } protected void stopUpdateTimer() { if (mUpdateTimer != null) { mUpdateTimer.cancel(); } } public Context getContex() { return mContex; } /** * Load an image * * @param id * @return */ protected Bitmap getImage(int id) { return BitmapFactory.decodeResource(mContex.getResources(), id); } /** * Get AudioClip * * @param id * @return */ protected AudioClip getAudioClip(int id) { return new AudioClip(mContex, id); } /** * Overload this to update the sprites on the game */ abstract protected void updatePhysics();
/** * Overload to initialize the game */ abstract protected void initialize(); abstract protected boolean gameOver(); abstract protected long getScore(); /** * Canvas update task * * @author vsilva * */ private class UpdateTask extends TimerTask { @Override public void run() { updatePhysics(); /** * Cause an invalidate to happen on a subsequent cycle * through the event loop. Use this to invalidate the View * from a non-UI thread. onDraw will be called sometime * in the future. */ postInvalidate(); } } /** * Halt game. Stops the update task. Called by a parent activity to halt * */ public void halt() { stopUpdateTimer(); } /** * Resume Game */ public void resume() { initialize(); startUpdateTimer(); } }
Any user-defined layout must define constructors with an Android Context
and set of attributes for being able to be processed properly by the system:
public ArcadeGame(Context context) { super(context); mContex = context; } public ArcadeGame(Context context, AttributeSet attrs) { super(context, attrs); mContex = context; }
Note that ArcadeGame
extends LinearLayout
and sends its arguments to the parent via super(context)
and super(context, attrs)
. This class also overloads the parent's method onLayout()
, which fires when the view should assign a size and position to each of its children:
/** * Fires on layout */ protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); try { // Init game initialize(); /** * start update task. Which will fire onDraw in the future */ startUpdateTimer(); } catch (Exception e) { // bug e.printStackTrace(); } }
onLayout
is in charge of initializing the game and starting the update timer. Note that initialize()
is an abstract method that must be overloaded by the child class (SpaceBlasterGame
) to perform initialization. The update timer, on the other hand, will be defined within AbstractGame
. To define a game loop, we can use a simple TimerTask
that will update itself using a period, as shown in Listing 3-4.
Example 3-4. Defining a Game Loop Using a Timer Task
// Timer period private long mPeriod = 1000; /** * Canvas update task * * @author vsilva * */ private class UpdateTask extends TimerTask { @Override
public void run() { updatePhysics(); /** * Cause an invalidate to happen on a subsequent cycle * through the event loop. Use this to invalidate the View * from a non-UI thread. onDraw will be called sometime * in the future. */ postInvalidate(); } } /** * A timer is used to move the sprite around */ protected void startUpdateTimer() { mUpdateTimer = new Timer(); mUpdateTimer.schedule(new UpdateTask(), 0, mPeriod); } protected void stopUpdateTimer() { if (mUpdateTimer != null) { mUpdateTimer.cancel(); } } /** * Set the update period * * @param period */ public void setUpdatePeriod(long period) { mPeriod = period; }
When the layout initializes, the onLayout()
method will fire and call initialize()
and startUpdateTimer()
. These methods will start the Timertask:run()
method, which updates the physics of the child class and invalidates the view by calling postInvalidate()
. Invalidating the view will, in turn, tell the system UI thread to refresh the display. This cycle will repeat itself until the value of the update period (mPeriod
) is reached, and this value can be set by the main class (SpaceBlaster
) using the setUpdatePeriod
method. In this way, we have created a simple refresh loop for the game.
The previous sections showed you the foundation for the main class, SpaceBlasterGame
. Here is where all the meat resides. Let's take a closer look at the most important sections of SpaceBlaster.java
. Note that the class has been stripped for simplicity in Listing 3-5, but the chapter source contains the full implementation.
Example 3-5. The Main Game Class
package ch03.game.sb; import ch03.common.AudioClip; import ch03.common.Tools; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; public class SpaceBlasterGame extends ArcadeGame { // Game name public static final String NAME = "SpaceBlaster"; // Refresh rate (ms) private static final long UPDATE_DELAY = 40; private Context mContext; // For text private Paint mTextPaint = new Paint(); // For Bitmaps private Paint mBitmapPaint = new Paint(); private Paint mLaserBarPaint = new Paint(); private Paint mShieldBarPaint = new Paint(); private Paint mShieldPaint = new Paint(); /** * Constructor * * @param context */ public SpaceBlasterGame(Context context) { super(context); mContext = context; super.setUpdatePeriod(UPDATE_DELAY); } public SpaceBlasterGame(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; super.setUpdatePeriod(UPDATE_DELAY); }
// Code removed for simplicity // See chapter source // ... }
The class constructor SpaceBlasterGame
starts by initializing the Android context by calling super(context)
and setting the game update delay to 40 milliseconds. Note that the context is critical for all the UI thread operations. A series of Paint
objects is also defined to hold the style and color information about how to draw geometries, text, and bitmaps:
mTextPaint
holds style and color information for the text. It will be initialized as white.
mBitmapPaint
holds style and color information for all bitmaps.
mLaserBarPaint
holds style and color information for the laser bar at the bottom of the screen.
mShieldBarPaint
holds style and color information for the shield bar at the bottom of the screen.
mShieldPaint
holds style and color information for the ship's shield. It will be used to draw an oval filled with a semitransparent light blue color around the ship.
Game initialization occurs in the initialize
method overloaded from the parent abstract class ArcadeGame
(see Listing 3-6). The call sequence goes as follows: LinearLayout.onLayout
calls ArcadeGame.initilize
, which calls SpaceBlasterGame.initilize
. This last method performs the following tasks:
Set the screen size.
Set the style and color attributes for the Paint objects: text, laser bar, and shield bar.
Load the game bitmap sprites: ship, meteor, laser bullet, and explosion sequence.
Load the audio clips.
Example 3-6. Game Initialization
public void initialize() { int n; // Screen size int width = getWidth(); int height = getHeight(); // Text Paints mTextPaint.setARGB(255, 255, 255, 255); mShieldPaint.setARGB(125, 0, 255, 255);
// Laser Bar Energy mLaserBarPaint.setARGB(255, 0, 255, 96); mLaserBarPaint.setStyle(Paint.Style.FILL); // Shield Bar Energy mShieldBarPaint.setARGB(255, 0, 255, 255); mShieldBarPaint.setStyle(Paint.Style.FILL); ship = getImage(R.drawable.sb_ship); bullet = getImage(R.drawable.sb_bullet); fire = new Bitmap[fireframe]; // Load fire image sprites int[] ids = new int[] { R.drawable.sb_fire0, R.drawable.sb_fire1 }; for (n = 0; n < fireframe; n++) { fire[n] = getImage(ids[n]); } // Load meteor explosion sequence sprites ids = new int[] { R.drawable.sb_boom0, R.drawable.sb_boom1, R.drawable.sb_boom2, R.drawable.sb_boom3, R.drawable.sb_boom4 }; boom = new Bitmap[bframes + 1]; for (n = 0; n <= bframes; n++) { boom[n] = getImage(ids[n]); } // ... // Meteor initialize meteor = getImage(R.drawable.sb_meteor); // ... // Load Audio clips try { blast = getAudioClip(R.raw.sb_blast); crash = getAudioClip(R.raw.sb_collisn); kill = getAudioClip(R.raw.sb_mdestr); } catch (Exception e) { Tools.MessageBox(mContext, "Audio Error: " + e.toString()); } initStars(); // ... }
Bitmap sprites are loaded by SpaceBlasterGame.initialize
from the drawables
section of the project using the getImage(RESOURCEID)
method. getImage
is defined in the base class ArcadeGame
, so it'll be easy for child classes to reuse. For example, the game's explosion is simply an array of bitmaps loaded from the drawables
folder (shown in Figure 3-5). To create an explosion, the game manipulates the explosion frame number when the display is drawn. The tricky part is keeping track of many explosions and their X and Y coordinates. Consider the next fragment, which loads bitmaps for the ship, laser bullet and explosion frames:
// Ship ship = getImage(R.drawable.sb_ship); // Laser Bullet bullet = getImage(R.drawable.sb_bullet); // Load meteor explosion sequence sprites ids = new int[] { R.drawable.sb_boom0, R.drawable.sb_boom1, R.drawable.sb_boom2, R.drawable.sb_boom3, R.drawable.sb_boom4 }; boom = new Bitmap[bframes + 1]; for (n = 0; n <= bframes; n++) { boom[n] = getImage(ids[n]); }
Another neat trick is to create a random star field in the background (see Figure 3-6). We can do this by using arrays of X and Y coordinates for each star. But we must also set style and colors using Paint
objects. Listing 3-7 demonstrates how to create the random star field and styles for SpaceBlaster
.
Example 3-7. A Random Star Field
/** * create the star field in the background */ public void initStars() { starsX = new int[numStars]; starsY = new int[numStars]; starsC = new Paint[numStars]; for (int i = 0; i < numStars; i++) { starsX[i] = (int) ((Math.random() * xSize − 1) + 1); starsY[i] = (int) ((Math.random() * ySize − 1) + 1); starsC[i] = newColor(); } } public Paint newColor() {
int r = Math.random() * 255; int g = Math.random() * 255; int b = Math.random() * 255; Paint p = new Paint(); p.setARGB(255, r, g, b); return p; }
A good sound implementation is critical for any successful game. Android gives you two choices when dealing with game sound:
Implement the sound logic in Java using Android's MediaPlayer
API.
Implement the sound natively (in C or C++). This could be an option if you have native code and wish to reuse it. This can be though as, by the time of this writing, Google provides no support for native development.
Java sound must be implemented using the MediaPlayer
API. This is a powerful media tool, but it has some drawbacks when dealing with game sound:
The MediaPlayer
can only open raw resources (sound files within your project) or files located in the file system. This is a serious caveat if the game packs sprites and sounds in a single file. 3D games like Doom and Wolf 3D use a single file for sprites and sounds, and they use a mixer device to combine streams of bytes simultaneously. This solution is not possible with the MediaPlayer
.
The MediaPlayer
API has some annoying idiosyncrasies when developing. For example, playing a resource sound is easy:
MediaPlayer mPlayer = MediaPlayer.create(Context, RESOURCEID); mPlayer.start();
However if you attempt to pause a sound before it plays or issue two simultaneous play commands, the MediaPlayer throws an IllegalState exception. The same thing happens if you try to replay a sound before preparing or making sure the player has been paused. These limitations make dealing with the player annoying and difficult for a game where multiple sounds must be replayed continuously.
The MediaPlayer
consumes significant resources and can slow things down quite a bit, especially if you have lots of sounds.
Listing 3-8 shows the AudioClip
sound class. It is designed to mirror the Java SE class of the same name by wrapping an instance of the MediaPlayer
and dealing with the media events behind the scenes.
Example 3-8. An Audio Helper Class
package ch03.common; import android.content.Context; import android.media.MediaPlayer; public class AudioClip { private MediaPlayer mPlayer; private String name; private boolean mPlaying = false; private boolean mLoop = false; public AudioClip(Context ctx, int resID) { // clip name name = ctx.getResources().getResourceName(resID); // Create a media player mPlayer = MediaPlayer.create(ctx, resID); // Listen for completion events mPlayer.setOnCompletionListener( new MediaPlayer.OnCompletionListener() {
@Override public void onCompletion(MediaPlayer mp) { mPlaying = false; if (mLoop) { mp.start(); } } }); } public synchronized void play() { if (mPlaying) return; if (mPlayer != null) { mPlaying = true; mPlayer.start(); } } public synchronized void stop() { try { mLoop = false; if (mPlaying) { mPlaying = false; mPlayer.pause(); } } catch (Exception e) { System.err.println("AduioClip::stop " + name + " " + e.toString()); } } public synchronized void loop() { mLoop = true; mPlaying = true; mPlayer.start(); } public void release() { if (mPlayer != null) { mPlayer.release(); mPlayer = null; } } }
AudioClip
wraps and instance of MediaPlayer
and is aware of the state of the sound: paying, paused, or looping. This awareness is necessary for dealing with IllegalState
exceptions explained previously. AudioClip
also listens for completion events via setOnCompletionListener(OnCompletionListener)
, and if the loop flag is set, it will replay the sound. Space Blaster has three sounds that can be loaded with the code:
blast = getAudioClip(R.raw.sb_blast); // Laser shot crash = getAudioClip(R.raw.sb_collisn); // Ship collision kill = getAudioClip(R.raw.sb_mdestr); // meteor destruction
getAudioClip
takes a sound resource ID as argument, and it is defined in the abstract class ArcadeGame
as follows:
protected AudioClip getAudioClip(int id) { return new AudioClip(mContex, id); }
Audio resources must be saved in the res/raw
folder within the project.
Native games like Doom and Wolf 3D, shown in later chapters, pack sound resources in their own formats and manipulate the sound device at the operating system level. Even though Android runs a Linux kernel behind the scenes, it is not a standard Linux distribution, especially with dealing with low-level devices, for example:
The standard Linux sound device (/dev/dsp
) and mixer (/dev/mixer
) are not supported, making reusing sound logic very difficult for native games. For some reason, Google decided to use the Sonivox Enhanced Audio System (EAS) via the /dev/eac
device. This caveat is compounded by the fact that Google provides no support for native development. Nevertheless, a solution is to cascade the sound name back to Java via JNI and play it using AudioClip
. This technique is explained in detail in Chapters 6 and 7.
If you have native code that uses OpenAL/OpenGL for accelerated sound and graphics, you are in trouble. Google wants all development done in its Java-based version of OpenGL and doesn't support OpenAL. This leaves you with the painful task of translating OpenGL logic line by line from C to Java. Nevertheless, all is not lost. I found a neat trick to leave your C-based OpenGL code intact and still be able to run it from Java, and I explain my trick in 21.330 Chapter 5.
Key and touch events can be handled by overloading the following methods (as shown in Listing 3-9):
onKeyDown
: This event fires when a key is pressed. The event receives two arguments: an integer representing the key code and a KeyEvent
. Note that the key code is an integer value defined by Android not related to the standard ASCII keys. To test for a specific key, simply compare the value with the static key constants in KeyEvent
.
onKeyUp
: This event fires when a key is released, and it receives the same arguments as onKeUp
. Furthermore, you can also check the status of the Shift or Alt keys using event.isShiftPressed()
or event.isAltPressed()
respectively.
onTouchEvent
: This event fires when you tap the screen, and it receives a MotionEvent
. Tap events have two states: DOWN
and UP
just like key events. You can check the tap state by comparing event.getAction()
with MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_UP
. You can also get the X and Y coordinates relative to the upper-left corner of the screen.
Example 3-9. Handling Touch and Key Events
/** * Android Key events */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { keyUp(keyCode); return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { keyDown(keyCode); return true; } public boolean onTouchEvent(MotionEvent event) { int tx = (int)event.getX(); int ty = (int)event.getY(); // Has the ship been touched. if so move it if ( tx >= x && tx <= x + ship.getWidth() && ty >= y && ty <= y + ship.getHeight()){ x = tx - (ship.getWidth()/2); y = ty - (ship.getHeight()/2); } // else handle tap else if ( event.getAction() == MotionEvent.ACTION_UP) {
// if not in game, start if ( !ingame ) { ingame = true; GameStart(); } else { // fire gun keyDown(KeyEvent.KEYCODE_SPACE); } } return true; } /** * Process key down event * @param key Android Key code * @return */ public boolean keyDown( int key) { if (ingame) { mousex = −1; if (key == KeyEvent.KEYCODE_DPAD_LEFT || key == KeyEvent.KEYCODE_Q) dx = −1; if (key == KeyEvent.KEYCODE_DPAD_RIGHT || key == KeyEvent.KEYCODE_W) dx = 1; if (key == KeyEvent.KEYCODE_DPAD_UP || key == KeyEvent.KEYCODE_O) dy = −1; if (key == KeyEvent.KEYCODE_DPAD_DOWN || key == KeyEvent.KEYCODE_L) dy = 1; if ( (key == KeyEvent.KEYCODE_SPACE) ) { if (bcur > 0) { fireGun(); } } } else { if (key == KeyEvent.KEYCODE_S) { ingame = true; GameStart(); } } if (key == KeyEvent.KEYCODE_E){ ingame = false; } if (key == KeyEvent.KEYCODE_Q){ // Arggg!! There should be a better wayt to quit! System.exit(0);
} return true; } /** * Process key up event * @param e Key event * @param key key code * @return */ public boolean keyUp(int key) { if (key == KeyEvent.KEYCODE_DPAD_LEFT || key == KeyEvent.KEYCODE_DPAD_RIGHT || key == KeyEvent.KEYCODE_Q || key == KeyEvent.KEYCODE_W) dx = 0; if (key == KeyEvent.KEYCODE_DPAD_UP || key == KeyEvent.KEYCODE_DPAD_DOWN || key == KeyEvent.KEYCODE_O || key == KeyEvent.KEYCODE_L) dy = 0; return true; }
In SpaceBlaster
, when the pad arrows are pressed, the position of the ship is shifted by 1 unit in the corresponding direction. S stars the game; E ends it, and Q terminates the application. The onTouchEvent
allows the ship to be dragged around the device screen using your fingertips. When the game is running, tapping anywhere on the device screen (other than the ship) will start the game if it's not already started or fire the laser gun if the game is already running.
At this point, the project, including all files and resources, should look as shown 24.251in Figure 3-7.
We can finally start gaming! Start your emulator, and run the game like so:
From the Eclipse main menu, click Run Run Configurations.
On the left side, right-click Android Application New.
Enter a name (SpaceBlaster), and select the project ch03.SpaceBlaster (see Figure 3-8).
Click Run.
Figure 3-6 shows how the game should look on the emulator. To play, tap the screen and use the arrows to move the ship around. Press the space bar to fire the gun.
In this chapter, you have taken the first steps in writing a simple Java game and looked at the different issues that affect Java game development for Android, such as the significant differences between the Android API and the Java ME standard and how that influences code reuse and multiplatform support. You also learned gaming tricks, including creating an XML user-defined linear layout, using a timer task to simulate a game loop, invalidating a view layout within a user-defined (non-UI) thread, loading sprites and sounds, and dealing with the idiosyncrasies of the media player. You also saw some drawing techniques for creating sprite animations and simple objects and setting text styles and colors using the Paint
object. Finally, I showed you how to deal with key and touch events.
In the next chapter, we take things further to create an arcade classic—Asteroids, a game that presents a new set of challenges when dealing with drawing techniques.
3.147.58.196