6. Cannon Game App


Objectives

In this chapter you’ll:

Image Create a simple game app that’s easy to code and fun to play.

Image Create a custom SurfaceView subclass for displaying the game’s graphics from a separate thread of execution.

Image Draw graphics using Paints and a Canvas.

Image Override View’s onTouchEvent method to fire a cannonball when the user touches the screen.

Image Perform simple collision detection.

Image Add sound to your app using a SoundPool and the AudioManager.

Image Override Fragment lifecycle method onDestroy.

Image Use immersive mode to enable the game to occupy the entire screen, but still allow the user to access the system bars.



Outline

6.1 Introduction

6.2 Test-Driving the Cannon Game App

6.3 Technologies Overview

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 Threads, SurfaceView and SurfaceHolder

6.3.6 Simple Collision Detection

6.3.7 Immersive Mode

6.4 Building the GUI and Resource Files

6.4.1 Creating the Project

6.4.2 Adjusting the Theme to Remove the App Title and App Bar

6.4.3 strings.xml

6.4.4 Colors

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 Class GameElement

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 Cannon Class

6.11.1 Instance Variables and Constructor

6.11.2 Method align

6.11.3 Method fireCannonball

6.11.4 Method draw

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.12.3 Method update

6.12.4 Method draw

6.13 CannonView Subclass of SurfaceView

6.13.1 package and import Statements

6.13.2 Instance Variables and Constants

6.13.3 Constructor

6.13.4 Overriding View Method onSizeChanged

6.13.5 Methods getScreenWidth, getScreenHeight, and playSound

6.13.6 Method newGame

6.13.7 Method updatePositions

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

6.14 Wrap-Up

Self-Review Exercises | Answers to Self-Review Exercises | Exercises


6.1 Introduction

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 Imageditions 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.

Image

Fig. 6.1 | Completed Cannon Game app.

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).

Image

Fig. 6.2 | Cannon Game app AlertDialogs showing a win and a loss.

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.]

6.2 Test-Driving the Cannon Game App

Opening and Running the App

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.

Playing the Game

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.

6.3 Technologies Overview

This section presents the new technologies that we use in the Cannon Game app in the order they’re encountered in the chapter.

6.3.1 Using the Resource Folder res/raw

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.

6.3.2 Activity and Fragment Lifecycle Methods

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 Fragments hosted by the Activity. We use this method in the MainActivityFragment to release the CannonView’s sound resources.


Image Error-Prevention Tip 6.1

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.


6.3.3 Overriding View Method onTouchEvent

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.

6.3.4 Adding Sound with 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 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.

6.3.5 Frame-by-Frame Animation with Threads, SurfaceView and SurfaceHolder

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.


Image Performance Tip 6.1

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.

6.3.6 Simple Collision Detection

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.

Fig. 6.3 | Game-development frameworks.

6.3.7 Immersive Mode

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.

6.4 Building the GUI and Resource Files

In this section, you’ll create the app’s resource files, GUI layout files and classes.

6.4.1 Creating the Project

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.

Configure the App for Landscape Orientation

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.

6.4.2 Adjusting the Theme to Remove the App Title and App Bar

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.

6.4.3 strings.xml

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.

Image

Fig. 6.4 | String resources used in the Cannon Game app.

6.4.4 Colors

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>

6.4.5 Adding the Sounds to the App

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.

6.4.6 Adding Class MainActivityFragment

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.

6.4.7 Editing activity_main.xml

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.

6.4.8 Adding the CannonView to fragment_main.xml

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 (Image) 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.

6.5 Overview of This App’s Classes

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, Targets, 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.

6.6 MainActivity Subclass of Activity

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   }


Fig. 6.5 | MainActivity class displays the MainActivityFragment.

6.7 MainActivityFragment Subclass of Fragment

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   }


Fig. 6.6 | MainActivityFragment creates and manages the CannonView.

6.8 Class GameElement

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   }


Fig. 6.7 | GameElement class represents a rectangle-bounded game element.

6.8.1 Instance Variables and Constructor

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

ints for the x and y position of the GameElement’s upper-left corner

ints 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.

6.8.2 Methods update, draw, and playSound

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.

6.9 Blocker Subclass of GameElement

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   }


Fig. 6.8 | Blocker subclass of GameElement.

6.10 Target Subclass of GameElement

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   }


Fig. 6.9 | Target subclass of GameElement.

6.11 Cannon Class

The Cannon class (Figs. 6.106.14) represents the cannon in the Cannon Game app. The cannon has a base and a barrel, and it can fire a cannonball.

6.11.1 Instance Variables and Constructor

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


Fig. 6.10 | Cannon instance variables and constructor.

6.11.2 Method align

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


Fig. 6.11 | Cannon method align.

6.11.3 Method fireCannonball

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


Fig. 6.12 | Cannon method fireCannonball.

6.11.4 Method draw

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


Fig. 6.13 | Cannon method draw.

Drawing the Cannon Barrel with Canvas Method drawLine

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).

Drawing the Cannon Base with Canvas Method drawCircle

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.

