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 for displaying the game’s graphics from a separate thread of execution.
Draw graphics using Paint
s and a Canvas
.
Override View
’s onTouchEvent
method to fire a cannonball when the user touches the screen.
Perform simple collision detection.
Add sound to your app using a SoundPool
and the AudioManager
.
Override Fragment
lifecycle method onDestroy
.
Use immersive mode to enable the game to occupy the entire screen, but still allow the user to access the system bars.
6.2 Test-Driving the Cannon Game App
6.3.1 Using the Resource Folder res/raw
6.3.2 Activity
and Fragment
Lifecycle Methods
6.3.3 Overriding View
Method onTouchEvent
6.3.4 Adding Sound with SoundPool
and AudioManager
6.3.5 Frame-by-Frame Animation with Thread
s, SurfaceView
and SurfaceHolder
6.3.6 Simple Collision Detection
6.4 Building the GUI and Resource Files
6.4.2 Adjusting the Theme to Remove the App Title and App Bar
6.4.5 Adding the Sounds to the App
6.4.6 Adding Class MainActivityFragment
6.4.7 Editing activity_main.xml
6.4.8 Adding the CannonView
to fragment_main.xml
6.5 Overview of This App’s Classes
6.6 MainActivity
Subclass of Activity
6.7 MainActivityFragment
Subclass of Fragment
6.8.1 Instance Variables and Constructor
6.8.2 Methods update
, draw
, and playSound
6.9 Blocker
Subclass of GameElement
6.10 Target
Subclass of GameElement
6.11.1 Instance Variables and Constructor
6.11.5 Methods getCannonball
and removeCannonball
6.12 Cannonball
Subclass of GameElement
6.12.1 Instance Variables and Constructor
6.12.2 Methods getRadius
, collidesWith
, isOnScreen
, and reverseVelocityX
6.13 CannonView
Subclass of SurfaceView
6.13.1 package
and import
Statements
6.13.2 Instance Variables and Constants
6.13.4 Overriding View
Method onSizeChanged
6.13.5 Methods getScreenWidth
, getScreenHeight
, and playSound
6.13.8 Method alignAndFireCannonball
6.13.9 Method showGameOverDialog
6.13.10 Method drawGameElements
6.13.11 Method testForCollisions
6.13.12 Methods stopGame
and releaseResources
6.13.13 Implementing the SurfaceHolder.Callback
Methods
6.13.14 Overriding View Method onTouchEvent
6.13.15 CannonThread
: Using a Thread
to Create a Game Loop
6.13.16 Methods hideSystemBars
and showSystemBars
Self-Review Exercises | Answers to Self-Review Exercises | Exercises
The Cannon Game1 app challenges you to destroy nine targets before a ten-second time limit expires (Fig. 6.1). The game consists of four types of visual components—a cannon that you control, a cannonball, nine targets and a blocker that defends the targets. You aim and fire the cannon by touching the screen—the cannon then aims at the touched point and fires the cannonball in a straight line in that direction.
1. We’d like to thank Prof. Hugues Bersini—author of a French-language object-oriented programming book for ditions Eyrolles, Secteur Informatique—for sharing with us his suggested refactoring of our original Cannon Game app. We used this as inspiration for our own refactoring in the latest versions of this app in this book and iOS® 8 for Programmers: An App-Driven Approach.
Each time you destroy a target, a three-second time bonus is added to your remaining time, and each time you hit the blocker, a two-second time penalty is subtracted from your remaining time. You win by destroying all nine target sections before you run out of time—if the timer reaches zero, you lose. At the end of the game, the app displays an AlertDialog
indicating whether you won or lost, and shows the number of shots fired and the elapsed time (Fig. 6.2).
When you fire the cannon, the game plays a firing sound. When a cannonball hits a target, a glass-breaking sound plays and that target disappears. When the cannonball hits the blocker, a hit sound plays and the cannonball bounces back. The blocker cannot be destroyed. Each of the targets and the blocker move vertically at different speeds, changing direction when they hit the top or bottom of the screen.
[Note: The Android Emulator performs slowly on some computers. For the best experience, you should test this app on an Android device. On a slow emulator, the cannonball will sometimes appear to pass through the blocker or targets.]
Open Android Studio and open the Cannon Game app from the CannonGame
folder in the book’s examples folder, then execute the app in the AVD or on a device. This builds the project and runs the app.
Tap the screen to aim and fire the cannon. You can fire a cannonball only if there is not another cannonball on the screen. If you’re running on an AVD, the mouse is your “finger.” Destroy all of the targets as fast as you can—the game ends if the timer runs out or you destroy all nine targets.
This section presents the new technologies that we use in the Cannon Game app in the order they’re encountered in the chapter.
Media files, such as the sounds used in the Cannon Game app, are placed in the app’s resource folder res/raw. Section 6.4.5 discusses how to create this folder. You’ll copy the app’s sound files into it.
We introduced Activity
and Fragment
lifecycle methods in Section 5.3.1. This app uses Fragment
lifecycle method onDestroy
. When an Activity
is shut down, its onDestroy method is called, which in turn calls the onDestroy methods of all the Fragment
s hosted by the Activity
. We use this method in the MainActivityFragment
to release the CannonView
’s sound resources.
Method onDestroy
is not guaranteed to be called, so it should be used only to release resources, not to save data. The Android documentation recommends that you save data in methods onPause
or onSaveInstanceState
.
Users interact with this app by touching the device’s screen. A touch aligns the cannon to face the touch point on the screen, then fires the cannon. To process simple touch events for the CannonView
, you’ll override View
method onTouchEvent (Section 6.13.14), then use constants from class MotionEvent
(package android.view
) to test which type of event occurred and process it accordingly.
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 audio streams for alarms, music, notifications, phone rings, system sounds, phone calls and more. You’ll configure and create a SoundPool
object using a SoundPool.Builder object. You’ll also use an AudioAttributes.Builder
object to create an AudioAttributes object that will be associated with the SoundPool
. We call the AudioAttributes
’s setUsage method to designate the audio as game audio. The Android documentation recommends that games use the music audio stream to play sounds, because that stream’s volume can be controlled via the device’s volume buttons. In addition, we use the Activity
’s setVolumeControlStream method to allow the game’s volume to be controlled with the device’s volume buttons. The method receives a constant from class AudioManager (package android.media
), which provides access to the device’s volume and phone-ringer controls.
This app performs its animations manually by updating the game elements from a separate thread of execution. To do this, we use a subclass of Thread
with a run
method that directs our custom CannonView
to update the positions of the game’s elements, then draws them. The run
method drives the frame-by-frame animations—this is known as the game loop.
All updates to an app’s user interface must be performed in the GUI thread of execution, because GUI components are not thread safe—updates performed outside the GUI thread can corrupt the GUI. Games, however, 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
that provides a dedicated drawing area in which other threads can display graphics on the screen in a thread-safe manner.
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.
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—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 6.13.11.
Game-development frameworks typically provide more sophisticated “pixel-perfect” collision-detection capabilities. Many such frameworks are available (free and fee-based) for developing the simplest 2D games to the most complex 3D console-style games (such as games for Sony’s PlayStation® and Microsoft’s Xbox®). Figure 6.3 lists a few game-development frameworks—there are dozens more. Many support multiple platforms, including Android and iOS. Some require C++ or other programming languages.
To immerse users in games, game developers often use full-screen themes, such as
Theme.Material.Light.NoActionBar.Fullscreen
that display only the bottom system bar. In landscape orientation on phones, that system bar appears at the screen’s right edge.
In Android 4.4 (KitKat), Google added support for full-screen immersive mode (Section 6.13.16), which enables an app to take advantage of the entire screen. When an app is in immersive mode, the user can swipe down from the top of the screen to display the system bars temporarily. If the user does not interact with the system bars, they disappear after a few seconds.
In this section, you’ll create the app’s resource files, GUI layout files and classes.
For this app, you’ll add a Fragment
and its layout manually—much of the autogenerated code in the Blank Activity template with a Fragment
is not needed in the Cannon Game. Create a new project using the Empty Activity template. In the Create New Project dialog’s New Project step, specify
• Application name: Cannon Game
• Company Domain: deitel.com
(or specify your own domain name)
In the layout editor, select Nexus 6 from the virtual-device drop-down list (Fig. 2.11). Once again, we’ll use this device as the basis for our design. Also, delete the Hello world! TextView
from activity_main.xml
. As you’ve done previously, add an app icon to your project.
The Cannon game is designed for only landscape orientation. Follow the steps you performed in Section 3.7 to set the screen orientation, but this time set android:screenOrientation
to landscape
rather than portrait
.
As we noted in Section 6.3.7, game developers often use full-screen themes, such as
Theme.Material.Light.NoActionBar.Fullscreen
that display only the bottom system bar, which in landscape orientation appears at the screen’s right edge. The AppCompat
themes do not include a full-screen theme by default, but you can modify the app’s theme to achieve this. To do so:
1. Open styles.xml
.
2. Add the following lines to the <style>
element:
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
The first line indicates that the title (usually the app’s name) should not be displayed. The second indicates that the app bar should not be displayed. The last line indicates that the app should use the full screen.
You created String
resources in earlier chapters, so we show here only a table of the String
resource names and corresponding values (Fig. 6.4). Double click strings.xml
in the res/values
folder, then click the Open editor link to display the Translations Editor for creating these String
resources.
This app draws targets of alternating colors on the Canvas
. For this app, we added the following dark blue and yellow color resources to colors.xml
:
<color name="dark">#1976D2</color>
<color name="light">#FFE100</color>
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 > Android resource directory, to open the New Resource Directory dialog
2. In the Resource type drop-down, select raw
. The Directory name will automatically change to raw
.
3. Click OK to create the folder.
4. Copy and paste the sound files into the res/raw
folder. In the Copy dialog that appears, click OK.
Next, you’ll add class MainActivityFragment
to the project:
1. In the Project window, right click the com.deitel.cannongame node and select New > Fragment > Fragment (Blank).
2. For Fragment Name specify MainActivityFragment
and for Fragment Layout Name specify fragment_main
.
3. Uncheck the checkboxes for Include fragment factory methods? and Include interface callbacks?
By default, fragment_main.xml
contains a FrameLayout
that displays a TextView
. A FrameLayout is designed to display one View
, but can also be used to layer views. Remove the TextView
—in this app, the FrameLayout
will display the CannonView
.
In this app, MainActivity
’s layout displays only MainActivityFragment
. Edit the layout as follows:
1. Open activity_main.xml
in the layout editor and switch to the Text tab.
2. Change RelativeLayout
to fragment
and remove the padding properties so that the fragment
element will fill the entire screen.
3. Switch to Design view, select fragment in the Component Tree, then set the id to fragment
.
4. Set the name to com.deitel.cannongame.MainActivityFragment
—rather than typing this, you can click the ellipsis button to the right of the name property’s value field, then select the class from the Fragments dialog that appears.
Recall that the layout editor’s Design view can show a preview of a fragment displayed in a particular layout. If you do not specify which fragment to preview in MainActivity
’s layout, the layout editor displays a "Rendering Problems" message. To specify the fragment to preview, right click the fragment—either in Design view or in the Component Tree and click Choose Preview Layout.... Then, in the Resources dialog, select the name of the fragment layout.
You’ll now add the CannonView
to fragment_main.xml
. You first must create CannonView.java
, so that you can select class CannonView
when placing a CustomView in the layout. Follow these steps to create CannonView.java
and add the CannonView
to the layout:
1. Expand the java
folder in the Project window.
2. Right click package com.deitel.cannongame
’s folder, then select New > Java Class.
3. In the Create New Class dialog that appears, enter CannonView
in the Name field, then click OK. The file will open in the editor automatically.
4. In CannonView.java
, indicate that CannonView
extends SurfaceView
. If the import
statement for the android.view.SurfaceView
class does not appear, place the cursor at the end of the class name SurfaceView
. Click the red bulb menu () that appears above the beginning of the line and select Import Class.
5. Place the cursor at the end of SurfaceView
if you have not already done so. Click the red bulb menu that appears and select Create constructor matching super. Choose the two-argument constructor in the list in the Choose Super Class Constructors dialog that appears, then click OK. The IDE will add the constructor to the file automatically.
6. Switch back to fragment_main.xml
’s Design view in the layout editor.
7. Click CustomView in the Custom section of the Palette.
8. In the Views dialog that appears, select CannonView (com.deitel.cannongame)
, then click OK.
9. Hover over and click the FrameLayout
in the Component Tree. The view (CustomView)—which is a CannonView
—should appear in the Component Tree within the FrameLayout
.
10. Ensure that view (CustomView) is selected in the Component Tree window. In the Properties window, set layout:width and layout:height to match_parent
.
11. In the Properties window, change the id from view
to cannonView
.
12. Save and close fragment_main.xml
.
This app consists of eight classes:
• MainActivity
(the Activity
subclass; Section 6.6)—Hosts the MainActivityFragment
.
• MainActivityFragment
(Section 6.7)—Displays the CannonView
.
• GameElement
(Section 6.8)—The superclass for items that move up and down (Blocker
and Target
) or across (Cannonball
) the screen.
• Blocker
(Section 6.9)—Represents a blocker, which makes destroying targets more challenging.
• Target
(Section 6.10)—Represents a target that can be destroyed by a cannonball.
• Cannon
(Section 6.11)—Represents the cannon, which fires a cannonball each time the user touches the screen.
• Cannonball
(Section 6.12)—Represents a cannonball that the cannon fires when the user touches the screen.
• CannonView
(Section 6.13)—Contains the game’s logic and coordinates the behaviors of the Blocker
, Target
s, Cannonball
and Cannon
.
You must create the classes GameElement
, Blocker
, Target
, Cannonball
and Cannon
. For each class, right click the package folder com.deitel.cannongame
in the project’s app/ java
folder and select New > Java Class. In the Create New Class dialog, enter the name of the class in the Name field and click OK.
Class MainActivity
(Fig. 6.5) is the host for the Cannon Game app’s MainActivityFragment
. In this app, we override only the Activity
method onCreate
, which inflates the GUI. We deleted the autogenerated MainActivity
methods that managed its menu, because the menu is not used in this app.
1 // MainActivity.java
2 // MainActivity displays the MainActivityFragment
3 package com.deitel.cannongame;
4
5 import android.support.v7.app.AppCompatActivity;
6 import android.os.Bundle;
7
8 public class MainActivity extends AppCompatActivity {
9 // called when the app first launches
10 @Override
11 protected void onCreate(Bundle savedInstanceState) {
12 super.onCreate(savedInstanceState);
13 setContentView(R.layout.activity_main);
14 }
15 }
Class MainActivityFragment
(Fig. 6.6) overrides four Fragment
methods:
• onCreateView
(lines 17–28)—As you learned in Section 4.3.3, this method is called after a Fragment
’s onCreate
method to build and return a View
containing the Fragment
’s GUI. Lines 22–23 inflate the GUI. Line 26 gets a reference to the MainActivityFragment
’s CannonView
so that we can call its methods.
• onActivityCreated
(lines 31–37)—This method is called after the Fragment
’s host Activity
is created. Line 36 calls the Activity
’s setVolumeControlStream
method to allow the game’s volume to be controlled by the device’s volume buttons. There are seven sound streams identified by AudioManager
constants, but the music stream (AudioManager.STREAM_MUSIC
) is recommended for sound in games, because this stream’s volume can be controlled via the device’s buttons.
• onPause
(lines 40–44)—When the MainActivity
is sent to the background (and thus, paused), MainActivityFragment
’s onPause
method executes. Line 43 calls the CannonView
’s stopGame
method (Section 6.13.12) to stop the game loop.
• onDestroy
(lines 47–51)—When the MainActivity
is destroyed, its onDestroy
method calls MainActivityFragment
’s onDestroy
. Line 50 calls the CannonView
’s releaseResources
method to release the sound resources (Section 6.13.12).
1 // MainActivityFragment.java
2 // MainActivityFragment creates and manages a CannonView
3 package com.deitel.cannongame;
4
5 import android.media.AudioManager;
6 import android.os.Bundle;
7 import android.support.v4.app.Fragment;
8 import android.view.LayoutInflater;
9 import android.view.View;
10 import android.view.ViewGroup;
11
12 public class MainActivityFragment extends Fragment {
13 private CannonView cannonView; // custom view to display the game
14
15 // called when Fragment's view needs to be created
16 @Override
17 public View onCreateView(LayoutInflater inflater, ViewGroup container,
18 Bundle savedInstanceState) {
19 super.onCreateView(inflater, container, savedInstanceState);
20
21 // inflate the fragment_main.xml layout
22 View view =
23 inflater.inflate(R.layout.fragment_main, container, false);
24
25 // get a reference to the CannonView
26 cannonView = (CannonView) view.findViewById(R.id.cannonView);
27 return view;
28 }
29
30 // set up volume control once Activity is created
31 @Override
32 public void onActivityCreated(Bundle savedInstanceState) {
33 super.onActivityCreated(savedInstanceState);
34
35 // allow volume buttons to set game volume
36 getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
37 }
38
39 // when MainActivity is paused, terminate the game
40 @Override
41 public void onPause() {
42 super.onPause();
43 cannonView.stopGame(); // terminates the game
44 }
45
46 // when MainActivity is paused, MainActivityFragment releases resources
47 @Override
48 public void onDestroy() {
49 super.onDestroy();
50 cannonView.releaseResources();
51 }
52 }
Class GameElement
(Fig. 6.7)—the superclass of the Blocker
, Target
and Cannonball
—contains the common data and functionality of an object that moves in the Cannon Game app.
1 // GameElement.java
2 // Represents a rectangle-bounded game element
3 package com.deitel.cannongame;
4
5 import android.graphics.Canvas;
6 import android.graphics.Paint;
7 import android.graphics.Rect;
8
9 public class GameElement {
10 protected CannonView view; // the view that contains this GameElement
11 protected Paint paint = new Paint(); // Paint to draw this GameElement
12 protected Rect shape; // the GameElement's rectangular bounds
13 private float velocityY; // the vertical velocity of this GameElement
14 private int soundId; // the sound associated with this GameElement
15
16 // public constructor
17 public GameElement(CannonView view, int color, int soundId, int x,
18 int y, int width, int length, float velocityY) {
19 this.view = view;
20 paint.setColor(color);
21 shape = new Rect(x, y, x + width, y + length); // set bounds
22 this.soundId = soundId;
23 this.velocityY = velocityY;
24 }
25
26 // update GameElement position and check for wall collisions
27 public void update(double interval) {
28 // update vertical position
29 shape.offset(0, (int) (velocityY * interval));
30
31 // if this GameElement collides with the wall, reverse direction
32 if (shape.top < 0 && velocityY < 0 ||
33 shape.bottom > view.getScreenHeight() && velocityY > 0)
34 velocityY *= -1; // reverse this GameElement's velocity
35 }
36
37 // draws this GameElement on the given Canvas
38 public void draw(Canvas canvas) {
39 canvas.drawRect(shape, paint);
40 }
41
42 // plays the sound that corresponds to this type of GameElement
43 public void playSound() {
44 view.playSound(soundId);
45 }
46 }
The GameElement
constructor receives a reference to the CannonView
(Section 6.13), which implements the game’s logic and draws the game elements. The constructor receives an int
representing the GameElement
’s 32-bit color, and an int
representing the ID of a sound that’s associated with this GameElement
. The CannonView
stores all of the sounds in the game and provides an ID for each. The constructor also receives
• int
s for the x
and y
position of the GameElement
’s upper-left corner
• int
s for its width
and height
, and
• an initial vertical velocity, velocityY
, of this GameElement
.
Line 20 sets the paint
object’s color, using the int
representation of the color passed to the constructor. Line 21 calculates the GameElement
’s bounds and stores them in a Rect
object that represents a rectangle.
A GameElement
has the following methods:
• update
(lines 27–35)—In each iteration of the game loop, this method is called to update the GameElement
’s position. Line 29 updates the vertical position of shape
, based on the vertical velocity (velocityY
) and the elapsed time between calls to update
, which the method receives as the parameter interval
. Lines 32–34 check whether this GameElement
is colliding with the top or bottom edge of the screen and, if so, reverse its vertical velocity.
• draw
(lines 38–40)—This method is called when a GameElement
needs to be redrawn on the screen. The method receives a Canvas
and draws this GameElement
as a rectangle on the screen—we’ll override this method in class Cannonball
to draw a circle instead. The GameElement
’s paint
instance variable specifies the rectangle’s color, and the GameElement
’s shape
specifies the rectangle’s bounds on the screen.
• playSound
(lines 43–45)—Every game element has an associated sound that can be played by calling method playSound
. This method passes the value of the soundId
instance variable to the CannonView
’s playSound
method. Class CannonView
loads and maintains references to the game’s sounds.
Class Blocker
(Fig. 6.8)—a subclass of GameElement
—represents the blocker, which makes it more difficult for the player to destroy targets. Class Blocker
’s missPenalty
is subtracted from the remaining game time if the Cannonball
collides with the Blocker
. The getMissPenalty
method (lines 17–19) returns the missPenalty
—this method is called from CannonView
’s testForCollisions
method when subtracting the missPenalty
from the remaining time (Section 6.13.11). The Blocker
constructor (lines 9–14) passes its arguments and the ID for the blocker-hit sound (CannonView.BLOCKER_SOUND_ID
) to the superclass constructor (line 11), then initializes missPenalty
.
1 // Blocker.java
2 // Subclass of GameElement customized for the Blocker
3 package com.deitel.cannongame;
4
5 public class Blocker extends GameElement {
6 private int missPenalty; // the miss penalty for this Blocker
7
8 // constructor
9 public Blocker(CannonView view, int color, int missPenalty, int x,
10 int y, int width, int length, float velocityY) {
11 super(view, color, CannonView.BLOCKER_SOUND_ID, x, y, width, length,
12 velocityY);
13 this.missPenalty = missPenalty;
14 }
15
16 // returns the miss penalty for this Blocker
17 public int getMissPenalty() {
18 return missPenalty;
19 }
20 }
Class Target
(Fig. 6.9)—a subclass of GameElement
—represents a target that the player can destroy. Class Target
’s hitPenalty
is added to the remaining game time if the Cannonball
collides with a Target
. The getHitReward
method (lines 17–19) returns the hitReward
—this method is called from CannonView
’s testForCollisions
method when adding the hitReward
to the remaining time (Section 6.13.11). The Target
constructor (lines 9–14) passes its arguments and the ID for the target-hit sound (CannonView.TARGET_SOUND_ID
) to the super constructor (line 11), then initializes hitReward
.
1 // Target.java
2 // Subclass of GameElement customized for the Target
3 package com.deitel.cannongame;
4
5 public class Target extends GameElement {
6 private int hitReward; // the hit reward for this target
7
8 // constructor
9 public Target(CannonView view, int color, int hitReward, int x, int y,
10 int width, int length, float velocityY) {
11 super(view, color, CannonView.TARGET_SOUND_ID, x, y, width, length,
12 velocityY);
13 this.hitReward = hitReward;
14 }
15
16 // returns the hit reward for this Target
17 public int getHitReward() {
18 return hitReward;
19 }
20 }
The Cannon
class (Figs. 6.10–6.14) represents the cannon in the Cannon Game app. The cannon has a base and a barrel, and it can fire a cannonball.
The Cannon
constructor (Fig. 6.10) has four parameters. It receives
• the CannonView
that this Cannon
is in (view
),
• the radius of the Cannon
’s base (baseRadius
),
• the length of the Cannon
’s barrel (barrelLength
) and
• the width of the Cannon
’s barrel (barrelWidth
).
Line 25 sets the width of the Paint
object’s stroke so that the barrel will be drawn with the given barrelWidth
. Line 27 aligns the Cannon
’s barrel to be initially parallel with the top and bottom edges of the screen. The Cannon
class has a Point barrelEnd
that’s used to draw the barrel, barrelAngle
to store the current angle of the barrel, and cannonball
to store the Cannonball
that was most recently fired if it’s still on the screen.
1 // Cannon.java
2 // Represents Cannon and fires the Cannonball
3 package com.deitel.cannongame;
4
5 import android.graphics.Canvas;
6 import android.graphics.Color;
7 import android.graphics.Paint;
8 import android.graphics.Point;
9
10 public class Cannon {
11 private int baseRadius; // Cannon base's radius
12 private int barrelLength; // Cannon barrel's length
13 private Point barrelEnd = new Point(); // endpoint of Cannon's barrel
14 private double barrelAngle; // angle of the Cannon's barrel
15 private Cannonball cannonball; // the Cannon's Cannonball
16 private Paint paint = new Paint(); // Paint used to draw the cannon
17 private CannonView view; // view containing the Cannon
18
19 // constructor
20 public Cannon(CannonView view, int baseRadius, int barrelLength,
21 int barrelWidth) {
22 this.view = view;
23 this.baseRadius = baseRadius;
24 this.barrelLength = barrelLength;
25 paint.setStrokeWidth(barrelWidth); // set width of barrel
26 paint.setColor(Color.BLACK); // Cannon's color is Black
27 align(Math.PI / 2); // Cannon barrel facing straight right
28 }
29
Method align
(Fig. 6.11) aims the cannon. The method receives as an argument the barrel angle in radians. We use the cannonLength
and the barrelAngle
to determine the x- and y-coordinate values for the endpoint of the cannon’s barrel, barrelEnd
—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. Line 32 stores the barrelAngle
so that the ball can be fired at angle
later.
30 // aligns the Cannon's barrel to the given angle
31 public void align(double barrelAngle) {
32 this.barrelAngle = barrelAngle;
33 barrelEnd.x = (int) (barrelLength * Math.sin(barrelAngle));
34 barrelEnd.y = (int) (-barrelLength * Math.cos(barrelAngle)) +
35 view.getScreenHeight() / 2;
36 }
37
The fireCannonball
method (Fig. 6.12) fires a Cannonball
across the screen at the Cannon
’s current trajectory (barrelAngle
). Lines 41–46 calculate the horizontal and vertical components of the Cannonball
’s velocity. Lines 49–50 calculate the radius of the Cannonball
, which is CannonView.CANNONBALL_RADIUS_PERCENT
of the screen height. Lines 53–56 “load the cannon” (that is, construct a new Cannonball
and position it inside the Cannon
). Finally, we play the Cannonball
’s firing sound (line 58).
38 // creates and fires Cannonball in the direction Cannon points
39 public void fireCannonball() {
40 // calculate the Cannonball velocity's x component
41 int velocityX = (int) (CannonView.CANNONBALL_SPEED_PERCENT *
42 view.getScreenWidth() * Math.sin(barrelAngle));
43
44 // calculate the Cannonball velocity's y component
45 int velocityY = (int) (CannonView.CANNONBALL_SPEED_PERCENT *
46 view.getScreenWidth() * -Math.cos(barrelAngle));
47
48 // calculate the Cannonball's radius
49 int radius = (int) (view.getScreenHeight() *
50 CannonView.CANNONBALL_RADIUS_PERCENT);
51
52 // construct Cannonball and position it in the Cannon
53 cannonball = new Cannonball(view, Color.BLACK,
54 CannonView.CANNON_SOUND_ID, -radius,
55 view.getScreenHeight() / 2 - radius, radius, velocityX,
56 velocityY);
57
58 cannonball.playSound(); // play fire Cannonball sound
59 }
60
The draw
method (Fig. 6.13) draws the Cannon
on the screen. We draw the Cannon
in two parts. First we draw the Cannon
’s barrel, then the Cannon
’s base.
61 // draws the Cannon on the Canvas
62 public void draw(Canvas canvas) {
63 // draw cannon barrel
64 canvas.drawLine(0, view.getScreenHeight() / 2, barrelEnd.x,
65 barrelEnd.y, paint);
66
67 // draw cannon base
68 canvas.drawCircle(0, (int) view.getScreenHeight() / 2,
69 (int) baseRadius, paint);
70 }
71
We use Canvas
’s drawLine method to display the Cannon
barrel (lines 64–65). 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 its thickness. Recall that paint
was configured to draw the barrel with the thickness given in the constructor (Fig. 6.10, line 25).
Lines 68–69 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
.
Figure 6.14 shows the getCannonball
and removeCannonball
methods. The getCannonball
method (lines 73–75) returns the current Cannonball
instance, which Cannon
stores. A cannonball
value of null
means that currently no Cannonball
exists in the game. The CannonView
uses this method to avoid firing a Cannonball
if another Cannonball
is already on the screen (Section 6.13.8, Fig. 6.26). The removeCannonball
method (lines 78–80 of Fig. 6.14) removes the CannnonBall
from the game by setting cannonball
to null
. The CannonView
uses this method to remove the Cannonball
from the game when it destroys a Target
or after it leaves the screen (Section 6.13.11, Fig. 6.29).
72 // returns the Cannonball that this Cannon fired
73 public Cannonball getCannonball() {
74 return cannonball;
75 }
76
77 // removes the Cannonball from the game
78 public void removeCannonball() {
79 cannonball = null;
80 }
81 }
The Cannonball
subclass of GameElement
(Sections 6.12.1–6.12.4) represents a cannonball fired from the cannon.
The Cannonball
constructor (Fig. 6.15) receives the cannonball’s radius
rather than width
and height
in the GameElement
constructor. Lines 15–16 call super
with width
and height
values calculated from the radius
. The constructor also receives the horizontal velocity of the Cannonball
, velocityX
, in addition to its vertical velocity, velocityY
. Line 18 initializes onScreen
to true
because the Cannonball
is initially on the screen.
1 // Cannonball.java
2 // Represents the Cannonball that the Cannon fires
3 package com.deitel.cannongame;
4
5 import android.graphics.Canvas;
6 import android.graphics.Rect;
7
8 public class Cannonball extends GameElement {
9 private float velocityX;
10 private boolean onScreen;
11
12 // constructor
13 public Cannonball(CannonView view, int color, int soundId, int x,
14 int y, int radius, float velocityX, float velocityY) {
15 super(view, color, soundId, x, y,
16 2 * radius, 2 * radius, velocityY);
17 this.velocityX = velocityX;
18 onScreen = true;
19 }
20
Method getRadius
(Fig. 6.16, lines 22–24) returns the Cannonball
’s radius by finding half the distance between the shape.right
and shape.left
bounds of the Cannonball
’s shape
. Method isOnScreen
(lines 32–34) returns true
if the Cannonball
is on the screen.
21 // get Cannonball's radius
22 private int getRadius() {
23 return (shape.right - shape.left) / 2;
24 }
25
26 // test whether Cannonball collides with the given GameElement
27 public boolean collidesWith(GameElement element) {
28 return (Rect.intersects(shape, element.shape) && velocityX > 0);
29 }
30
31 // returns true if this Cannonball is on the screen
32 public boolean isOnScreen() {
33 return onScreen;
34 }
35
36 // reverses the Cannonball's horizontal velocity
37 public void reverseVelocityX() {
38 velocityX *= -1;
39 }
40
The collidesWith
method (line 27–29) checks whether the cannonball has collided with the given GameElement
. We perform simple collision detection, based on the rectangular boundary of the Cannonball
. Two conditions must be met if the Cannonball
is colliding with the GameElement
:
• The Cannonball
’s bounds, which are stored in the shape Rect
, must intersect the bounds of the given GameElement
’s shape
. Rect
’s intersects
method is used to check if the bounds of the Cannonball
and the given GameElement
intersect.
• The Cannonball
must be moving horizontally towards the given GameElement
. The Cannonball
travels from left to right (unless it hits the blocker). If velocityX
(the horizontal velocity) is positive, the Cannonball
is moving left-to-right toward the given GameElement
.
The reverseVelocityX
method reverses the horizontal velocity of the Cannonball
by multiplying velocityX
by -1
. If the collidesWith
method returns true
, CannonView
method testForCollisions
calls reverseVelocityX
to reverse the ball’s horizontal velocity, so the cannonball bounces back toward the cannon (Section 6.13.11).
The update
method (Fig. 6.17) first calls the superclass’s update
method (line 44) to update the Cannonball
’s vertical velocity and to check for vertical collisions. Line 47 uses Rect
’s offset method to horizontally translate the bounds of this Cannonball
. We multiply its horizontal velocity (velocityX
) by the amount of time that passed (interval
) to determine the translation amount. Lines 50–53 set onScreen
to false
if the Cannonball
hits one of the screen’s edges.
41 // updates the Cannonball's position
42 @Override
43 public void update(double interval) {
44 super.update(interval); // updates Cannonball's vertical position
45
46 // update horizontal position
47 shape.offset((int) (velocityX * interval), 0);
48
49 // if Cannonball goes off the screen
50 if (shape.top < 0 || shape.left < 0 ||
51 shape.bottom > view.getScreenHeight() ||
52 shape.right > view.getScreenWidth())
53 onScreen = false; // set it to be removed
54 }
55
The draw
method (Fig. 6.18) overrides GameElement
’s draw
method and uses 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.
56 // draws the Cannonball on the given canvas
57 @Override
58 public void draw(Canvas canvas) {
59 canvas.drawCircle(shape.left + getRadius(),
60 shape.top + getRadius(), getRadius(), paint);
61 }
62 }
Class CannonView
(Figs. 6.19–6.33) is a custom subclass of View
that implements the Cannon Game’s logic and draws game objects on the screen.
Figure 6.19 lists the package
statement and the import
statements for class CannonView
. Section 6.3 discussed the key new classes and interfaces that class CannonView
uses. We’ve highlighted them in Fig. 6.19.
1 // CannonView.java
2 // Displays and controls the Cannon Game
3 package com.deitel.cannongame;
4
5 import android.app.Activity;
6 import android.app.AlertDialog;
7 import android.app.Dialog;
8 import android.app.DialogFragment;
9 import android.content.Context;
10 import android.content.DialogInterface;
11 import android.graphics.Canvas;
12 import android.graphics.Color;
13 import android.graphics.Paint;
14 import android.graphics.Point;
15 import android.media.AudioAttributes;
16 import android.media.SoundPool;
17 import android.os.Build;
18 import android.os.Bundle;
19 import android.util.AttributeSet;
20 import android.util.Log;
21 import android.util.SparseIntArray;
22 import android.view.MotionEvent;
23 import android.view.SurfaceHolder;
24 import android.view.SurfaceView;
25 import android.view.View;
26
27 import java.util.ArrayList;
28 import java.util.Random;
29
30 public class CannonView extends SurfaceView
31 implements SurfaceHolder.Callback {
32
Figure 6.20 lists the large number of class CannonView
’s constants and instance variables. We’ll explain each as we encounter it in the discussion. Many of the constants are used in calculations that scale the game elements’ sizes based on the screen’s dimensions.
33 private static final String TAG = "CannonView"; // for logging errors
34
35 // constants for game play
36 public static final int MISS_PENALTY = 2; // seconds deducted on a miss
37 public static final int HIT_REWARD = 3; // seconds added on a hit
38
39 // constants for the Cannon
40 public static final double CANNON_BASE_RADIUS_PERCENT = 3.0 / 40;
41 public static final double CANNON_BARREL_WIDTH_PERCENT = 3.0 / 40;
42 public static final double CANNON_BARREL_LENGTH_PERCENT = 1.0 / 10;
43
44 // constants for the Cannonball
45 public static final double CANNONBALL_RADIUS_PERCENT = 3.0 / 80;
46 public static final double CANNONBALL_SPEED_PERCENT = 3.0 / 2;
47
48 // constants for the Targets
49 public static final double TARGET_WIDTH_PERCENT = 1.0 / 40;
50 public static final double TARGET_LENGTH_PERCENT = 3.0 / 20;
51 public static final double TARGET_FIRST_X_PERCENT = 3.0 / 5;
52 public static final double TARGET_SPACING_PERCENT = 1.0 / 60;
53 public static final double TARGET_PIECES = 9;
54 public static final double TARGET_MIN_SPEED_PERCENT = 3.0 / 4;
55 public static final double TARGET_MAX_SPEED_PERCENT = 6.0 / 4;
56
57 // constants for the Blocker
58 public static final double BLOCKER_WIDTH_PERCENT = 1.0 / 40;
59 public static final double BLOCKER_LENGTH_PERCENT = 1.0 / 4;
60 public static final double BLOCKER_X_PERCENT = 1.0 / 2;
61 public static final double BLOCKER_SPEED_PERCENT = 1.0;
62
63 // text size 1/18 of screen width
64 public static final double TEXT_SIZE_PERCENT = 1.0 / 18;
65
66 private CannonThread cannonThread; // controls the game loop
67 private Activity activity; // to display Game Over dialog in GUI thread
68 private boolean dialogIsDisplayed = false;
69
70 // game objects
71 private Cannon cannon;
72 private Blocker blocker;
73 private ArrayList<Target> targets;
74
75 // dimension variables
76 private int screenWidth;
77 private int screenHeight;
78
79 // variables for the game loop and tracking statistics
80 private boolean gameOver; // is the game over?
81 private double timeLeft; // time remaining in seconds
82 private int shotsFired; // shots the user has fired
83 private double totalElapsedTime; // elapsed seconds
84
85 // constants and variables for managing sounds
86 public static final int TARGET_SOUND_ID = 0;
87 public static final int CANNON_SOUND_ID = 1;
88 public static final int BLOCKER_SOUND_ID = 2;
89 private SoundPool soundPool; // plays sound effects
90 private SparseIntArray soundMap; // maps IDs to SoundPool
91
92 // Paint variables used when drawing each item on the screen
93 private Paint textPaint; // Paint used to draw text
94 private Paint backgroundPaint; // Paint used to clear the drawing area
95
Figure 6.21 shows class CannonView
’s constructor. When a View
is inflated, its constructor is called with a Context
and an AttributeSet
as arguments. The Context
is the Activity
that displays the MainActivityFragment
containing the CannonView
, and the AttributeSet (package android.util
) contains the CannonView
attribute values that are set in the layout’s XML document. These arguments are passed to the superclass constructor (line 96) to ensure that the custom View
is properly configured with the values of any standard View
attributes specified in the XML. Line 99 stores a reference to the MainActivity
so we can use it at the end of a game to display an AlertDialog
from the GUI thread. Though we chose to store the Activity
reference, we can access this at any time by calling the inherited View
method getContext
.
96 // constructor
97 public CannonView(Context context, AttributeSet attrs) {
98 super(context, attrs); // call superclass constructor
99 activity = (Activity) context; // store reference to MainActivity
100
101 // register SurfaceHolder.Callback listener
102 getHolder().addCallback(this);
103
104 // configure audio attributes for game audio
105 AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
106 attrBuilder.setUsage(AudioAttributes.USAGE_GAME);
107
108 // initialize SoundPool to play the app's three sound effects
109 SoundPool.Builder builder = new SoundPool.Builder();
110 builder.setMaxStreams(1);
111 builder.setAudioAttributes(attrBuilder.build());
112 soundPool = builder.build();
113
114 // create Map of sounds and pre-load sounds
115 soundMap = new SparseIntArray(3); // create new SparseIntArray
116 soundMap.put(TARGET_SOUND_ID,
117 soundPool.load(context, R.raw.target_hit, 1));
118 soundMap.put(CANNON_SOUND_ID,
119 soundPool.load(context, R.raw.cannon_fire, 1));
120 soundMap.put(BLOCKER_SOUND_ID,
121 soundPool.load(context, R.raw.blocker_hit, 1));
122
123 textPaint = new Paint();
124 backgroundPaint = new Paint();
125 backgroundPaint.setColor(Color.WHITE);
126 }
127
Line 102 registers this
(i.e., the CannonView
) as the SurfaceHolder.Callback
that receives method calls when the SurfaceView
is created, updated and destroyed. Inherited SurfaceView
method getHolder returns the SurfaceHolder
object for managing the SurfaceView
, and SurfaceHolder
method addCallback stores the object that implements interface SurfaceHolder.Callback
.
Lines 105–121 configure the sounds that we use in the app. First we create an AudioAttributes.Builder
object (line 105) and call the setUsage
method (line 106), which receives a constant that represents what the audio will be used for. For this app, we use the AudioAttribute.USAGE_GAME
constant, which indicates that the audio is being used as game audio. Next, we create a SoundPool.Builder
object (line 109), which will enable us to create the SoundPool
that’s used to load and play the app’s sound effects. Next, we call SoundPool.Builder
’s setMaxStreams
method (line 110), which takes an argument that 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
. Some more complex games might play many sounds at the same time. We then call AudioAttributes.Builder
’s setAudioAttributes
method (line 111) to use the audio attributes with the SoundPool
object after creating it.
Line 115 creates a SparseIntArray
(soundMap
), which maps integer keys to integer values. SparseIntArray
is similar to—but more efficient than—a HashMap<Integer, Integer>
for small numbers of key–value pairs. In this case, we map the sound keys (defined in Fig. 6.20, lines 86–88) to the loaded sounds’ IDs, which are represented by the return values of the SoundPool
’s load method (called in Fig. 6.21, lines 117, 119 and 121). Each sound ID can be used to play a sound (and later to return its resources to the system). 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 123–124 create the Paint
objects that are used when drawing the game’s background and Time remaining text. The text color defaults to black and line 125 sets the background color to white.
Figure 6.22 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 landscape 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. The first time this method is called, the old width and height are 0. Lines 138–139 configure the textPaint
object, which is used to draw the Time remaining text. Line 138 sets the size of the text to be TEXT_SIZE_PERCENT
of the height of the screen (screenHeight
). We arrived at the value for TEXT_SIZE_PERCENT
and the other scaling factors in Fig. 6.20 via trial and error, choosing values that made the game elements look nice on the screen.
128 // called when the size of the SurfaceView changes,
129 // such as when it's first added to the View hierarchy
130 @Override
131 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
132 super.onSizeChanged(w, h, oldw, oldh);
133
134 screenWidth = w; // store CannonView's width
135 screenHeight = h; // store CannonView's height
136
137 // configure text properties
138 textPaint.setTextSize((int) (TEXT_SIZE_PERCENT * screenHeight));
139 textPaint.setAntiAlias(true); // smoothes the text
140 }
141
In Fig. 6.23, the methods getScreenWidth
and getScreenHeight
return the width and height of the screen, which are updated in the onSizeChanged
method (Fig. 6.22). Using soundPool
’s play method, the playSound
method (lines 153–155) plays the sound in soundMap
with the given soundId
, which was associated with the sound when soundMap
was constructed (Fig. 6.21, lines 113–119). The soundId
is used as the soundMap
key to locate the sound’s ID in the SoundPool
. An object of class GameElement
can call the playSound
method to play its sound.
142 // get width of the game screen
143 public int getScreenWidth() {
144 return screenWidth;
145 }
146
147 // get height of the game screen
148 public int getScreenHeight() {
149 return screenHeight;
150 }
151
152 // plays a sound with the given soundId in soundMap
153 public void playSound(int soundId) {
154 soundPool.play(soundMap.get(soundId), 1, 1, 1, 0, 1f);
155 }
156
Method newGame
(Fig. 6.24) resets the instance variables that are used to control the game. Lines 160–163 create a new Cannon
object with
• a base radius of CANNON_BASE_RADIUS_PERCENT
of the screen height,
• a barrel length of CANNON_BARREL_LENGTH_PERCENT
of the screen width and
• a barrel width of CANNON_BARREL_WIDTH_PERCENT
of the screen height.
157 // reset all the screen elements and start a new game
158 public void newGame() {
159 // construct a new Cannon
160 cannon = new Cannon(this,
161 (int) (CANNON_BASE_RADIUS_PERCENT * screenHeight),
162 (int) (CANNON_BARREL_LENGTH_PERCENT * screenWidth),
163 (int) (CANNON_BARREL_WIDTH_PERCENT * screenHeight));
164
165 Random random = new Random(); // for determining random velocities
166 targets = new ArrayList<>(); // construct a new Target list
167
168 // initialize targetX for the first Target from the left
169 int targetX = (int) (TARGET_FIRST_X_PERCENT * screenWidth);
170
171 // calculate Y coordinate of Targets
172 int targetY = (int) ((0.5 - TARGET_LENGTH_PERCENT / 2) *
173 screenHeight);
174
175 // add TARGET_PIECES Targets to the Target list
176 for (int n = 0; n < TARGET_PIECES; n++) {
177
178 // determine a random velocity between min and max values
179 // for Target n
180 double velocity = screenHeight * (random.nextDouble() *
181 (TARGET_MAX_SPEED_PERCENT - TARGET_MIN_SPEED_PERCENT) +
182 TARGET_MIN_SPEED_PERCENT);
183
184 // alternate Target colors between dark and light
185 int color = (n % 2 == 0) ?
186 getResources().getColor(R.color.dark,
187 getContext().getTheme()) :
188 getResources().getColor(R.color.light,
189 getContext().getTheme());
190
191 velocity *= -1; // reverse the initial velocity for next Target
192
193 // create and add a new Target to the Target list
194 targets.add(new Target(this, color, HIT_REWARD, targetX, targetY,
195 (int) (TARGET_WIDTH_PERCENT * screenWidth),
196 (int) (TARGET_LENGTH_PERCENT * screenHeight),
197 (int) velocity));
198
199 // increase the x coordinate to position the next Target more
200 // to the right
201 targetX += (TARGET_WIDTH_PERCENT + TARGET_SPACING_PERCENT) *
202 screenWidth;
203 }
204
205 // create a new Blocker
206 blocker = new Blocker(this, Color.BLACK, MISS_PENALTY,
207 (int) (BLOCKER_X_PERCENT * screenWidth),
208 (int) ((0.5 - BLOCKER_LENGTH_PERCENT / 2) * screenHeight),
209 (int) (BLOCKER_WIDTH_PERCENT * screenWidth),
210 (int) (BLOCKER_LENGTH_PERCENT * screenHeight),
211 (float) (BLOCKER_SPEED_PERCENT * screenHeight));
212
213 timeLeft = 10; // start the countdown at 10 seconds
214
215 shotsFired = 0; // set the initial number of shots fired
216 totalElapsedTime = 0.0; // set the time elapsed to zero
217
218 if (gameOver) {// start a new game after the last game ended
219 gameOver = false; // the game is not over
220 cannonThread = new CannonThread(getHolder()); // create thread
221 cannonThread.start(); // start the game loop thread
222 }
223
224 hideSystemBars();
225 }
226
Line 165 creates a new Random
object that’s used to randomize the Target
velocities. Line 166 creates a new ArrayList
of Target
s. Line 169 initializes targetX
to the number of pixels from the left that the first Target
will be positioned on the screen. The first Target
is positioned TARGET_FIRST_X_PERCENT
of the way across the screen. Lines 172–173 initialize targetY
with a value to vertically center all Target
s on the screen. Lines 176–203 construct TARGET_PIECES
(9) new Target
s and add them to targets
. Lines 180–182 set the velocity of the new Target
to a random value between the screen height percentages TARGET_MIN_SPEED_PERCENT
and TARGET_MAX_SPEED_PERCENT
. Lines 185–189 set the color of the new Target
to alternate between the R.color.dark
and R.color.light
colors and alternate between positive and negative vertical velocities. Line 191 reverses the target velocity for each new target so that some targets move up to start and some move down. The new Target
is constructed and added to targets
(lines 194–197). The Target
is given a width of TARGET_WIDTH_PERCENT
of the screen width and a height of TARGET_HEIGHT_PERCENT
of the screen height. Finally, targetX
is incremented to position the next Target
.
A new Blocker
is constructed and stored in blocker
in lines 206–211. The Blocker
is positioned BLOCKER_X_PERCENT
of the screen width from the left and is vertically centered on the screen to start the game. The Blocker
’s width is BLOCKER_WIDTH_PERCENT
of the screen width and the Blocker
’s height is BLOCKER_HEIGHT_PERCENT
of the screen height. The Blocker
’s speed is BLOCKER_SPEED_PERCENT
of the screen height.
If variable gameOver
is true
, which occurs only after the first game completes, line 219 resets gameOver
and lines 220–221 create a new CannonThread
and call its start
method to begin the game loop that controls the game. Line 224 calls method hideSystemBars
(Section 6.13.16) to put the app in immersive mode—this hides the system bars and enables the user to display them at any time by swiping down from the top of the screen.
Method updatePositions
(Fig. 6.25) is called by the CannonThread
’s run
method (Section 6.13.15) 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 and current animation frames. 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 Section 6.13.15.
227 // called repeatedly by the CannonThread to update game elements
228 private void updatePositions(double elapsedTimeMS) {
229 double interval = elapsedTimeMS / 1000.0; // convert to seconds
230
231 // update cannonball's position if it is on the screen
232 if (cannon.getCannonball() != null)
233 cannon.getCannonball().update(interval);
234
235 blocker.update(interval); // update the blocker's position
236
237 for (GameElement target : targets)
238 target.update(interval); // update the target's position
239
240 timeLeft -= interval; // subtract from time left
241
242 // if the timer reached zero
243 if (timeLeft <= 0) {
244 timeLeft = 0.0;
245 gameOver = true; // the game is over
246 cannonThread.setRunning(false); // terminate thread
247 showGameOverDialog(R.string.lose); // show the losing dialog
248 }
249
250 // if all pieces have been hit
251 if (targets.isEmpty()) {
252 cannonThread.setRunning(false); // terminate thread
253 showGameOverDialog(R.string.win); // show winning dialog
254 gameOver = true;
255 }
256 }
257
Line 229 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.
To update the positions of the GameElement
s, lines 232–238 call the update
methods of the Cannonball
(if there is one on the screen), the Blocker
and all of the remaining Target
s. The update
method receives the time elapsed since the previous frame so that the positions can be updated by the correct amount for the interval.
We decrease timeLeft
by the time that has passed since the prior animation frame (line 240). If timeLeft
has reached zero, the game is over, so we set timeLeft
to 0.0 just in case it was negative; otherwise, sometimes a negative final time would display 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.
When the user touches the screen, method onTouchEvent
(Section 6.13.14) calls alignAndFireCannonball
(Fig. 6.26). Lines 267–272 calculate the angle
necessary to aim the cannon at the touch point. Line 275 calls Cannon
’s align
method to aim the cannon
with trajectory angle
. Finally, if the Cannonball
exists and is on the screen, lines 280–281 fire the Cannonball
and increment shotsFired
.
258 // aligns the barrel and fires a Cannonball if a Cannonball is not
259 // already on the screen
260 public void alignAndFireCannonball(MotionEvent event) {
261 // get the location of the touch in this view
262 Point touchPoint = new Point((int) event.getX(),
263 (int) event.getY());
264
265 // compute the touch's distance from center of the screen
266 // on the y-axis
267 double centerMinusY = (screenHeight / 2 - touchPoint.y);
268
269 double angle = 0; // initialize angle to 0
270
271 // calculate the angle the barrel makes with the horizontal
272 angle = Math.atan2(touchPoint.x, centerMinusY);
273
274 // point the barrel at the point where the screen was touched
275 cannon.align(angle);
276
277 // fire Cannonball if there is not already a Cannonball on screen
278 if (cannon.getCannonball() == null ||
279 !cannon.getCannonball().isOnScreen()) {
280 cannon.fireCannonball();
281 ++shotsFired;
282 }
283 }
284
When the game ends, the showGameOverDialog
method (Fig. 6.27) displays a DialogFragment
(using the techniques you learned in Section 4.7.10) containing an AlertDialog
that indicates whether the player won or lost, the number of shots fired and the total time elapsed. The call to method setPositiveButton
(lines 301–311) creates a reset button for starting a new game.
285 // display an AlertDialog when the game ends
286 private void showGameOverDialog(final int messageId) {
287 // DialogFragment to display game stats and start new game
288 final DialogFragment gameResult =
289 new DialogFragment() {
290 // create an AlertDialog and return it
291 @Override
292 public Dialog onCreateDialog(Bundle bundle) {
293 // create dialog displaying String resource for messageId
294 AlertDialog.Builder builder =
295 new AlertDialog.Builder(getActivity());
296 builder.setTitle(getResources().getString(messageId));
297
298 // display number of shots fired and total time elapsed
299 builder.setMessage(getResources().getString(
300 R.string.results_format, shotsFired, totalElapsedTime));
301 builder.setPositiveButton(R.string.reset_game,
302 new DialogInterface.OnClickListener() {
303 // called when "Reset Game" Button is pressed
304 @Override
305 public void onClick(DialogInterface dialog,
306 int which) {
307 dialogIsDisplayed = false;
308 newGame(); // set up and start a new game
309 }
310 }
311 );
312
313 return builder.create(); // return the AlertDialog
314 }
315 };
316
317 // in GUI thread, use FragmentManager to display the DialogFragment
318 activity.runOnUiThread(
319 new Runnable() {
320 public void run() {
321 showSystemBars(); // exit immersive mode
322 dialogIsDisplayed = true;
323 gameResult.setCancelable(false); // modal dialog
324 gameResult.show(activity.getFragmentManager(), "results");
325 }
326 }
327 );
328 }
329
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 318–327 call Activity
method runOnUiThread to specify a Runnable
that should execute in the GUI thread as soon as possible. The argument is an object of an anonymous inner class that implements Runnable
. The Runnable
’s run
method calls method showSystemBars
(Section 6.13.16) to remove the app from immersive mode, then indicates that the dialog is displayed and displays it.
The method drawGameElements
(Fig. 6.28) draws the Cannon
, Cannonball
, Blocker
and Target
s on the SurfaceView
using the Canvas
that the CannonThread
(Section 6.13.15) obtains from the SurfaceView
’s SurfaceHolder
.
First, we call Canvas
’s drawRect method (lines 333–334) to clear the Canvas
so that the game elements can be displayed in their new positions. The method receives the rectangle’s upper-left x-y coordinates, width and height, and the Paint
object that specifies the drawing characteristics—recall that backgroundPaint
sets the drawing color to white.
330 // draws the game to the given Canvas
331 public void drawGameElements(Canvas canvas) {
332 // clear the background
333 canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
334 backgroundPaint);
335
336 // display time remaining
337 canvas.drawText(getResources().getString(
338 R.string.time_remaining_format, timeLeft), 50, 100, textPaint);
339
340 cannon.draw(canvas); // draw the cannon
341
342 // draw the GameElements
343 if (cannon.getCannonball() != null &&
344 cannon.getCannonball().isOnScreen())
345 cannon.getCannonball().draw(canvas);
346
347 blocker.draw(canvas); // draw the blocker
348
349 // draw all of the Targets
350 for (GameElement target : targets)
351 target.draw(canvas);
352 }
353
Next, we call Canvas
’s drawText method (lines 337–338) 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 Fig. 6.22, lines 138–139) to describe how the text should be rendered (that is, the text’s font size, color and other attributes).
Lines 339–350 draw the Cannon
, the Cannonball
(if it is on the screen), the Blocker
, and each of the Target
s. Each of these elements is drawn by calling its draw
method and passing in canvas
.
The testForCollisions
method (Fig. 6.29) checks whether the Cannonball
is colliding with any of the Target
s or with the Blocker
, and applies certain effects in the game if a collision occurs. Lines 359–360 check whether a Cannonball
is on the screen. If so, line 362 calls the Cannonball
’s collidesWith
method to determine whether the Cannonball
is colliding with a Target
. If ther is a collision, line 363 calls the Target
’s playSound
method to play the target-hit sound, line 366 increments timeLeft
by the hit reward associated with the Target
, and lines 368–369 remove the Cannonball
and Target
from the screen. Line 370 decrements n
to ensure the target that’s now in position n
gets tested for a collision. Line 376 destroys the Cannonball
associated with Cannon
if it’s not on the screen. If the Cannonball
is still on the screen, lines 380–381 call collidesWith
again to determine whether the Cannonball
is colliding with the Blocker
. If so, line 382 calls the Blocker
’s playSound
method to play the blocker-hit sound, line 385 reverses the cannonball
’s horizontal velocity by calling class Cannonball
’s reverseVelocityX
method, and line 388 decrements timeLeft
by the miss penalty associated with the Blocker
.
354 // checks if the ball collides with the Blocker or any of the Targets
355 // and handles the collisions
356 public void testForCollisions() {
357 // remove any of the targets that the Cannonball
358 // collides with
359 if (cannon.getCannonball() != null &&
360 cannon.getCannonball().isOnScreen()) {
361 for (int n = 0; n < targets.size(); n++) {
362 if (cannon.getCannonball().collidesWith(targets.get(n))) {
363 targets.get(n).playSound(); // play Target hit sound
364
365 // add hit rewards time to remaining time
366 timeLeft += targets.get(n).getHitReward();
367
368 cannon.removeCannonball(); // remove Cannonball from game
369 targets.remove(n); // remove the Target that was hit
370 --n; // ensures that we don't skip testing new target n
371 break;
372 }
373 }
374 }
375 else { // remove the Cannonball if it should not be on the screen
376 cannon.removeCannonball();
377 }
378
379 // check if ball collides with blocker
380 if (cannon.getCannonball() != null &&
381 cannon.getCannonball().collidesWith(blocker)) {
382 blocker.playSound(); // play Blocker hit sound
383
384 // reverse ball direction
385 cannon.getCannonball().reverseVelocityX();
386
387 // deduct blocker's miss penalty from remaining time
388 timeLeft -= blocker.getMissPenalty();
389 }
390 }
391
Class MainActivityFragment
’s onPause
and onDestroy
methods (Section 6.13) call class CannonView
’s stopGame
and releaseResources
methods (Fig. 6.30), respectively. Method stopGame
(lines 393–396) 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 399–402) calls the SoundPool
’s release method to release the resources associated with the SoundPool
.
392 // stops the game: called by CannonGameFragment's onPause method
393 public void stopGame() {
394 if (cannonThread != null)
395 cannonThread.setRunning(false); // tell thread to terminate
396 }
397
398 // release resources: called by CannonGame's onDestroy method
399 public void releaseResources() {
400 soundPool.release(); // release all resources used by the SoundPool
401 soundPool = null;
402 }
403
Figure 6.31 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 landscape orientation. This method is called when the SurfaceView
’s size or orientation changes, and would typically be used to redisplay graphics based on those changes.
404 // called when surface changes size
405 @Override
406 public void surfaceChanged(SurfaceHolder holder, int format,
407 int width, int height) { }
408
409 // called when surface is first created
410 @Override
411 public void surfaceCreated(SurfaceHolder holder) {
412 if (!dialogIsDisplayed) {
413 newGame(); // set up and start a new game
414 cannonThread = new CannonThread(holder); // create thread
415 cannonThread.setRunning(true); // start game running
416 cannonThread.start(); // start the game loop thread
417 }
418 }
419
420 // called when the surface is destroyed
421 @Override
422 public void surfaceDestroyed(SurfaceHolder holder) {
423 // ensure that thread terminates properly
424 boolean retry = true;
425 cannonThread.setRunning(false); // terminate cannonThread
426
427 while (retry) {
428 try {
429 cannonThread.join(); // wait for cannonThread to finish
430 retry = false;
431 }
432 catch (InterruptedException e) {
433 Log.e(TAG, "Thread interrupted", e);
434 }
435 }
436 }
437
Method surfaceCreated
(lines 410–418) 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 loop. Method surfaceDestroyed
(lines 421–436) is called when the SurfaceView
is destroyed—e.g., when the app terminates. We use surfaceDestroyed
to ensure that the CannonThread
terminates properly. First, line 425 calls CannonThread
’s setRunning
method with false
as an argument to indicate that the thread should stop, then lines 427–435 wait for the thread to terminate. This ensures that no attempt is made to draw to the SurfaceView
once surfaceDestroyed
completes execution.
In this example, we override View
method onTouchEvent
(Fig. 6.32) to determine when the user touches the screen. The MotionEvent
parameter contains information about the event that occurred. Line 442 uses the MotionEvent
’s getAction
method to determine which type of touch event occurred. Then, lines 445–446 determine whether the user touched the screen (MotionEvent.ACTION_DOWN
) or dragged a finger across the screen (MotionEvent.ACTION_MOVE
). In either case, line 448 calls the cannonView
’s alignAndFireCannonball
method to aim and fire the cannon toward that touch point. Line 451 then returns true
to indicate that the touch event was handled.
438 // called when the user touches the screen in this activity
439 @Override
440 public boolean onTouchEvent(MotionEvent e) {
441 // get int representing the type of action which caused this event
442 int action = e.getAction();
443
444 // the user touched the screen or dragged along the screen
445 if (action == MotionEvent.ACTION_DOWN ||
446 action == MotionEvent.ACTION_MOVE) {
447 // fire the cannonball toward the touch point
448 alignAndFireCannonball(e);
449 }
450
451 return true;
452 }
453
Figure 6.33 defines a subclass of Thread
which updates the game. The thread maintains a reference to the SurfaceView
’s SurfaceHolder
(line 456) and a boolean
indicating whether the thread is running.
454 // Thread subclass to control the game loop
455 private class CannonThread extends Thread {
456 private SurfaceHolder surfaceHolder; // for manipulating canvas
457 private boolean threadIsRunning = true; // running by default
458
459 // initializes the surface holder
460 public CannonThread(SurfaceHolder holder) {
461 surfaceHolder = holder;
462 setName("CannonThread");
463 }
464
465 // changes running state
466 public void setRunning(boolean running) {
467 threadIsRunning = running;
468 }
469
470 // controls the game loop
471 @Override
472 public void run() {
473 Canvas canvas = null; // used for drawing
474 long previousFrameTime = System.currentTimeMillis();
475
476 while (threadIsRunning) {
477 try {
478 // get Canvas for exclusive drawing from this thread
479 canvas = surfaceHolder.lockCanvas(null);
480
481 // lock the surfaceHolder for drawing
482 synchronized(surfaceHolder) {
483 long currentTime = System.currentTimeMillis();
484 double elapsedTimeMS = currentTime - previousFrameTime;
485 totalElapsedTime += elapsedTimeMS / 1000.0;
486 updatePositions(elapsedTimeMS); // update game state
487 testForCollisions(); // test for GameElement collisions
488 drawGameElements(canvas); // draw using the canvas
489 previousFrameTime = currentTime; // update previous time
490 }
491 }
492 finally {
493 // display canvas's contents on the CannonView
494 // and enable other threads to use the Canvas
495 if (canvas != null)
496 surfaceHolder.unlockCanvasAndPost(canvas);
497 }
498 }
499 }
500 }
The class’s run
method (lines 471–499) drives the frame-by-frame animations—this is known 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 474 gets the system’s current time in milliseconds when the thread begins running. Lines 476–498 loop until threadIsRunning
is false
.
First we obtain the Canvas
for drawing on the SurfaceView
by calling SurfaceHolder
method lockCanvas (line 479). Only one thread at a time can draw to a SurfaceView
. To ensure this, you must first lock the SurfaceHolder
by specifying it as the expression in the parentheses of a synchronized
block (line 482). Next, we get the current time in milliseconds, then calculate the elapsed time and add that to the total time so far—this will be used to help display the amount of time left in the game. Line 486 calls method updatePositions
to move all the game elements, passing the elapsed time in milliseconds as an argument. This ensures 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. Line 487 calls testForCollisions
to determine whether the Cannonball
collided with the Blocker
or a Target
:
• If a collision occurs with the Blocker, testForCollisions
reverses the Cannonball
’s velocity.
• If a collision occurs with a Target
, testForCollisions
removes the Cannonball
.
Finally, line 488 calls the drawGameElements
method to draw the game elements using the SurfaceView
’s Canvas,
and line 489 stores the currentTime
as the previousFrameTime
to prepare to calculate the elapsed time between this animation frame and the next.
This app uses immersive mode—at any time during game play, the user can view the system bars by swiping down from the top of the screen. Immersive mode is available only on devices running Android 4.4 or higher. So, methods hideSystemBars
and showSystemBars
(Fig. 6.34) first check whether the device’s Android version—Build.VERSION_SDK_INT
—is greater than or equal to Build.VERSION_CODES_KITKAT
—the constant for Android 4.4 (API level 19). If so, both methods use View
method setSystemUiVisibility to configure the system bars and app bar (though we already hid the app bar by modifying this app’s theme). To hide the system bars and app bar and place the UI into immersive mode, you pass to setSystemUiVisibility
the constants that are combined via the bitwise OR (|
) operator in lines 505–510. To show the system bars and app bar, you pass to setSystemUiVisibility
the constants that are combined in lines 517–519. These combinations of View
constants ensure that the CannonView
is not resized each time the system bars and app bar are hidden and redisplayed. Instead, the system bars and app bar overlay the CannonView
—that is, part of the CannonView
is temporarily hidden when the system bars are on the screen. For more information on immersive mode, visit
501 // hide system bars and app bar
502 private void hideSystemBars() {
503 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
504 setSystemUiVisibility(
505 View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
506 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
507 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
508 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
509 View.SYSTEM_UI_FLAG_FULLSCREEN |
510 View.SYSTEM_UI_FLAG_IMMERSIVE);
511 }
512
513 // show system bars and app bar
514 private void showSystemBars() {
515 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
516 setSystemUiVisibility(
517 View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
518 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
519 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
520 }
521 }
In this chapter, you created the Cannon Game app, which challenges the player to destroy nine targets before a 10-second time limit expires. The user aims and fires the cannon by touching the screen. To draw on the screen from a separate thread, you created a custom view by extending class SurfaceView.
You learned that custom component class names must be fully qualified in the XML layout element that represents the component. We presented additional Fragment
lifecycle methods. You learned that method onPause
is called when a Fragment
is paused and method onDestroy
is called when the Fragment
is destroyed. You handled touches by overriding View
’s onTouchEvent
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 performs its animations by updating the game elements on a SurfaceView
from a separate thread of execution. To do this, you extended class Thread
and created a run
method that displays graphics by calling 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, regardless of their processor speeds. Finally, you used immersive mode to enable the app to use the entire screen.
In Chapter 7, you’ll build the WeatherViewer app. You’ll use web services to interact with the 16-day weather forecast web service from OpenWeatherMap.org
. Like many of today’s web services, the OpenWeatherMap.org
web service will return the forecast data in JavaScript Object Notation (JSON) format. You’ll process the response using the JSONObject
and JSONArray
classes from the org.json
package. You’ll then display the daily forecast in a ListView
.
6.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.
f) __________ enables an app to take advantage of the entire screen.
6.2 State whether each of the following is true or false. If false, explain why.
a) The Android documentation recommends that games use the music audio stream to play sounds.
b) 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.
c) A Canvas
draws on a View
’s Bitmap
.
d) Format String
s that contain multiple format specifiers must number the format specifiers for localization purposes.
e) 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.
f) 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
.
f) immersive mode.
a) True.
b) 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.
c) True.
d) True.
e) True.
f) True.
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) 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
).
d) 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.
e) Method _____________ is called for the current Activity
when another activity receives the focus.
6.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.
6.5 (Enhanced Cannon Game App) Modify the Cannon Game app as follows:
a) Use images for the cannon base and cannonball.
b) Display a dashed line showing the cannonball’s path.
c) Play a sound when the blocker hits the top or bottom of the screen.
d) Play a sound when the target hits the top or bottom of the screen.
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) Add an explosion animation each time the cannonball hits the blocker.
j) When the cannonball hits the blocker, increase the blocker’s length by 5%.
k) Make the game more difficult as it progresses by increasing the speed of the target and the blocker.
l) Increase the number of obstacles between the cannon and the target.
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.
6.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.
6.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. 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 (use Fragment
s to display separate CannonView
objects). The two players will need to be sitting side-by-side to play this version of the game.
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!”
6.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.
6.9 (Digital Clock App) Create an app that displays a digital clock on the screen. Include alarmclock functionality.
6.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.
6.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.
6.12 (Animated Towers of Hanoi App) Every budding computer scientist must grapple with certain classic problems, and the Towers of Hanoi (see Fig. 6.35) 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