Objectives
In this chapter you’ll:
• Create a simple game app that’s easy to code and fun to play.
• Create a custom SurfaceView
subclass and use it to display the game’s graphics from a separate thread of execution.
• Draw graphics using Paint
s and a Canvas
.
• Play sounds in respone to various game events.
• Manually perform frame-by-frame animations using a game loop that accounts for varying frame rates across devices.
• Override Activity
’s onTouchEvent
to process touch events when the user touches the screen or drags a finger on the screen.
• Use a GestureDetector
to recognize more sophisticated user touch motions, such as double taps.
• Perform simple collision detection.
• Add sound to your app using a SoundPool
and the AudioManager
.
• Override three additional Activity
lifecycle methods.
7.2 Test-Driving the Cannon Game app
7.4 Building the App’s GUI and Resource Files
7.4.5 Adding the Sounds to the App
7.5.1 Line
Class Maintains a Line’s Endpoints
7.5.2 CannonGame
Subclass of Activity
7.5.3 CannonView
Subclass of View
Self-Review Exercises | Answers to Self-Review Exercises | Exercises
The Cannon Game app challenges you to destroy a seven-piece target before a ten-second time limit expires (Fig. 7.1). The game consists of four visual components—a cannon that you control, a cannonball, the target and a blocker that defends the target. You aim the cannon by touching the screen—the cannon then aims at the touched point. The cannon fires a cannonball when you double-tap the screen. At the end of the game, the app displays an AlertDialog
indicating whether you won or lost, and showing the number of shots fired and the elapsed time (Fig. 7.2).
The game begins with a 10-second time limit. Each time you hit a target section, three seconds are added to the time limit, and each time you hit the blocker, two seconds are subtracted. You win by destroying all seven target sections before time runs out. If the timer reaches zero, you lose.
When you fire the cannon, the game plays a firing sound. The target consists of seven pieces. When a cannonball hits the target, a glass-breaking sound plays and that piece of the target disappears from the screen. When the cannonball hits the blocker, a hit sound plays and the cannonball bounces back. The blocker cannot be destroyed. The target and blocker move vertically at different speeds, changing direction when they hit the top or bottom of the screen.
Open Eclipse and import the Cannon Game app project. Perform the following steps:
1. Open the Import Dialog. Select File > Import... to open the Import dialog.
2. Import the Cannon Game app’s project. In the Import dialog, expand the General node and select Existing Projects into Workspace, then click Next > to proceed to the Import Projects step. Ensure that Select root directory is selected, then click the Browse... button. In the Browse for Folder dialog, locate the CannonGame
folder in the book’s examples folder, select it and click OK. Click Finish to import the project into Eclipse. The project now appears in the Package Explorer window at the left side of the Eclipse window.
3. Launch the Cannon Game app. In Eclipse, right click the CannonGame
project in the Package Explorer window, then select Run As > Android Application from the menu that appears.
Drag your finger on the screen or tap it to aim the cannon. Double tap the screen to fire a shot. You can fire a cannonball only if there is not another cannonball on the screen. If you’re running this in an AVD, your “finger” is the mouse. Try to destroy the target as fast as you can—if the timer runs out, the game ends.
This section presents the many new technologies that we use in the Cannon Game app in the order they’re encountered throughout the chapter.
String
Formatting Resources in strings.xml
In this app, we define String
resources to represent the format String
s that are used in calls to class Resource
’s method getString
(or to class String
’s static
method format
). When format String
s contain multiple format specifiers, you’re required to number them (from 1) to indicate the order in which the corresponding values will be substituted into the format String
. In some spoken languages, a String
’s phrasing might result in the values being placed at different locations in the localized String
resources. In such cases, the localized versions of strings.xml
can use the original format-specifier numbers, but place the format specifiers at appropriate locations in the localized String
s. The syntax for numbering format specifiers is shown in Section 7.4.3.
View
to a LayoutYou can create a custom view by extending class View
or one of its subclasses, as we do with class CannonView
(Section 7.5.3), which extends SurfaceView
(discussed shortly). To add a custom component to a layout’s XML file, you must fully qualify its class name in the XML element that represents the component. This is demonstrated in Section 7.4.4.
raw
Media files, such as the sounds used in the Cannon Game app are placed in the app’s resource folder res/raw
. Section 7.4.5 discusses how to create this folder. You’ll then drag the app’s sound files into it.
Activity
Lifecycle Methods onPause
and onDestroy
This app uses additional Activity
lifecycle methods. Method onPause
is called for the current Activity
when another activity receives the focus, which sends the current activity to the background. We use onPause
to suspend game play so that the game does not continue executing when the user cannot interact with it.
When an Activity
is shut down, its onDestroy
method is called. We use this method to release the app’s sound resources. These lifecycle methods are used in Section 7.5.2.
Activity
Method onTouchEvent
As you know, users interact with this app by touching the device’s screen. A touch or single tap aligns the cannon to face the touch or single tap point on the screen. To process simple touch events for an Activity
, you can override class Activity
’s onTouchEvent
method (Section 7.5.2) then use constants from class MotionEvent
(package android.view
) to test which type of event occurred and process it accordingly.
GestureDetector
and SimpleOnGestureListener
For more complex gestures, like the double taps that fire the cannon, you’ll use a GestureDetector
(package android.view
), which can recognize user actions that represent a series of MotionEvent
s. A GestureDetector
allows an app to react to more sophisticated user interactions such as flings, double-taps, long presses and scrolls. Your apps can respond to such events by implementing the methods of the GestureDetector.OnGestureListener
and GestureDetector.OnDoubleTapListener
interfaces. Class GestureDetector.SimpleOnGestureListener
is an adapter class that implements all the methods of these two interfaces, so you can extend this class and override just the method(s) you need from these interfaces. In Section 7.5.2, we initialize a GestureDetector
with a SimpleGestureListener
, which will handle the double tap event that fires the cannon.
SoundPool
and AudioManager
An app’s sound effects are managed with a SoundPool
(package android.media
), which can be used to load, play and unload sounds. Sounds are played using one of Android’s several audio streams, which include streams for alarms, DTMF tones, music, notifications, phone rings, system sounds and phone calls. The Android documentation recommends that games use the music audio stream to play sounds. We use the Activity
’s setVolumeControlStream
method to specify that the game’s volume can be controlled with the device’s volume keys and should be the same as the device’s music playback volume. The method receives a constant from class AudioManager
(package android.media
).
Thread
s, SurfaceView
and SurfaceHolder
This app performs its animations manually by updating the game elements from a separate thread of execuion. To do this, we use a subclass of Thread
with a run
method that directs our custom CannonView
to update the positions of all the game’s elements, then draws the elements. Normally, all updates to an app’s user interface must be performed in the GUI thread of execution. However, in Android, it’s important to minimize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.
Games often require complex logic that should be performed in separate threads of execution and those threads often need to draw to the screen. For such cases, Android provides class SurfaceView
—a subclass of View
to which any thread can draw. You manipulate a SurfaceView
via an object of class SurfaceHolder
, which enables you to obtain a Canvas
on which you can draw graphics. Class SurfaceHolder
also provides methods that give a thread exclusive access to the Canvas
for drawing, because only one thread at a time can draw to a SurfaceView
. Each SurfaceView
subclass should implement the interface SurfaceHolder.Callback
, which contains methods that are called when the SurfaceView
is created, changed (e.g., its size or orientation changes) or destroyed.
The CannonView
performs simple collision detection to determine whether the cannonball has collided with any of the CannonView
’s edges, with the blocker or with a section of the target. These techniques are presented in Section 7.5.3. [Note: Many game-development frameworks provide more sophisticated collision detection capabilities.]
Paint
and Canvas
We use methods of class Canvas
(package android.graphics
) to draw text, lines and circles. A Canvas
draws on a View
’s Bitmap
. Each drawing method in class Canvas
uses an object of class Paint
(package android.graphics
) to specify drawing characteristics, including color, line thickness, font size and more. These capabilities are presented with the drawGameElements
method in Section 7.5.3. For more details on the drawing characteristics you can specify with a Paint
object, visit
developer.android.com/reference/android/graphics/Paint.html
In this section, you’ll create the app’s resource files and main.xml
layout file.
Begin by creating a new Android project named CannonGame
. Specify the following values in the New Android Project dialog, then press Finish:
• Build Target: Ensure that Android 2.3.3 is checked
• Application name: Cannon Game
• Package name: com.deitel.cannongame
• Create Activity: CannonGame
• Min SDK Version: 8
.
AndroidManifest.xml
Figure 7.3 shows this app’s AndroidManifest.xml
file. As in Section 6.6, we set the activity
element’s android:screenOrientation
attribute to "portrait"
(line 9) so that the app always displays in portrait mode.
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="com.deitel.cannongame" android:versionCode="1"
4 android:versionName="1.0">
5 <application android:icon="@drawable/icon"
6 android:label="@string/app_name" android:debuggable="true">
7 <activity android:name=".CannonGame"
8 android:label="@string/app_name"
9 android:screenOrientation="portrait">
10 <intent-filter>
11 <action android:name="android.intent.action.MAIN" />
12 <category android:name="android.intent.category.LAUNCHER" />
13 </intent-filter>
14 </activity>
15 </application>
16 <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10"/>
17 </manifest>
strings.xml
We’ve specified format String
s (Fig. 7.4. lines 4–5 and 9–10) in this app’s strings.xml
file. As mentioned in Section 7.3, format String
s that contain multiple format specifiers must number the format specifiers for localization purposes. The notation 1$
in %1$.1f
(line 5) indicates that the first argument after the format String
should replace the format specifier %1$d
. Similarly, %2$.1f
indicates that the second argument after the format String
should replace the format specifier %2$.1f
. The d
in the first format specifier indicates that we’re formatting a decimal integer and the f
in the second one indicates that we’re formatting a floating-point value. In localized versions of strings.xml
, the format specifiers %1$d
and %2$.1f
can be reordered as necessary—the first argument after the format String
in a call to Resources
method getString
or String
method format
will replace %1$d
—regardless of where it appears in the format String
—and the second argument will replace %2$.1f
regardless of where they appear in the format String
.
1 <?xml version="1.0" encoding="UTF-8"?>
2 <resources>
3 <string name="app_name">Cannon Game</string>
4 <string name="results_format">
5 Shots fired: %1$d
Total time: %2$.1f</string>
6 <string name="reset_game">Reset Game</string>
7 <string name="win">You win!</string>
8 <string name="lose">You lose!</string>
9 <string name="time_remaining_format">
10 Time remaining: %.1f seconds</string>
11 </resources>
main.xml
In this app, we deleted the default main.xml
file and replaced it with one containing a FrameLayout
. The only component in this app’s layout is an instance of our custom View
subclass, CannonView
, which you’ll add to the project in Section 7.5.3. Figure 7.5 shows the completed main.xml
in which we manually entered the XML element shown in lines 2–7. That element indicates that the CannonView
should occupy the entire width and height of the parent layout and should have a white background. Recall from Section 7.3 that you must fully qualify a custom View
’s class name in the layout XML, so line 2 refers to the CannonView
as com.deitel.cannongame.CannonView
.
1 <?xml version="1.0" encoding="utf-8"?>
2 <com.deitel.cannongame.CannonView
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:id="@+id/cannonView"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="@android:color/white"/>
As we mentioned previously, sound files are stored in the app’s res/raw
folder. This app uses three sound files—blocker_hit.wav
, target_hit.wav
and cannon_fire.wav
—which are located with the book’s examples in the sounds
folder. To add these files to your project:
1. Right click the app’s res
folder then select New > Folder.
2. Specify the folder name raw
and click Finish to create the folder.
3. Drag the sound files into the res/raw
folder.
This app consists of three classes—Line
(Fig. 7.6), CannonGame
(the Activity
subclass; Figs. 7.7–7.10) and CannonView
(Figs. 7.11–7.23).
1 // Line.java
2 // Class Line represents a line with two endpoints.
3 package com.deitel.cannongame;
4
5 import android.graphics.Point;
6
7 public class Line
8 {
9 public Point start; // starting Point
10 public Point end; // ending Point
11
12 // default constructor initializes Points to the origin (0, 0)
13 public Line()
14 {
15 start = new Point(0, 0); // start Point
16 end = new Point(0, 0); // end Point
17 } // end method Line
18 } // end class Line
Line
Class Maintains a Line’s EndpointsClass Line
(Fig. 7.6) simply groups two Point
s that represent a line’s starting Point
and ending Point
. We use objects of this class to define the blocker and target. To add class Line
to the project:
1. Expand the project’s src
node in the Package Explorer.
2. Right click the package (com.deitel.cannongame
) and select New > Class to display the New Java Class dialog.
3. In the dialog’s Name field, enter Line
and click Finish.
4. Enter the code in Fig. 7.6 into the Line.java
file.
CannonGame
Subclass of Activity
Class CannonGame
(Figs. 7.7–7.10) is the Cannon Game app’s main Activity
.
1 // CannonGame.java
2 // Main Activity for the Cannon Game app.
3 package com.deitel.cannongame;
4
5 import android.app.Activity;
6 import android.os.Bundle;
7 import android.media.AudioManager;
8 import android.view.GestureDetector;
9 import android.view.MotionEvent;
10 import android.view.GestureDetector.SimpleOnGestureListener;
11
12 public class CannonGame extends Activity
13 {
14 private GestureDetector gestureDetector; // listens for double taps
15 private CannonView cannonView; // custom view to display the game
16
package
Statement, import
Statements and Instance VariablesSection 7.3 discussed the key new classes and interfaces that class CannonGame
uses. We’ve highlighted these classes and interfaces in Fig. 7.7. Line 15 declares variable cannonView
, which will enable class CannonGame
to interact with the CannonView
.
Activity
Methods onCreate
, onPause
and onDestroy
Figure 7.8 presents overridden Activity
methods onCreate
(lines 18–32), onPause
(lines 35–40) and onDestroy
(lines 43–48). Method onCreate
inflates the activity’s main.xml
layout, then gets a reference to the CannonView
object (line 25). Line 28 creates the GestureDetector
that detects double taps for this activity using the gestureListener
, which is defined in Fig. 7.10. Line 31 allows the game’s audio volume to be controlled by the device’s volume keys.
17 // called when the app first launches
18 @Override
19 public void onCreate(Bundle savedInstanceState)
20 {
21 super.onCreate(savedInstanceState); // call super's onCreate method
22 setContentView(R.layout.main); // inflate the layout
23
24 // get the CannonView
25 cannonView = (CannonView) findViewById(R.id.cannonView);
26
27 // initialize the GestureDetector
28 gestureDetector = new GestureDetector(this, gestureListener);
29
30 // allow volume keys to set game volume
31 setVolumeControlStream(AudioManager.STREAM_MUSIC);
32 } // end method onCreate
33
34 // when the app is pushed to the background, pause it
35 @Override
36 public void onPause()
37 {
38 super.onPause(); // call the super method
39 cannonView.stopGame(); // terminates the game
40 } // end method onPause
41
42 // release resources
43 @Override
44 protected void onDestroy()
45 {
46 super.onDestroy();
47 cannonView.releaseResources();
48 } // end method onDestroy
49
Method onPause
(lines 35–40) ensures that the CannonGame
activity does not continue executing when it’s sent to the background. If the game did continue executing, not only would the user not be able to interact with the game because another activity has the focus, but the app would also continue consuming battery power—a precious resource for mobile devices. When onPause
is called, line 39 calls the cannonView
’s stopGame
method (Fig. 7.21) to terminate the game’s thread—we don’t save the game’s state in this example.
When the activity is shut down, method onDestroy
(lines 43–46) calls the cannonView
’s releaseResources
method (Fig. 7.21), which releases the app’s sound resources.
Activity
Method onTouchEvent
In this example, we override method onTouchEvent
(Fig. 7.9) to determine when the user touches the screen or moves a finger across the screen. The MotionEvent
parameter contains information about the event that occurred. Line 55 uses the MotionEvent
’s getAction
method to determine which type of event occurred. Then, lines 58–59 determine whether the user touched the screen (MotionEvent.ACTION_DOWN
) or moved a finger across the screen (MotionEvent.ACTION_MOVE
). In either case, line 61 calls the cannonView
’s alignCannon
method (Fig. 7.18) to aim the cannon towards that touch point. Line 65 then passes the MotionEvent
object to the gestureDetector
’s onTouchEvent
method to check whether a double tap occurred.
50 // called when the user touches the screen in this Activity
51 @Override
52 public boolean onTouchEvent(MotionEvent event)
53 {
54 // get int representing the type of action which caused this event
55 int action = event.getAction();
56
57 // the user user touched the screen or dragged along the screen
58 if (action == MotionEvent.ACTION_DOWN ||
59 action == MotionEvent.ACTION_MOVE)
60 {
61 cannonView.alignCannon(event); // align the cannon
62 } // end if
63
64 // call the GestureDetector's onTouchEvent method
65 return gestureDetector.onTouchEvent(event);
66 } // end method onTouchEvent
SimpleOnGestureListener
Figure 7.10 creates the SimpleGestureListener
named gestureListener
which was registered at line 28 with the GestureDetector
. Recall that SimpleGestureListener
is an adapter class that implements all the methods of interfaces OnGestureListener
and OnDoubleTapListener
. The methods simply return false
—indicating that the events were not handled. We override only the onDoubleTap
method (lines 71–76), which is called when the user double taps the screen. Line 74 calls CannonView
’s fireCannonBall
method (Fig. 7.17) to fire a cannonball. Method fireCannonBall
obtains the screen location of the double-tap from its MotionEvent
argument—this is used to aim the shot at the correct angle. Line 75 returns true
indicating that the event was handled.
67 // listens for touch events sent to the GestureDetector
68 SimpleOnGestureListener gestureListener = new SimpleOnGestureListener()
69 {
70 // called when the user double taps the screen
71 @Override
72 public boolean onDoubleTap(MotionEvent e)
73 {
74 cannonView.fireCannonball(e); // fire the cannonball
75 return true; // the event was handled
76 } // end method onDoubleTap
77 }; // end gestureListener
78 } // end class CannonGame
CannonView
Subclass of View
Class CannonView
(Figs. 7.11–7.23) is a custom subclass of View
that implements the Cannon Game’s logic and draws game objects on the screen. To add the class to the project:
1. Expand the project’s src
node in the Package Explorer.
2. Right click the package (com.deitel.cannongame
) and select New > Class to display the New Java Class dialog.
3. In the dialog’s Name field, enter CannonView
, in the Superclass field enter android.view.View
, then click Finish.
4. Enter the code in Figs. 7.11–7.21 into the CannonView.java
file.
1 // CannonView.java
2 // Displays the Cannon Game
3 package com.deitel.cannongame;
4
5 import java.util.HashMap;
6 import java.util.Map;
7
8 import android.app.Activity;
9 import android.app.AlertDialog;
10 import android.content.Context;
11 import android.content.DialogInterface;
12 import android.graphics.Canvas;
13 import android.graphics.Color;
14 import android.graphics.Paint;
15 import android.graphics.Point;
16 import android.media.AudioManager;
17 import android.media.SoundPool;
18 import android.util.AttributeSet;
19 import android.view.MotionEvent;
20 import android.view.SurfaceHolder;
21 import android.view.SurfaceView;
22
package
and import
StatementsFigure 7.11 lists the package
statement and the import
statements for class CannonView
. Section 7.3 discussed the key new classes and interfaces that class CannonView
uses. We’ve highlighted them in Fig. 7.11.
CannonView
Instance Variables and ConstantsFigure 7.12 lists the large number of class CannonView
’s constants and instance variables. Most are self explanatory, but we’ll explain each as we encounter it in the discussion.
23 public class CannonView extends SurfaceView
24 implements SurfaceHolder.Callback
25 {
26 private CannonThread cannonThread; // controls the game loop
27 private Activity activity; // to display Game Over dialog in GUI thread
28 private boolean dialogIsDisplayed = false;
29
30 // constants for game play
31 public static final int TARGET_PIECES = 7; // sections in the target
32 public static final int MISS_PENALTY = 2; // seconds deducted on a miss
33 public static final int HIT_REWARD = 3; // seconds added on a hit
34
35 // variables for the game loop and tracking statistics
36 private boolean gameOver; // is the game over?
37 private double timeLeft; // the amount of time left in seconds
38 private int shotsFired; // the number of shots the user has fired
39 private double totalTimeElapsed; // the number of seconds elapsed
40
41 // variables for the blocker and target
42 private Line blocker; // start and end points of the blocker
43 private int blockerDistance; // blocker distance from left
44 private int blockerBeginning; // blocker distance from top
45 private int blockerEnd; // blocker bottom edge distance from top
46 private int initialBlockerVelocity; // initial blocker speed multiplier
47 private float blockerVelocity; // blocker speed multiplier during game
48
49 private Line target; // start and end points of the target
50 private int targetDistance; // target distance from left
51 private int targetBeginning; // target distance from top
52 private double pieceLength; // length of a target piece
53 private int targetEnd; // target bottom's distance from top
54 private int initialTargetVelocity; // initial target speed multiplier
55 private float targetVelocity; // target speed multiplier during game
56
57 private int lineWidth; // width of the target and blocker
58 private boolean[] hitStates; // is each target piece hit?
59 private int targetPiecesHit; // number of target pieces hit (out of 7)
60
61 // variables for the cannon and cannonball
62 private Point cannonball; // cannonball image's upper-left corner
63 private int cannonballVelocityX; // cannonball's x velocity
64 private int cannonballVelocityY; // cannonball's y velocity
65 private boolean cannonballOnScreen; // is the cannonball on the screen
66 private int cannonballRadius; // cannonball radius
67 private int cannonballSpeed; // cannonball speed
68 private int cannonBaseRadius; // cannon base radius
69 private int cannonLength; // cannon barrel length
70 private Point barrelEnd; // the endpoint of the cannon's barrel
71 private int screenWidth; // width of the screen
72 private int screenHeight; // height of the screen
73
74 // constants and variables for managing sounds
75 private static final int TARGET_SOUND_ID = 0;
76 private static final int CANNON_SOUND_ID = 1;
77 private static final int BLOCKER_SOUND_ID = 2;
78 private SoundPool soundPool; // plays sound effects
79 private Map<Integer, Integer> soundMap; // maps IDs to SoundPool
80
81 // Paint variables used when drawing each item on the screen
82 private Paint textPaint; // Paint used to draw text
83 private Paint cannonballPaint; // Paint used to draw the cannonball
84 private Paint cannonPaint; // Paint used to draw the cannon
85 private Paint blockerPaint; // Paint used to draw the blocker
86 private Paint targetPaint; // Paint used to draw the target
87 private Paint backgroundPaint; // Paint used to clear the drawing area
88
CannonView
ConstructorFigure 7.13 shows class CannonView
’s constructor. When a View
is inflated, its constructor is called and passed a Context
and an AttributeSet
as arguments. In this case, the Context
is the Activity
(CannonGame
) to which the CannonView
is attached and the AttributeSet
(package android.util
) contains the values for any attributes that are set in the layout’s XML document. These arguments should be passed to the superclass constructor (line 92) to ensure that the custom View
object is properly configured with the values of any standard View
attributes specified in the XML.
89 // public constructor
90 public CannonView(Context context, AttributeSet attrs)
91 {
92 super(context, attrs); // call super's constructor
93 activity = (Activity) context;
94
95 // register SurfaceHolder.Callback listener
96 getHolder().addCallback(this);
97
98 // initialize Lines and points representing game items
99 blocker = new Line(); // create the blocker as a Line
100 target = new Line(); // create the target as a Line
101 cannonball = new Point(); // create the cannonball as a point
102
103 // initialize hitStates as a boolean array
104 hitStates = new boolean[TARGET_PIECES];
105
106 // initialize SoundPool to play the app's three sound effects
107 soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
108
109 // create Map of sounds and pre-load sounds
110 soundMap = new HashMap<Integer, Integer>(); // create new HashMap
111 soundMap.put(TARGET_SOUND_ID,
112 soundPool.load(context, R.raw.target_hit, 1));
113 soundMap.put(CANNON_SOUND_ID,
114 soundPool.load(context, R.raw.cannon_fire, 1));
115 soundMap.put(BLOCKER_SOUND_ID,
116 soundPool.load(context, R.raw.blocker_hit, 1));
117
118 // construct Paints for drawing text, cannonball, cannon,
119 // blocker and target; these are configured in method onSizeChanged
120 textPaint = new Paint(); // Paint for drawing text
121 cannonPaint = new Paint(); // Paint for drawing the cannon
122 cannonballPaint = new Paint(); // Paint for drawing a cannonball
123 blockerPaint = new Paint(); // Paint for drawing the blocker
124 targetPaint = new Paint(); // Paint for drawing the target
125 backgroundPaint = new Paint(); // Paint for drawing the target
126 } // end CannonView constructor
127
Line 93 stores a reference to the parent Activity
so we can use it at the end of a game to display an AlertDialog
from the Activity
’s GUI thread. Line 96 registers this
(i.e., the CannonView
) as the object that implements SurfaceHolder.Callback
to receive the method calls that indicate when the SurfaceView
is created, updated and destroyed. SurfaceView
method getHolder
returns the corresponding SurfaceHolder
object for managing the SurfaceView
, and SurfaceHolder
method addCallback
stores the object that implements SurfaceHolder.Callback
.
Lines 99–101 create the blocker
and target
as Line
s and the cannonball
as a Point
. Next, we create boolean
array hitStates
to keep track of which of the target’s seven pieces have been hit (and thus should not be drawn).
Lines 107–116 configure the sounds that we use in the app. First, we create the SoundPool
that’s used to load and play the app’s sound effects. The constructor’s first argument represents the maximum number of simultaneous sound streams that can play at once. We play only one sound at a time, so we pass 1
. The second argument specifies which audio stream will be used to play the sounds. There are seven sound streams identified by constants in class AudioManager
, but the documentation for class SoundPool
recommends using the stream for playing music (AudioManager.STREAM_MUSIC
) for sound in games. The last argument represents the sound quality, but the documentation indicates that this value is not currently used and 0
should be specified as the default value.
Line 110 creates a HashMap
(soundMap
). Then, lines 111–116 populate it, using the constants at lines 75–77 as keys. The corresponding values are the return values of the SoundPool
’s load
method, which returns an ID that can be used to play (or unload) a sound. SoundPool
method load
receives three arguments—the application’s Context
, a resource ID representing the sound file to load and the sound’s priority. According to the documentation for this method, the last argument is not currently used and should be specified as 1
.
Lines 120–125 create the Paint
objects that are used when drawing the game’s objects. We configure these in method onSizeChanged
, because some of the Paint
settings depend on scaling the game elements based on the device’s screen size.
View
Method onSizeChanged
Figure 7.14 overrides class View
’s onSizeChanged
method, which is called whenever the View
’s size changes, including when the View
is first added to the View
hierarchy as the layout is inflated. This app always displays in portrait mode, so onSizeChanged
is called only once when the activity’s onCreate
method inflates the GUI. The method receives the View
’s new width and height and its old width and height—when this method is called the first time, the old width and height are 0. The calculations performed here scale the game’s on-screen elements based on the device’s pixel width and height—we arrived at our scaling factors via trial and error. After the calculations, line 173 calls method newGame
(Fig. 7.15).
128 // called when the size of this View changes--including when this
129 // view is first added to the view hierarchy
130 @Override
131 protected void onSizeChanged(int w, int h, int oldw, int oldh)
132 {
133 super.onSizeChanged(w, h, oldw, oldh);
134
135 screenWidth = w; // store the width
136 screenHeight = h; // store the height
137 cannonBaseRadius = h / 18; // cannon base radius 1/18 screen height
138 cannonLength = w / 8; // cannon length 1/8 screen width
139
140 cannonballRadius = w / 36; // cannonball radius 1/36 screen width
141 cannonballSpeed = w * 3 / 2; // cannonball speed multiplier
142
143 lineWidth = w / 24; // target and blocker 1/24 screen width
144
145 // configure instance variables related to the blocker
146 blockerDistance = w * 5 / 8; // blocker 5/8 screen width from left
147 blockerBeginning = h / 8; // distance from top 1/8 screen height
148 blockerEnd = h * 3 / 8; // distance from top 3/8 screen height
149 initialBlockerVelocity = h / 2; // initial blocker speed multiplier
150 blocker.start = new Point(blockerDistance, blockerBeginning);
151 blocker.end = new Point(blockerDistance, blockerEnd);
152
153 // configure instance variables related to the target
154 targetDistance = w * 7 / 8; // target 7/8 screen width from left
155 targetBeginning = h / 8; // distance from top 1/8 screen height
156 targetEnd = h * 7 / 8; // distance from top 7/8 screen height
157 pieceLength = (targetEnd - targetBeginning) / TARGET_PIECES;
158 initialTargetVelocity = -h / 4; // initial target speed multiplier
159 target.start = new Point(targetDistance, targetBeginning);
160 target.end = new Point(targetDistance, targetEnd);
161
162 // endpoint of the cannon's barrel initially points horizontally
163 barrelEnd = new Point(cannonLength, h / 2);
164
165 // configure Paint objects for drawing game elements
166 textPaint.setTextSize(w / 20); // text size 1/20 of screen width
167 textPaint.setAntiAlias(true); // smoothes the text
168 cannonPaint.setStrokeWidth(lineWidth * 1.5f); // set line thickness
169 blockerPaint.setStrokeWidth(lineWidth); // set line thickness
170 targetPaint.setStrokeWidth(lineWidth); // set line thickness
171 backgroundPaint.setColor(Color.WHITE); // set background color
172
173 newGame(); // set up and start a new game
174 } // end method onSizeChanged
175
176 // reset all the screen elements and start a new game
177 public void newGame()
178 {
179 // set every element of hitStates to false--restores target pieces
180 for (int i = 0; i < TARGET_PIECES; ++i)
181 hitStates[i] = false;
182
183 targetPiecesHit = 0; // no target pieces have been hit
184 blockerVelocity = initialBlockerVelocity; // set initial velocity
185 targetVelocity = initialTargetVelocity; // set initial velocity
186 timeLeft = 10; // start the countdown at 10 seconds
187 cannonballOnScreen = false; // the cannonball is not on the screen
188 shotsFired = 0; // set the initial number of shots fired
189 totalElapsedTime = 0.0; // set the time elapsed to zero
190 blocker.start.set(blockerDistance, blockerBeginning);
191 blocker.end.set(blockerDistance, blockerEnd);
192 target.start.set(targetDistance, targetBeginning);
193 target.end.set(targetDistance, targetEnd);
194
195 if (gameOver)
196 {
197 gameOver = false; // the game is not over
198 cannonThread = new CannonThread(getHolder());
199 cannonThread.start();
200 } // end if
201 } // end method newGame
202
CannonView
Method newGame
Method newGame
(Fig. 7.15) resets the initial values of the instance variables that are used to control the game. If variable gameOver
is true
, which occurs only after the first game completes, line 197 resets gameOver
and lines 198–199 create a new CannonThread
and start it to begin the new game.
CannonView
Method updatePositions
Method updatePositions
(Fig. 7.16) is called by the CannonThread
’s run
method (Fig. 7.23) to update the on-screen elements’ positions and to perform simple collision detection. The new locations of the game elements are calculated based on the elapsed time in milliseconds between the previous frame of the animation and the current frame of the animation. This enables the game to update the amount by which each game element moves based on the device’s refresh rate. We discuss this in more detail when we cover game loops in Fig. 7.23.
203 // called repeatedly by the CannonThread to update game elements
204 private void updatePositions(double elapsedTimeMS)
205 {
206 double interval = elapsedTimeMS / 1000.0; // convert to seconds
207
208 if (cannonballOnScreen) // if there is currently a shot fired
209 {
210 // update cannonball position
211 cannonball.x += interval * cannonballVelocityX;
212 cannonball.y += interval * cannonballVelocityY;
213
214 // check for collision with blocker
215 if (cannonball.x + cannonballRadius > blockerDistance &&
216 cannonball.x - cannonballRadius < blockerDistance &&
217 cannonball.y + cannonballRadius > blocker.start.y &&
218 cannonball.y - cannonballRadius < blocker.end.y)
219 {
220 cannonballVelocityX *= -1; // reverse cannonball's direction
221 timeLeft -= MISS_PENALTY; // penalize the user
222
223 // play blocker sound
224 soundPool.play(soundMap.get(BLOCKER_SOUND_ID), 1, 1, 1, 0, 1f)
225 } // end if
226
227 // check for collisions with left and right walls
228 else if (cannonball.x + cannonballRadius > screenWidth ||
229 cannonball.x - cannonballRadius < 0)
230 cannonballOnScreen = false; // remove cannonball from screen
231
232 // check for collisions with top and bottom walls
233 else if (cannonball.y + cannonballRadius > screenHeight ||
234 cannonball.y - cannonballRadius < 0)
235 cannonballOnScreen = false; // make the cannonball disappear
236
237 // check for cannonball collision with target
238 else if (cannonball.x + cannonballRadius > targetDistance &&
239 cannonball.x - cannonballRadius < targetDistance &&
240 cannonball.y + cannonballRadius > target.start.y &&
241 cannonball.y - cannonballRadius < target.end.y)
242 {
243 // determine target section number (0 is the top)
244 int section =
245 (int) ((cannonball.y - target.start.y) / pieceLength);
246
247 // check if the piece hasn't been hit yet
248 if ((section >= 0 && section < TARGET_PIECES) &&
249 !hitStates[section])
250 {
251 hitStates[section] = true; // section was hit
252 cannonballOnScreen = false; // remove cannonball
253 timeLeft += HIT_REWARD; // add reward to remaining time
254
255 // play target hit sound
256 soundPool.play(soundMap.get(TARGET_SOUND_ID), 1,
257 1, 1, 0, 1f);
258
259 // if all pieces have been hit
260 if (++targetPiecesHit == TARGET_PIECES)
261 {
262 cannonThread.setRunning(false);
263 showGameOverDialog(R.string.win); // show winning dialog
264 gameOver = true; // the game is over
265 } // end if
266 } // end if
267 } // end else if
268 } // end if
269
270 // update the blocker's position
271 double blockerUpdate = interval * blockerVelocity;
272 blocker.start.y += blockerUpdate;
273 blocker.end.y += blockerUpdate;
274
275 // update the target's position
276 double targetUpdate = interval * targetVelocity;
277 target.start.y += targetUpdate;
278 target.end.y += targetUpdate;
279
280 // if the blocker hit the top or bottom, reverse direction
281 if (blocker.start.y < 0 || blocker.end.y > screenHeight)
282 blockerVelocity *= -1;
283
284 // if the target hit the top or bottom, reverse direction
285 if (target.start.y < 0 || target.end.y > screenHeight)
286 targetVelocity *= -1;
287
288 timeLeft -= interval; // subtract from time left
289
290 // if the timer reached zero
291 if (timeLeft <= 0)
292 {
293 timeLeft = 0.0;
294 gameOver = true; // the game is over
295 cannonThread.setRunning(false);
296 showGameOverDialog(R.string.lose); // show the losing dialog
297 } // end if
298 } // end method updatePositions
299
Line 206 converts the elapsed time since the last animation frame from milliseconds to seconds. This value is used to modify the positions of various game elements.
Line 208 checks whether the cannonball is on the screen. If it is, we update its position by adding the distance it should have traveled since the last timer event. This is calculated by multiplying its velocity by the amount of time that passed (lines 211–212). Lines 215–218 check whether the cannonball has collided with the blocker. We perform simple collision detection, based on the rectangular boundary of the cannonball. There are four conditions that must be met if the cannonball is in contact with the blocker:
• The cannonball’s x-coordinate plus the cannon ball’s radius must be greater than the blocker’s distance from the left edge of the screen (blockerDistance
) (line 215). This means that the cannonball has reached the blocker’s distance from the left edge of the screen.
• The cannonball’s x-coordinate minus the cannon ball’s radius must also be less than the blocker’s distance from the left edge of the screen (line 216). This ensures that the cannonball has not yet passed the blocker.
• Part of the cannonball must be lower than the top of the blocker (line 217).
• Part of the cannonball must be higher than the bottom of the blocker (line 218).
If all these conditions are met, we reverse the cannonball’s direction on the screen (line 220), penalize the user by subtracting MISS_PENALTY
from timeLeft
, then call soundPool
’s play
method to play the blocker hit sound—BLOCKER_SOUND_ID
is used as the soundMap
key to locate the sound’s ID in the SoundPool
.
We remove the cannonball if it reaches any of the screen’s edges. Lines 228–230 test whether the cannonball has collided with the left or right wall and, if it has, remove the cannonball from the screen. Lines 233–235 remove the cannonball if it collides with the top or bottom of the screen.
We then check whether the cannonball has hit the target
(lines 238–241). These conditions are similar to those used to determine whether the cannonball collided with the blocker
. If the cannonball hit the target
, we determine which section of the target
was hit. Lines 244–245 determine which section has been hit—dividing the distance between the cannonball and the bottom of the target
by the length of a piece. This expression evaluates to 0
for the top-most section and 6
for the bottom-most. We check whether that section was previously hit, using the hitStates
array (line 249). If it wasn’t, we set the corresponding hitStates
element to true
and remove the cannonball from the screen. We then add HIT_REWARD
to timeLeft
, increasing the game’s time remaining, and play the target hit sound (TARGET_SOUND_ID
). We increment targetPiecesHit
, then determine whether it’s equal to TARGET_PIECES
(line 260). If so, the game is over, so we terminate the CannonThread
by calling its setRunning
method with the argument false
, invoke method showGameOverDialog
with the String
resource ID representing the winning message and set gameOver
to true
.
Now that all possible cannonball collisions have been checked, the blocker
and target
positions must be updated. Lines 271–273 change the blocker
’s position by multiplying blockerVelocity
by the amount of time that has passed since the last update and adding that value to the current x- and y-coordinates. Lines 276–278 do the same for the target
. If the blocker
has collided with the top or bottom wall, its direction is reversed by multiplying its velocity by -1
(lines 281–282). Lines 285–286 perform the same check and adjustment for the full length of the target
, including any sections that have already been hit.
We decrease timeLeft
by the time that has passed since the prior animation frame. If timeLeft
has reached zero, the game is over—we set timeLeft
to 0.0 just in case it was negative; otherwise, we’ll sometimes display a negative final time on the screen). Then we set gameOver
to true
, terminate the CannonThread
by calling its setRunning
method with the argument false
and call method showGameOverDialog
with the String
resource ID representing the losing message.
CannonView
Method fireCannonball
When the user double taps the screen, the event handler for that event (Fig. 7.10) calls method fireCannonball
(Fig. 7.17) to fire a cannonball. If there’s already a cannonball on the screen, the method returns immediately; otherwise, it fires the cannon. Line 306 calls alignCannon
to aim the cannon at the double-tap point and get the cannon’s angle. Lines 309–310 “load” the cannon (that is, position the cannonball inside the cannon). Then, lines 313 and 316 calculate the horizontal and vertical components of the cannonball’s velocity. Next, we set cannonballOnScreen
to true
so that the cannonball will be drawn by method drawGameElements
(Fig. 7.19) and increment shotsFired
. Finally, we play the cannon’s firing sound (CANNON_SOUND_ID
).
300 // fires a cannonball
301 public void fireCannonball(MotionEvent event)
302 {
303 if (cannonballOnScreen) // if a cannonball is already on the screen
304 return; // do nothing
305
306 double angle = alignCannon(event); // get the cannon barrel's angle
307
308 // move the cannonball to be inside the cannon
309 cannonball.x = cannonballRadius; // align x-coordinate with cannon
310 cannonball.y = screenHeight / 2; // centers ball vertically
311
312 // get the x component of the total velocity
313 cannonballVelocityX = (int) (cannonballSpeed * Math.sin(angle));
314
315 // get the y component of the total velocity
316 cannonballVelocityY = (int) (-cannonballSpeed * Math.cos(angle));
317 cannonballOnScreen = true; // the cannonball is on the screen
318 ++shotsFired; // increment shotsFired
319
320 // play cannon fired sound
321 soundPool.play(soundMap.get(CANNON_SOUND_ID), 1, 1, 1, 0, 1f);
322 } // end method fireCannonball
323
CannonView
Method alignCannon
Method alignCannon
(Fig. 7.18) aims the cannon at the point where the user double tapped the screen. Line 328 gets the x- and y-coordinates of the double tap from the MotionEvent
argument. We compute the vertical distance of the touch from the center of the screen. If this is not zero, we calculate cannon barrel’s angle
from the horizontal (line 338). If the touch is on the lower-half of the screen we adjust the angle
by Math.PI
(line 342). We then use the cannonLength
and the angle
to determine the x and y coordinate values for the endpoint of the cannon’s barrel—this is used to draw a line from the cannon base’s center at the left edge of the screen to the cannon’s barrel endpoint.
324 // aligns the cannon in response to a user touch
325 public double alignCannon(MotionEvent event)
326 {
327 // get the location of the touch in this view
328 Point touchPoint = new Point((int) event.getX(), (int) event.getY());
329
330 // compute the touch's distance from center of the screen
331 // on the y-axis
332 double centerMinusY = (screenHeight / 2 - touchPoint.y);
333
334 double angle = 0; // initialize angle to 0
335
336 // calculate the angle the barrel makes with the horizontal
337 if (centerMinusY != 0) // prevent division by 0
338 angle = Math.atan((double) touchPoint.x / centerMinusY);
339
340 // if the touch is on the lower half of the screen
341 if (touchPoint.y > screenHeight / 2)
342 angle += Math.PI; // adjust the angle
343
344 // calculate the endpoint of the cannon barrel
345 barrelEnd.x = (int) (cannonLength * Math.sin(angle));
346 barrelEnd.y =
347 (int) (-cannonLength * Math.cos(angle) + screenHeight / 2);
348
349 return angle; // return the computed angle
350 } // end method alignCannon
351
The method drawGameElements
(Fig. 7.19) draws the cannon, cannonball, blocker and target on the SurfaceView
using the Canvas
that the CannonThread
obtains from the SurfaceView
’s SurfaceHolder
.
352 // draws the game to the given Canvas
353 public void drawGameElements(Canvas canvas)
354 {
355 // clear the background
356 canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
357 backgroundPaint);
358
359 // display time remaining
360 canvas.drawText(getResources().getString(
361 R.string.time_remaining_format, timeLeft), 30, 50, textPaint);
362
363 // if a cannonball is currently on the screen, draw it
364 if (cannonballOnScreen)
365 canvas.drawCircle(cannonball.x, cannonball.y, cannonballRadius,
366 cannonballPaint);
367
368 // draw the cannon barrel
369 canvas.drawLine(0, screenHeight / 2, barrelEnd.x, barrelEnd.y,
370 cannonPaint);
371
372 // draw the cannon base
373 canvas.drawCircle(0, (int) screenHeight / 2,
374 (int) cannonBaseRadius, cannonPaint);
375
376 // draw the blocker
377 canvas.drawLine(blocker.start.x, blocker.start.y, blocker.end.x,
378 blocker.end.y, blockerPaint);
379
380 Point currentPoint = new Point(); // start of current target section
381
382 // initialize curPoint to the starting point of the target
383 currentPoint.x = target.start.x;
384 currentPoint.y = target.start.y;
385
386 // draw the target
387 for (int i = 1; i <= TARGET_PIECES; ++i)
388 {
389 // if this target piece is not hit, draw it
390 if (!hitStates[i - 1])
391 {
392 // alternate coloring the pieces yellow and blue
393 if (i % 2 == 0)
394 targetPaint.setColor(Color.YELLOW);
395 else
396 targetPaint.setColor(Color.BLUE);
397
398 canvas.drawLine(currentPoint.x, currentPoint.y, target.end.x,
399 (int) (currentPoint.y + pieceLength), targetPaint);
400 }
401
402 // move curPoint to the start of the next piece
403 currentPoint.y += pieceLength;
404 } // end for
405 } // end method drawGameElements
406
First, we call Canvas
’s drawRect
method (lines 356–357) to clear the Canvas
so that all the game elements can be displayed in their new positions. The method receives as arguments the rectangle’s upper-left x-y coordinates, the rectangle’s width and height, and the Paint
object that specifies the drawing characteristics—recall that backgroundPaint
sets the drawing color to white. Next, we call Canvas
’s drawText
method (lines 360–361) to display the time remaining in the game. We pass as arguments the String
to be displayed, the x- and y-coordinates at which to display it and the textPaint
(configured in lines 166–167) to describe how the text should be rendered (that is, the text’s font size, color and other attributes).
If the cannonball is on the screen, lines 365–366 use Canvas
’s drawCircle
method to draw the cannonball in its current position. The first two arguments represent the coordinates of the circle’s center. The third argument is the circle’s radius. The last argument is the Paint
object specifying the circle’s drawing characteristics.
We use Canvas
’s drawLine
method to display the cannon barrel (lines 369–370), the blocker (lines 377–378) and the target pieces (lines 398–399). This method receives five parameters—the first four represent the x-y coordinates of the line’s start and end, and the last is the Paint
object specifying the line’s characteristics, such as the line’s thickness.
Lines 373–374 use Canvas
’s drawCircle
method to draw the cannon’s half-circle base by drawing a circle that’s centered at the left edge of the screen—because a circle is displayed based on its center point, half of this circle is drawn off the left side of the SurfaceView
.
Lines 380–404 draw the target sections. We iterate through the target’s sections, drawing each in the correct color—blue for the odd-numbered pieces and yellow for the others. Only those sections that haven’t been hit are displayed.
CannonView
Method showGameOverDialog
When the game ends, the showGameOverDialog
method (Fig. 7.20) displays an AlertDialog
indicating whether the player won or lost, the number of shots fired and the total time elapsed. Lines 419–430 call the Builder
’s setPositiveButton
method to create a reset button. The onClick
method of the button’s listener indicates that the dialog is no longer displayed and calls newGame
to set up and start a new game. A dialog must be displayed from the GUI thread, so lines 432–440 call Activity
method runOnUiThread
and pass it an object of an anonymous inner class that implements Runnable
. The Runnable
’s run
method indicates that the dialog is displayed and then displays it.
407 // display an AlertDialog when the game ends
408 private void showGameOverDialog(int messageId)
409 {
410 // create a dialog displaying the given String
411 final AlertDialog.Builder dialogBuilder =
412 new AlertDialog.Builder(getContext());
413 dialogBuilder.setTitle(getResources().getString(messageId));
414 dialogBuilder.setCancelable(false);
415
416 // display number of shots fired and total time elapsed
417 dialogBuilder.setMessage(getResources().getString(
418 R.string.results_format, shotsFired, totalElapsedTime));
419 dialogBuilder.setPositiveButton(R.string.reset_game,
420 new DialogInterface.OnClickListener()
421 {
422 // called when "Reset Game" Button is pressed
423 @Override
424 public void onClick(DialogInterface dialog, int which)
425 {
426 dialogIsDisplayed = false;
427 newGame(); // set up and start a new game
428 } // end method onClick
429 } // end anonymous inner class
430 ); // end call to setPositiveButton
431
432 activity.runOnUiThread(
433 new Runnable() {
434 public void run()
435 {
436 dialogIsDisplayed = true;
437 dialogBuilder.show(); // display the dialog
438 } // end method run
439 } // end Runnable
440 ); // end call to runOnUiThread
441 } // end method showGameOverDialog
442
CannonView
Methods stopGame
and releaseResources
Activity
class CannonGame
’s onPause
and onDestroy
methods (Fig. 7.8) call class CannonView
’s stopGame
and releaseResources
methods (Fig. 7.21), respectively. Method stopGame
(lines 444–448) is called from the main Activity
to stop the game when the Activity
’s onPause
method is called—for simplicity, we don’t store the game’s state in this example. Method releaseResources
(lines 451–455) calls the SoundPool
’s release
method to release the resources associated with the SoundPool
.
443 // pauses the game
444 public void stopGame()
445 {
446 if (cannonThread != null)
447 cannonThread.setRunning(false);
448 } // end method stopGame
449
450 // releases resources; called by CannonGame's onDestroy method
451 public void releaseResources()
452 {
453 soundPool.release(); // release all resources used by the SoundPool
454 soundPool = null;
455 } // end method releaseResources
456
SurfaceHolder.Callback
MethodsFigure 7.22 implements the surfaceChanged
, surfaceCreated
and surfaceDestroyed
methods of interface SurfaceHolder.Callback
. Method surfaceChanged
has an empty body in this app because the app is always displayed in portrait view. This method is called when the SurfaceView
’s size or orientation changes, and would typically be used to redisplay graphics based on those changes. Method surfaceCreated
(lines 465–471) is called when the SurfaceView
is created—e.g., when the app first loads or when it resumes from the background. We use surfaceCreated
to create and start the CannonThread
to begin the game. Method surfaceDestroyed
(lines 474–492) is called when the SurfaceView
is destroyed—e.g., when the app terminates. We use the method to ensure that the CannonThread
terminates properly. First, line 479 calls CannonThread
’s setRunning
method with false
as an argument to indicate that the thread should stop, then lines 481–491 wait for the thead to terminate. This ensures that no attempt is made to draw to the SurfaceView
once surfaceDestroyed
completes execution.
457 // called when surface changes size
458 @Override
459 public void surfaceChanged(SurfaceHolder holder, int format,
460 int width, int height)
461 {
462 } // end method surfaceChanged
463
464 // called when surface is first created
465 @Override
466 public void surfaceCreated(SurfaceHolder holder)
467 {
468 cannonThread = new CannonThread(holder);
469 cannonThread.setRunning(true);
470 cannonThread.start(); // start the game loop thread
471 } // end method surfaceCreated
472
473 // called when the surface is destroyed
474 @Override
475 public void surfaceDestroyed(SurfaceHolder holder)
476 {
477 // ensure that thread terminates properly
478 boolean retry = true;
479 cannonThread.setRunning(false);
480
481 while (retry)
482 {
483 try
484 {
485 cannonThread.join();
486 retry = false;
487 } // end try
488 catch (InterruptedException e)
489 {
490 } // end catch
491 } // end while
492 } // end method surfaceDestroyed
493
CannonThread
: Using a Thread
to Create a Game LoopFigure 7.23 defines a subclass of Thread
which updates the game. The thread maintains a reference to the SurfaceView
’s SurfaceHolder
(line 497) and a boolean
indicating whether the thread is running. The class’s run
method (lines 514–543) drives the frame-by-frame animations—this is know as the game loop. Each update of the game elements on the screen is performed based on the number of milliseconds that have passed since the last update. Line 518 gets the system’s current time in milliseconds when the thread begins running. Lines 520–542 loop until threadIsRunning
is false
.
494 // Thread subclass to control the game loop
495 private class CannonThread extends Thread
496 {
497 private SurfaceHolder surfaceHolder; // for manipulating canvas
498 private boolean threadIsRunning = true; // running by default
499
500 // initializes the surface holder
501 public CannonThread(SurfaceHolder holder)
502 {
503 surfaceHolder = holder;
504 setName("CannonThread");
505 } // end constructor
506
507 // changes running state
508 public void setRunning(boolean running)
509 {
510 threadIsRunning = running;
511 } // end method setRunning
512
513 // controls the game loop
514 @Override
515 public void run()
516 {
517 Canvas canvas = null; // used for drawing
518 long previousFrameTime = System.currentTimeMillis();
519
520 while (threadIsRunning)
521 {
522 try
523 {
524 canvas = surfaceHolder.lockCanvas(null);
525
526 // lock the surfaceHolder for drawing
527 synchronized(surfaceHolder)
528 {
529 long currentTime = System.currentTimeMillis();
530 double elapsedTimeMS = currentTime - previousFrameTime;
531 totalElapsedTime += elapsedTimeMS / 1000.0;
532 updatePositions(elapsedTimeMS); // update game state
533 drawGameElements(canvas); // draw
534 previousFrameTime = currentTime; // update previous time
535 } // end synchronized block
536 } // end try
537 finally
538 {
539 if (canvas != null)
540 surfaceHolder.unlockCanvasAndPost(canvas);
541 } // end finally
542 } // end while
543 } // end method run
544 } // end nested class CannonThread
545 } // end class CannonView
First we must obtain the Canvas
for drawing on the SurfaceView
by calling SurfaceHolder
method lockCanvas
(line 524). Only one thread at a time can draw to a SurfaceView
, so we must first lock the SurfaceHolder
, which we do with a synchronized block. Next, we get the current time in milliseconds, then calculate the elapsed time and add that to the total time that has elapsed so far—this will be used to help display the amount of time left in the game. Line 532 calls method updatePositions
with the elapsed time in milliseconds as an argument—this moves all the game elements using the elapsed time to help scale the amount of movement. This helps ensure that the game operates at the same speed regardless of how fast the device is. If the time between frames is larger (i.e, the device is slower), the game elements will move further when each frame of the animation is displayed. If the time between frames is smaller (i.e, the device is faster), the game elements will move less when each frame of the animation is displayed. Finally, line 533 draws the game elements using the SurfaceView
’s Canvas
and line 534 stores the currentTime
as the previousFrameTime
to prepare to calculate the elapsed time in the next frame of the animation.
In this chapter, you created the Cannon Game app, which challenged the player to destroy a seven-piece target before a 10-second time limit expired. The user aimed the cannon by touching the screen. The cannon fired a cannonball when the user double-tapped the screen.
You learned how to define String
resources to represent the format String
s that are used in calls to class Resource
’s getString
method and class String
’s format
method, and how to number format specifiers for localization purposes. You created a custom view by extending class SurfaceView
and learned that custom component class names must be fully qualified in the XML layout element that represents the component.
We presented additional Activity
lifecycle methods. You learned that method onPause
is called for the current Activity
when another activity receives the focus and that method onDestroy
is called when the system shuts down an Activity
.
You handled touches and single taps by overriding Activity
’s onTouchEvent
method. To handle the double taps that fired the cannon, you used a GestureDetector
. You responded to the double tap event with a SimpleGestureListener
that contained an overridden onDoubleTap
method.
You added sound effects to the app’s res/raw
folder and managed them with a SoundPool
. You also used the system’s AudioManager
service to obtain the device’s current music volume and use it as the playback volume.
This app manually performed its animations by updating the game elements on a SurfaceView
from a separate thread of execution. To do this, extended class Thread
and created a run
method that displayed graphics with methods of class Canvas
. You used the SurfaceView
’s SurfaceHolder
to obtain the appropriate Canvas
. You also learned how to build a game loop that controls a game based on the amount of time that has elapsed between animation frames, so that the game will operate at the same overall speed on all devices.
The next chapter presents the SpotOn game app—our first Android 3.x app. SpotOn uses Android 3.x’s property animation to animate View
s that contain images. The app tests the user’s reflexes by animating multiple spots that must be touched before they disappear.
7.1. Fill in the blanks in each of the following statements:
a. You can create a custom view by extending class View
or __________.
b. To process simple touch events for an Activity
, you can override class Activity
’s onTouchEvent
method then use constants from class __________ (package android.view
) to test which type of event occurred and process it accordingly.
c. Each SurfaceView
subclass should implement the interface __________, which contains methods that are called when the SurfaceView
is created, changed (e.g., its size or orientation changes) or destroyed.
d. The d
in a format specifier indicates that you’re formatting a decimal integer and the f
in a format specifier indicates that you’re formatting a(n) __________ value.
e. Sound files are stored in the app’s __________ folder.
7.2. State whether each of the following is true or false. If false, explain why.
a. One use of onStop
is to suspend a game play so that it does not continue executing when the user cannot interact with it.
b. The Android documentation recommends that games use the music audio stream to play sounds.
c. In Android, it’s important to maximize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.
d. A Canvas
draws on a View
’s Bitmap
.
e. Format String
s that contain multiple format specifiers must number the format specifiers for localization purposes.
f. There are seven sound streams identified by constants in class AudioManager
, but the documentation for class SoundPool
recommends using the stream for playing music (AudioManager.STREAM_MUSIC
) for sound in games.
g. Custom component class names must be fully qualified in the XML layout element that represents the component.
a. one of its subclasses.
b. MotionEvent
.
c. SurfaceHolder.Callback
.
d. floating-point.
e. res/raw
.
a. False. One use of onPause
is to suspend a game play so that it does not continue executing when the user cannot interact with it.
b. True.
c. False. In Android, it’s important to minimize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.
d. True.
e. True.
f. True.
g. True.
7.3. Fill in the blanks in each of the following statements:
a. Method __________ is called for the current Activity
when another activity receives the focus, which sends the current activity to the background.
b. When an Activity
is shut down, its __________ method is called.
c. A(n) __________ allows an app to react to more sophisticated user interactions such as flings, double-taps, long presses and scrolls.
d. Activity
’s __________ method specifies that an app’s volume can be controlled with the device’s volume keys and should be the same as the device’s music playback volume. The method receives a constant from class AudioManager
(package android.media
).
e. Games often require complex logic that should be performed in separate threads of execution and those threads often need to draw to the screen. For such cases, Android provides class __________—a subclass of View
to which any thread can draw.
f. Method __________ is called for the current Activity
when another activity receives the focus.
7.4. State whether each of the following is true or false. If false, explain why.
a. Class SurfaceHolder
also provides methods that give a thread shared access to the Canvas
for drawing, because only one thread at a time can draw to a SurfaceView
.
b. A MotionEvent.ACTION_TOUCH
indicates that the user touched the screen and indicates that the user moved a finger across the screen (MotionEvent.ACTION_MOVE
).
c. When a View
is inflated, its constructor is called and passed a Context
and an AttributeSet
as arguments.
d. SoundPool
method start
receives three arguments—the application’s Context
, a resource ID representing the sound file to load and the sound’s priority.
e. When a game loop controls a game based on the amount of time that has elapsed between animation frames, the game will operate at different speeds as appropriate for each device.
7.5. (Enhanced Cannon Game App) Modify the Cannon Game app as follows:
a. Use images for the cannon base and cannonball.
b. Play a sound when the blocker hits the top or bottom of the screen.
c. Play a sound when the target hits the top or bottom of the screen.
d. Allow the user to aim and fire the cannon using a single tap.
e. Enhance the app to have nine levels. In each level, the target should have the same number of target pieces as the level.
f. Keep score. Increase the user’s score for each target piece hit by 10 times the current level. Decrease the score by 15 times the current level each time the user hits the blocker. Display the highest score on the screen in the upper-left corner.
g. Save the top five high scores in a SharedPreferences
file. When the game ends display an AlertDialog
with the scores shown in descending order. If the user’s score is one of the top five, highlight that score by displaying an asterisk (*) next to it.
h. Add an explosion animation each time the cannonball hits one of the target pieces.
i. Make the game more difficult as it progresses by increasing the speed of the target and the blocker.
j. Add multiplayer functionality allowing two users to play on the same device.
k. Increase the number of obstacles between the cannon and the target.
l. Allow the user to move the cannon up and down the screen before aiming and firing.
m. Add a bonus round that lasts for four seconds. Change the color of the target and add music to indicate that it is a bonus round. If the user hits a piece of the target during those four seconds, give the user 1000 bonus points.
7.6. (Brick Game App) Create a game similar to the cannon game that shoots pellets at a stationary brick wall. The goal is to destroy enough of the wall to shoot the moving target behind it. The faster you break through the wall and get the target, the higher your score. Vary the color of the bricks and the number of shots required to destroy each—for example, red bricks can be destroyed in three shots, yellow bricks can be destroyed in six shots, etc. Include multiple layers to the wall and a small moving target (e.g., an icon, animal, etc.). Keep score. Increase difficulty with each round by adding more layers to the wall and increasing the speed of the moving target.
7.7. (Tablet App: Multiplayer Horse Race with Cannon Game) One of the most popular carnival or arcade games is the horse race. Each player is assigned a horse. To move the horse, the players must perform a skill—such as shooting a stream of water at a target. Each time a player hits a target, that player’s horse moves forward. The goal is to hit the target as many times as possible and as quickly as possible to move the horse toward the finish line and win the race.
Create a multiplayer tablet app that simulates the Horse Race game with two players. Instead of a stream of water, use the Cannon Game as the skill that will move each horse. Each time a player hits a target piece with the cannonball, move that player’s horse one position to the right.
Set the orientation of the screen to landscape and target API level 11 (Android 3.0) or higher so the game runs on tablets. Split the screen into three sections. The first section should run across the entire width of the top of the screen; this will be the race track. Below the race track, include two sections side-by-side. In each of these sections, include separate Cannon Games. The two players will need to be sitting side-by-side to play this version of the game. (In a later chapter, you’ll learn how to use Bluetooth, which you can then use to allow players to compete from separate devices.)
In the race track, include two horses that start on the left and move right toward a finish line at the right-side of the screen. Number the horses “1” and “2.”
Include the many sounds of a traditional horse race. You can find free audios online at websites such as www.audiomicro.com/ or create your own. Before the race, play an audio of the traditional bugle call—the “Call to Post”—that signifies to the horses to take their mark. Include the sound of the shot to start the race, followed by the announcer saying “And they’re off!”
7.8. (Bouncing Ball Game App) Create a game app in which the user’s goal is to prevent a bouncing ball from falling off the bottom of the screen. When the user presses the start button, a ball bounces off the top, left and right sides (the “walls”) of the screen. A horizontal bar on the bottom of the screen serves as a paddle to prevent the ball from hitting the bottom of the screen. (The ball can bounce off the paddle, but not the bottom of the screen.) Allow the user to drag the paddle left and right. If the ball hits the paddle, it bounces up, and the game continues. If the ball hits the bottom, the game ends. Decrease the paddle’s width every 20 seconds and increase the speed of the ball to make the game more challenging. Consider adding obstacles at random locations.
7.9. (Digital Clock App) Create an app that displays a digital clock on the screen. Include alarmclock functionality.
7.10. (Analog Clock App) Create an app that displays an analog clock with hour, minute and second hands that move appropriately as the time changes.
7.11. (Fireworks Designer App) Create an app that enables the user to create a customized fireworks display. Create a variety of fireworks demonstrations. Then orchestrate the firing of the fireworks for maximum effect. You might synchronize your fireworks with audios or videos. You could overlay the fireworks on a picture.
7.12. (Animated Towers of Hanoi App) Every budding computer scientist must grapple with certain classic problems, and the Towers of Hanoi (see Fig. 7.24) is one of the most famous. Legend has it that in a temple in the Far East, priests are attempting to move a stack of disks from one peg to another. The initial stack has 64 disks threaded onto one peg and arranged from bottom to top by decreasing size. The priests are attempting to move the stack from this peg to a second peg under the constraints that exactly one disk is moved at a time and at no time may a larger disk be placed above a smaller disk. A third peg is available for temporarily holding disks. Supposedly, the world will end when the priests complete their task, so there’s little incentive for us to facilitate their efforts.
Let’s assume that the priests are attempting to move the disks from peg 1 to peg 3. We wish to develop an algorithm that will display the precise sequence of peg-to-peg disk transfers.
If we were to approach this problem with conventional methods, we would rapidly find ourselves hopelessly knotted up in managing the disks. Instead, if we attack the problem with recursion in mind, it immediately becomes tractable. Moving n disks can be viewed in terms of moving only n – 1 disks (hence the recursion) as follows:
a. Move n – 1 disks from peg 1 to peg 2, using peg 3 as a temporary holding area.
b. Move the last disk (the largest) from peg 1 to peg 3.
c. Move the n – 1 disks from peg 2 to peg 3, using peg 1 as a temporary holding area.
The process ends when the last task involves moving n = 1 disk (i.e., the base case). This task is accomplished by simply moving the disk, without the need for a temporary holding area.
Write an app to solve the Towers of Hanoi problem. Allow the user to enter the number of disks. Use a recursive Tower
method with four parameters:
a. the number of disks to be moved,
b. the peg on which these disks are initially threaded,
c. the peg to which this stack of disks is to be moved, and
d. the peg to be used as a temporary holding area.
Your app should display the precise instructions it will take to move the disks from the starting peg to the destination peg and should show animations of the disks moving from peg to peg. For example, to move a stack of three disks from peg 1 to peg 3, your app should display the following series of moves and the corresponding animations:
1 --> 3 (This notation means "Move one disk from peg 1 to peg 3.")
1 --> 2
3 --> 2
1 --> 3
2 --> 1
2 --> 3
1 --> 3
7.13. (DivvideAndConquer Game App: Open Source) Check out the open-source Android game, DivideAndConquer, on the Google Code site (apps-for-android.googlecode.com/svn/trunk/DivideAndConquer/). The goal of the game is to contain the bouncing balls by creating walls around them. Possible modifications and enhancements include:
a. Change the graphics.
b. Add sounds.
c. Add bonus rounds when the user hits a certain score.
d. Record the highest scores.
7.14. (Standup Timer App) Standup Timer is an open-source Android app that functions as a stop watch (github.com/jwood/standup-timer). Possible modifications and enhancements include:
a. Change the graphics.
b. Include digital and analog versions of a clock.
c. Allow the user to select from multiple sounds.
d. Give the user a warning signal before time is about to run out (either audible or visual).