6.11.5 Methods getCannonball and removeCannonball

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   }


Fig. 6.14 | CannonView methods getCannonball and removeCannonball.

6.12 Cannonball Subclass of GameElement

The Cannonball subclass of GameElement (Sections 6.12.16.12.4) represents a cannonball fired from the cannon.

6.12.1 Instance Variables and Constructor

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


Fig. 6.15 | Cannonball instance variables and constructor.

6.12.2 Methods getRadius, collidesWith, isOnScreen, and reverseVelocityX

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


Fig. 6.16 | Cannonball methods getRadius, collidesWith, isOnScreen and reverseVelocityX.

Checking for Collisions with Another GameElement with the collidesWith Method

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.

Reversing the Cannonball’s Horizontal Velocity with reverseVelocityX

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).

6.12.3 Method update

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


Fig. 6.17 | Overridden GameElement method update.

6.12.4 Method draw

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   }


Fig. 6.18 | Overridden GameElement method draw.

6.13 CannonView Subclass of SurfaceView

Class CannonView (Figs. 6.196.33) is a custom subclass of View that implements the Cannon Game’s logic and draws game objects on the screen.

6.13.1 package and import Statements

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


Fig. 6.19 | CannonView class’s package and import statements.

6.13.2 Instance Variables and Constants

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


Fig. 6.20 | CannonView class’s static and instance variables.

6.13.3 Constructor

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


Fig. 6.21 | CannonView constructor.

Registering the SurfaceHolder.Callback Listener

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.

Configuring the SoundPool and Loading the Sounds

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.

Creating the Paint Objects Used to Draw the Background and Timer Text

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.

6.13.4 Overriding View Method onSizeChanged

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


Fig. 6.22 | Overriding View method onSizeChanged.

6.13.5 Methods getScreenWidth, getScreenHeight, and playSound

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


Fig. 6.23 | CannonView methods getScreenWidth, getScreenHeight and playSound.

6.13.6 Method newGame

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


Fig. 6.24 | CannonView method newGame.

Line 165 creates a new Random object that’s used to randomize the Target velocities. Line 166 creates a new ArrayList of Targets. 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 Targets on the screen. Lines 176–203 construct TARGET_PIECES (9) new Targets 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.

6.13.7 Method updatePositions

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


Fig. 6.25 | CannonView method updatePositions.

Elapsed Time Since the Last Animation Frame

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.

Updating the Cannonball, Blocker and Target Positions

To update the positions of the GameElements, lines 232–238 call the update methods of the Cannonball (if there is one on the screen), the Blocker and all of the remaining Targets. 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.

Updating the Time Left and Determining Whether Time Ran Out

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.

6.13.8 Method alignAndFireCannonball

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


Fig. 6.26 | CannonView method alignAndFireCannonball.

6.13.9 Method showGameOverDialog

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


Fig. 6.27 | CannonView method showGameOverDialog.

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.

6.13.10 Method drawGameElements

The method drawGameElements (Fig. 6.28) draws the Cannon, Cannonball, Blocker and Targets on the SurfaceView using the Canvas that the CannonThread (Section 6.13.15) obtains from the SurfaceView’s SurfaceHolder.

Clearing the Canvas with Method drawRect

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


Fig. 6.28 | CannonView method drawGameElements.

Displaying the Time Remaining with Canvas Method drawText

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).

Drawing the Cannon, Cannonball, Blocker and Targets with the draw Method

Lines 339–350 draw the Cannon, the Cannonball (if it is on the screen), the Blocker, and each of the Targets. Each of these elements is drawn by calling its draw method and passing in canvas.

6.13.11 Method testForCollisions

The testForCollisions method (Fig. 6.29) checks whether the Cannonball is colliding with any of the Targets 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


Fig. 6.29 | CannonView method testForCollisions.

6.13.12 Methods stopGame and releaseResources

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


Fig. 6.30 | CannonView methods stopGame and releaseResources.

6.13.13 Implementing the SurfaceHolder.Callback Methods

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


Fig. 6.31 | Implementing the SurfaceHolder.Callback methods.

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.

6.13.14 Overriding View Method onTouchEvent

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


Fig. 6.32 | Overriding View method onTouchEvent.

6.13.15 CannonThread: Using a Thread to Create a Game Loop

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     }


Fig. 6.33 | Nested class CannonThread manages the game loop, updating the game elements every TIME_INTERVAL milliseconds.

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.

6.13.16 Methods hideSystemBars and showSystemBars

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  }


Fig. 6.34 | DoodleView methods hideSystemBars and showSystemBars.

6.14 Wrap-Up

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.

Self-Review Exercises

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 Strings 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.

Answers to Self-Review Exercises

6.1

a) one of its subclasses.

b) MotionEvent.

c) SurfaceHolder.Callback.

d) floating-point.

e) res/raw.

f) immersive mode.

6.2

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.

Exercises

6.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) 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 Fragments 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.

Image

Fig. 6.35 | The Towers of Hanoi for the case with four disks.

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.178.181