7. Cannon Game App

Objectives

In this chapter you’ll:

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

• Create a custom SurfaceView subclass and use it to display the game’s graphics from a separate thread of execution.

• Draw graphics using Paints and a Canvas.

• Play sounds in respone to various game events.

• Manually perform frame-by-frame animations using a game loop that accounts for varying frame rates across devices.

• Override Activity’s onTouchEvent to process touch events when the user touches the screen or drags a finger on the screen.

• Use a GestureDetector to recognize more sophisticated user touch motions, such as double taps.

• Perform simple collision detection.

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

• Override three additional Activity lifecycle methods.

Outline

7.1 Introduction

7.2 Test-Driving the Cannon Game app

7.3 Technologies Overview

7.4 Building the App’s GUI and Resource Files

7.4.1 Creating the Project

7.4.2 AndroidManifest.xml

7.4.3 strings.xml

7.4.4 main.xml

7.4.5 Adding the Sounds to the App

7.5 Building the App

7.5.1 Line Class Maintains a Line’s Endpoints

7.5.2 CannonGame Subclass of Activity

7.5.3 CannonView Subclass of View

7.6 Wrap-Up

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

7.1. Introduction

The Cannon Game app challenges you to destroy a seven-piece target before a ten-second time limit expires (Fig. 7.1). The game consists of four visual components—a cannon that you control, a cannonball, the target and a blocker that defends the target. You aim the cannon by touching the screen—the cannon then aims at the touched point. The cannon fires a cannonball when you double-tap the screen. At the end of the game, the app displays an AlertDialog indicating whether you won or lost, and showing the number of shots fired and the elapsed time (Fig. 7.2).

Image

Fig. 7.1. Completed Cannon Game app.

Image

Fig. 7.2. Cannon Game app AlertDialogs showing a win and a loss.

The game begins with a 10-second time limit. Each time you hit a target section, three seconds are added to the time limit, and each time you hit the blocker, two seconds are subtracted. You win by destroying all seven target sections before time runs out. If the timer reaches zero, you lose.

When you fire the cannon, the game plays a firing sound. The target consists of seven pieces. When a cannonball hits the target, a glass-breaking sound plays and that piece of the target disappears from the screen. When the cannonball hits the blocker, a hit sound plays and the cannonball bounces back. The blocker cannot be destroyed. The target and blocker move vertically at different speeds, changing direction when they hit the top or bottom of the screen.

7.2. Test-Driving the Cannon Game App

Opening and Running the App

Open Eclipse and import the Cannon Game app project. Perform the following steps:

1. Open the Import Dialog. Select File > Import... to open the Import dialog.

2. Import the Cannon Game app’s project. In the Import dialog, expand the General node and select Existing Projects into Workspace, then click Next > to proceed to the Import Projects step. Ensure that Select root directory is selected, then click the Browse... button. In the Browse for Folder dialog, locate the CannonGame folder in the book’s examples folder, select it and click OK. Click Finish to import the project into Eclipse. The project now appears in the Package Explorer window at the left side of the Eclipse window.

3. Launch the Cannon Game app. In Eclipse, right click the CannonGame project in the Package Explorer window, then select Run As > Android Application from the menu that appears.

Playing the Game

Drag your finger on the screen or tap it to aim the cannon. Double tap the screen to fire a shot. You can fire a cannonball only if there is not another cannonball on the screen. If you’re running this in an AVD, your “finger” is the mouse. Try to destroy the target as fast as you can—if the timer runs out, the game ends.

7.3. Technologies Overview

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

Defining String Formatting Resources in strings.xml

In this app, we define String resources to represent the format Strings that are used in calls to class Resource’s method getString (or to class String’s static method format). When format Strings contain multiple format specifiers, you’re required to number them (from 1) to indicate the order in which the corresponding values will be substituted into the format String. In some spoken languages, a String’s phrasing might result in the values being placed at different locations in the localized String resources. In such cases, the localized versions of strings.xml can use the original format-specifier numbers, but place the format specifiers at appropriate locations in the localized Strings. The syntax for numbering format specifiers is shown in Section 7.4.3.

Attaching a Custom View to a Layout

You can create a custom view by extending class View or one of its subclasses, as we do with class CannonView (Section 7.5.3), which extends SurfaceView (discussed shortly). To add a custom component to a layout’s XML file, you must fully qualify its class name in the XML element that represents the component. This is demonstrated in Section 7.4.4.

Using the Resource Folder raw

Media files, such as the sounds used in the Cannon Game app are placed in the app’s resource folder res/raw. Section 7.4.5 discusses how to create this folder. You’ll then drag the app’s sound files into it.

Activity Lifecycle Methods onPause and onDestroy

This app uses additional Activity lifecycle methods. Method onPause is called for the current Activity when another activity receives the focus, which sends the current activity to the background. We use onPause to suspend game play so that the game does not continue executing when the user cannot interact with it.

When an Activity is shut down, its onDestroy method is called. We use this method to release the app’s sound resources. These lifecycle methods are used in Section 7.5.2.

Overriding Activity Method onTouchEvent

As you know, users interact with this app by touching the device’s screen. A touch or single tap aligns the cannon to face the touch or single tap point on the screen. To process simple touch events for an Activity, you can override class Activity’s onTouchEvent method (Section 7.5.2) then use constants from class MotionEvent (package android.view) to test which type of event occurred and process it accordingly.

GestureDetector and SimpleOnGestureListener

For more complex gestures, like the double taps that fire the cannon, you’ll use a GestureDetector (package android.view), which can recognize user actions that represent a series of MotionEvents. A GestureDetector allows an app to react to more sophisticated user interactions such as flings, double-taps, long presses and scrolls. Your apps can respond to such events by implementing the methods of the GestureDetector.OnGestureListener and GestureDetector.OnDoubleTapListener interfaces. Class GestureDetector.SimpleOnGestureListener is an adapter class that implements all the methods of these two interfaces, so you can extend this class and override just the method(s) you need from these interfaces. In Section 7.5.2, we initialize a GestureDetector with a SimpleGestureListener, which will handle the double tap event that fires the cannon.

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 several audio streams, which include streams for alarms, DTMF tones, music, notifications, phone rings, system sounds and phone calls. The Android documentation recommends that games use the music audio stream to play sounds. We use the Activity’s setVolumeControlStream method to specify that the game’s volume can be controlled with the device’s volume keys and should be the same as the device’s music playback volume. The method receives a constant from class AudioManager (package android.media).

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 execuion. To do this, we use a subclass of Thread with a run method that directs our custom CannonView to update the positions of all the game’s elements, then draws the elements. Normally, all updates to an app’s user interface must be performed in the GUI thread of execution. However, in Android, it’s important to minimize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.

Games often require complex logic that should be performed in separate threads of execution and those threads often need to draw to the screen. For such cases, Android provides class SurfaceView—a subclass of View to which any thread can draw. You manipulate a SurfaceView via an object of class SurfaceHolder, which enables you to obtain a Canvas on which you can draw graphics. Class SurfaceHolder also provides methods that give a thread exclusive access to the Canvas for drawing, because only one thread at a time can draw to a SurfaceView. Each SurfaceView subclass should implement the interface SurfaceHolder.Callback, which contains methods that are called when the SurfaceView is created, changed (e.g., its size or orientation changes) or destroyed.

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 7.5.3. [Note: Many game-development frameworks provide more sophisticated collision detection capabilities.]

Drawing Graphics Using Paint and Canvas

We use methods of class Canvas (package android.graphics) to draw text, lines and circles. A Canvas draws on a View’s Bitmap. Each drawing method in class Canvas uses an object of class Paint (package android.graphics) to specify drawing characteristics, including color, line thickness, font size and more. These capabilities are presented with the drawGameElements method in Section 7.5.3. For more details on the drawing characteristics you can specify with a Paint object, visit

developer.android.com/reference/android/graphics/Paint.html

7.4. Building the App’s GUI and Resource Files

In this section, you’ll create the app’s resource files and main.xml layout file.

7.4.1. Creating the Project

Begin by creating a new Android project named CannonGame. Specify the following values in the New Android Project dialog, then press Finish:

Build Target: Ensure that Android 2.3.3 is checked

Application name: Cannon Game

Package name: com.deitel.cannongame

Create Activity: CannonGame

Min SDK Version: 8.

7.4.2. AndroidManifest.xml

Figure 7.3 shows this app’s AndroidManifest.xml file. As in Section 6.6, we set the activity element’s android:screenOrientation attribute to "portrait" (line 9) so that the app always displays in portrait mode.


 1   <?xml version="1.0" encoding="utf-8"?>
 2   <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 3      package="com.deitel.cannongame" android:versionCode="1"
 4      android:versionName="1.0">
 5      <application android:icon="@drawable/icon"
 6         android:label="@string/app_name" android:debuggable="true">
 7         <activity android:name=".CannonGame"
 8            android:label="@string/app_name"
 9            android:screenOrientation="portrait">
10            <intent-filter>
11               <action android:name="android.intent.action.MAIN" />
12               <category android:name="android.intent.category.LAUNCHER" />
13            </intent-filter>
14         </activity>
15      </application>
16      <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10"/>
17   </manifest>


Fig. 7.3. AndroidManifest.xml.

7.4.3. strings.xml

We’ve specified format Strings (Fig. 7.4. lines 4–5 and 9–10) in this app’s strings.xml file. As mentioned in Section 7.3, format Strings that contain multiple format specifiers must number the format specifiers for localization purposes. The notation 1$ in %1$.1f (line 5) indicates that the first argument after the format String should replace the format specifier %1$d. Similarly, %2$.1f indicates that the second argument after the format String should replace the format specifier %2$.1f. The d in the first format specifier indicates that we’re formatting a decimal integer and the f in the second one indicates that we’re formatting a floating-point value. In localized versions of strings.xml, the format specifiers %1$d and %2$.1f can be reordered as necessary—the first argument after the format String in a call to Resourcesmethod getString or String method format will replace %1$d—regardless of where it appears in the format String—and the second argument will replace %2$.1f regardless of where they appear in the format String.


 1   <?xml version="1.0" encoding="UTF-8"?>
 2   <resources>
 3      <string name="app_name">Cannon Game</string>
 4      <string name="results_format">
 5         Shots fired: %1$d Total time: %2$.1f</string>
 6      <string name="reset_game">Reset Game</string>
 7      <string name="win">You win!</string>
 8      <string name="lose">You lose!</string>
 9      <string name="time_remaining_format">
10         Time remaining: %.1f seconds</string>
11   </resources>


Fig. 7.4. Strings defined in strings.xml.

7.4.4. main.xml

In this app, we deleted the default main.xml file and replaced it with one containing a FrameLayout. The only component in this app’s layout is an instance of our custom View subclass, CannonView, which you’ll add to the project in Section 7.5.3. Figure 7.5 shows the completed main.xml in which we manually entered the XML element shown in lines 2–7. That element indicates that the CannonView should occupy the entire width and height of the parent layout and should have a white background. Recall from Section 7.3 that you must fully qualify a custom View’s class name in the layout XML, so line 2 refers to the CannonView as com.deitel.cannongame.CannonView.


1   <?xml version="1.0" encoding="utf-8"?>
2   <com.deitel.cannongame.CannonView                            
3      xmlns:android="http://schemas.android.com/apk/res/android"
4      android:id="@+id/cannonView"                              
5      android:layout_width="match_parent"                       
6      android:layout_height="match_parent"                      
7      android:background="@android:color/white"/>               


Fig. 7.5. Cannon Game app’s XML layout (main.xml).

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

2. Specify the folder name raw and click Finish to create the folder.

3. Drag the sound files into the res/raw folder.

7.5. Building the App

This app consists of three classes—Line (Fig. 7.6), CannonGame (the Activity subclass; Figs. 7.77.10) and CannonView (Figs. 7.117.23).


 1   // Line.java
 2   // Class Line represents a line with two endpoints.
 3   package com.deitel.cannongame;
 4
 5   import android.graphics.Point;
 6
 7   public class Line
 8   {
 9      public Point start; // starting Point
10      public Point end; // ending Point
11
12      // default constructor initializes Points to the origin (0, 0)
13      public Line()
14      {
15         start = new Point(0, 0); // start Point
16         end = new Point(0, 0); // end Point
17      } // end method Line
18   } // end class Line


Fig. 7.6. Class Line represents a line with two endpoints.

7.5.1. Line Class Maintains a Line’s Endpoints

Class Line (Fig. 7.6) simply groups two Points that represent a line’s starting Point and ending Point. We use objects of this class to define the blocker and target. To add class Line to the project:

1. Expand the project’s src node in the Package Explorer.

2. Right click the package (com.deitel.cannongame) and select New > Class to display the New Java Class dialog.

3. In the dialog’s Name field, enter Line and click Finish.

4. Enter the code in Fig. 7.6 into the Line.java file.

7.5.2. CannonGame Subclass of Activity

Class CannonGame (Figs. 7.77.10) is the Cannon Game app’s main Activity.


 1   // CannonGame.java
 2   // Main Activity for the Cannon Game app.
 3   package com.deitel.cannongame;
 4
 5   import android.app.Activity;
 6   import android.os.Bundle;
 7   import android.media.AudioManager;                          
 8   import android.view.GestureDetector;                        
 9   import android.view.MotionEvent;                            
10   import android.view.GestureDetector.SimpleOnGestureListener;
11
12   public class CannonGame extends Activity
13   {
14      private GestureDetector gestureDetector; // listens for double taps
15      private CannonView cannonView; // custom view to display the game
16


Fig. 7.7. CannonGame package statement, import statements and instance variables.

package Statement, import Statements and Instance Variables

Section 7.3 discussed the key new classes and interfaces that class CannonGame uses. We’ve highlighted these classes and interfaces in Fig. 7.7. Line 15 declares variable cannonView, which will enable class CannonGame to interact with the CannonView.

Overriding Activity Methods onCreate, onPause and onDestroy

Figure 7.8 presents overridden Activity methods onCreate (lines 18–32), onPause (lines 35–40) and onDestroy (lines 43–48). Method onCreate inflates the activity’s main.xml layout, then gets a reference to the CannonView object (line 25). Line 28 creates the GestureDetector that detects double taps for this activity using the gestureListener, which is defined in Fig. 7.10. Line 31 allows the game’s audio volume to be controlled by the device’s volume keys.


17      // called when the app first launches
18      @Override
19      public void onCreate(Bundle savedInstanceState)
20      {
21         super.onCreate(savedInstanceState); // call super's onCreate method
22         setContentView(R.layout.main); // inflate the layout
23
24         // get the CannonView
25         cannonView = (CannonView) findViewById(R.id.cannonView);
26
27         // initialize the GestureDetector
28         gestureDetector = new GestureDetector(this, gestureListener);
29
30         // allow volume keys to set game volume
31         setVolumeControlStream(AudioManager.STREAM_MUSIC);
32      } // end method onCreate
33
34      // when the app is pushed to the background, pause it
35      @Override
36      public void onPause()
37      {
38         super.onPause(); // call the super method
39         cannonView.stopGame(); // terminates the game
40      } // end method onPause
41
42      // release resources
43      @Override
44      protected void onDestroy()
45      {
46         super.onDestroy();
47         cannonView.releaseResources();
48      } // end method onDestroy
49


Fig. 7.8. Overriding Activity methods onCreate, onPause and onDestroy.

Method onPause (lines 35–40) ensures that the CannonGame activity does not continue executing when it’s sent to the background. If the game did continue executing, not only would the user not be able to interact with the game because another activity has the focus, but the app would also continue consuming battery power—a precious resource for mobile devices. When onPause is called, line 39 calls the cannonView’s stopGame method (Fig. 7.21) to terminate the game’s thread—we don’t save the game’s state in this example.

When the activity is shut down, method onDestroy (lines 43–46) calls the cannonView’s releaseResources method (Fig. 7.21), which releases the app’s sound resources.

Overriding Activity Method onTouchEvent

In this example, we override method onTouchEvent (Fig. 7.9) to determine when the user touches the screen or moves a finger across the screen. The MotionEvent parameter contains information about the event that occurred. Line 55 uses the MotionEvent’s getAction method to determine which type of event occurred. Then, lines 58–59 determine whether the user touched the screen (MotionEvent.ACTION_DOWN) or moved a finger across the screen (MotionEvent.ACTION_MOVE). In either case, line 61 calls the cannonView’s alignCannon method (Fig. 7.18) to aim the cannon towards that touch point. Line 65 then passes the MotionEvent object to the gestureDetector’s onTouchEvent method to check whether a double tap occurred.


50      // called when the user touches the screen in this Activity
51      @Override
52      public boolean onTouchEvent(MotionEvent event)
53      {
54         // get int representing the type of action which caused this event
55         int action = event.getAction();
56
57         // the user user touched the screen or dragged along the screen
58         if (action == MotionEvent.ACTION_DOWN ||
59            action == MotionEvent.ACTION_MOVE)
60         {
61            cannonView.alignCannon(event); // align the cannon
62         } // end if
63
64         // call the GestureDetector's onTouchEvent method
65         return gestureDetector.onTouchEvent(event);
66      } // end method onTouchEvent


Fig. 7.9. Overriding Activity method onTouchEvent.

Anonymous Inner Class That Extends SimpleOnGestureListener

Figure 7.10 creates the SimpleGestureListener named gestureListener which was registered at line 28 with the GestureDetector. Recall that SimpleGestureListener is an adapter class that implements all the methods of interfaces OnGestureListener and OnDoubleTapListener. The methods simply return false—indicating that the events were not handled. We override only the onDoubleTap method (lines 71–76), which is called when the user double taps the screen. Line 74 calls CannonView’s fireCannonBall method (Fig. 7.17) to fire a cannonball. Method fireCannonBall obtains the screen location of the double-tap from its MotionEvent argument—this is used to aim the shot at the correct angle. Line 75 returns true indicating that the event was handled.


67      // listens for touch events sent to the GestureDetector
68      SimpleOnGestureListener gestureListener = new SimpleOnGestureListener()
69      {
70         // called when the user double taps the screen
71         @Override
72         public boolean onDoubleTap(MotionEvent e)
73         {
74            cannonView.fireCannonball(e); // fire the cannonball
75            return true; // the event was handled
76         } // end method onDoubleTap
77      }; // end gestureListener
78   } // end class CannonGame


Fig. 7.10. Anonymous inner class that extends SimpleOnGestureListener.

7.5.3. CannonView Subclass of View

Class CannonView (Figs. 7.117.23) is a custom subclass of View that implements the Cannon Game’s logic and draws game objects on the screen. To add the class to the project:

1. Expand the project’s src node in the Package Explorer.

2. Right click the package (com.deitel.cannongame) and select New > Class to display the New Java Class dialog.

3. In the dialog’s Name field, enter CannonView, in the Superclass field enter android.view.View, then click Finish.

4. Enter the code in Figs. 7.117.21 into the CannonView.java file.


 1   // CannonView.java
 2   // Displays the Cannon Game
 3   package com.deitel.cannongame;
 4
 5   import java.util.HashMap;
 6   import java.util.Map;
 7
 8   import android.app.Activity;
 9   import android.app.AlertDialog;
10   import android.content.Context;
11   import android.content.DialogInterface;
12   import android.graphics.Canvas;   
13   import android.graphics.Color;    
14   import android.graphics.Paint;    
15   import android.graphics.Point;    
16   import android.media.AudioManager;
17   import android.media.SoundPool;   
18   import android.util.AttributeSet; 
19   import android.view.MotionEvent;  
20   import android.view.SurfaceHolder;
21   import android.view.SurfaceView;  
22


Fig. 7.11. CannonView class’s package and import statements.

package and import Statements

Figure 7.11 lists the package statement and the import statements for class CannonView. Section 7.3 discussed the key new classes and interfaces that class CannonView uses. We’ve highlighted them in Fig. 7.11.

CannonView Instance Variables and Constants

Figure 7.12 lists the large number of class CannonView’s constants and instance variables. Most are self explanatory, but we’ll explain each as we encounter it in the discussion.


23   public class CannonView extends SurfaceView   
24      implements SurfaceHolder.Callback          
25   {
26      private CannonThread cannonThread; // controls the game loop
27      private Activity activity; // to display Game Over dialog in GUI thread
28      private boolean dialogIsDisplayed = false;
29
30      // constants for game play
31      public static final int TARGET_PIECES = 7; // sections in the target
32      public static final int MISS_PENALTY = 2; // seconds deducted on a miss
33      public static final int HIT_REWARD = 3; // seconds added on a hit
34
35      // variables for the game loop and tracking statistics
36      private boolean gameOver; // is the game over?
37      private double timeLeft; // the amount of time left in seconds
38      private int shotsFired; // the number of shots the user has fired
39      private double totalTimeElapsed; // the number of seconds elapsed
40
41      // variables for the blocker and target
42      private Line blocker; // start and end points of the blocker
43      private int blockerDistance; // blocker distance from left
44      private int blockerBeginning; // blocker distance from top
45      private int blockerEnd; // blocker bottom edge distance from top
46      private int initialBlockerVelocity; // initial blocker speed multiplier
47      private float blockerVelocity; // blocker speed multiplier during game
48
49      private Line target; // start and end points of the target
50      private int targetDistance; // target distance from left
51      private int targetBeginning; // target distance from top
52      private double pieceLength; // length of a target piece
53      private int targetEnd; // target bottom's distance from top
54      private int initialTargetVelocity; // initial target speed multiplier
55      private float targetVelocity; // target speed multiplier during game
56
57      private int lineWidth; // width of the target and blocker
58      private boolean[] hitStates; // is each target piece hit?
59      private int targetPiecesHit; // number of target pieces hit (out of 7)
60
61      // variables for the cannon and cannonball
62      private Point cannonball; // cannonball image's upper-left corner
63      private int cannonballVelocityX; // cannonball's x velocity
64      private int cannonballVelocityY; // cannonball's y velocity
65      private boolean cannonballOnScreen; // is the cannonball on the screen
66      private int cannonballRadius; // cannonball radius
67      private int cannonballSpeed; // cannonball speed
68      private int cannonBaseRadius; // cannon base radius
69      private int cannonLength; // cannon barrel length
70      private Point barrelEnd; // the endpoint of the cannon's barrel
71      private int screenWidth; // width of the screen
72      private int screenHeight; // height of the screen
73
74      // constants and variables for managing sounds
75      private static final int TARGET_SOUND_ID = 0;
76      private static final int CANNON_SOUND_ID = 1;
77      private static final int BLOCKER_SOUND_ID = 2;
78      private SoundPool soundPool; // plays sound effects
79      private Map<Integer, Integer> soundMap; // maps IDs to SoundPool
80
81      // Paint variables used when drawing each item on the screen
82      private Paint textPaint; // Paint used to draw text
83      private Paint cannonballPaint; // Paint used to draw the cannonball
84      private Paint cannonPaint; // Paint used to draw the cannon
85      private Paint blockerPaint; // Paint used to draw the blocker
86      private Paint targetPaint; // Paint used to draw the target
87      private Paint backgroundPaint; // Paint used to clear the drawing area
88


Fig. 7.12. CannonView class’s fields.

CannonView Constructor

Figure 7.13 shows class CannonView’s constructor. When a View is inflated, its constructor is called and passed a Context and an AttributeSet as arguments. In this case, the Context is the Activity (CannonGame) to which the CannonView is attached and the AttributeSet (package android.util) contains the values for any attributes that are set in the layout’s XML document. These arguments should be passed to the superclass constructor (line 92) to ensure that the custom View object is properly configured with the values of any standard View attributes specified in the XML.


89      // public constructor
90      public CannonView(Context context, AttributeSet attrs)
91      {
92         super(context, attrs); // call super's constructor
93         activity = (Activity) context;
94
95         // register SurfaceHolder.Callback listener
96         getHolder().addCallback(this);
97
98         // initialize Lines and points representing game items
99         blocker = new Line(); // create the blocker as a Line
100        target = new Line(); // create the target as a Line
101        cannonball = new Point(); // create the cannonball as a point
102
103        // initialize hitStates as a boolean array
104        hitStates = new boolean[TARGET_PIECES];
105
106        // initialize SoundPool to play the app's three sound effects
107        soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
108
109        // create Map of sounds and pre-load sounds
110        soundMap = new HashMap<Integer, Integer>(); // create new HashMap
111        soundMap.put(TARGET_SOUND_ID,                                    
112           soundPool.load(context, R.raw.target_hit, 1));                
113        soundMap.put(CANNON_SOUND_ID,                                    
114           soundPool.load(context, R.raw.cannon_fire, 1));               
115        soundMap.put(BLOCKER_SOUND_ID,                                   
116           soundPool.load(context, R.raw.blocker_hit, 1));               
117
118        // construct Paints for drawing text, cannonball, cannon,
119        // blocker and target; these are configured in method onSizeChanged
120        textPaint = new Paint(); // Paint for drawing text
121        cannonPaint = new Paint(); // Paint for drawing the cannon
122        cannonballPaint = new Paint(); // Paint for drawing a cannonball
123        blockerPaint = new Paint(); // Paint for drawing the blocker
124        targetPaint = new Paint(); // Paint for drawing the target
125        backgroundPaint = new Paint(); // Paint for drawing the target
126     } // end CannonView constructor
127


Fig. 7.13. CannonView constructor.

Line 93 stores a reference to the parent Activity so we can use it at the end of a game to display an AlertDialog from the Activity’s GUI thread. Line 96 registers this (i.e., the CannonView) as the object that implements SurfaceHolder.Callback to receive the method calls that indicate when the SurfaceView is created, updated and destroyed. SurfaceView method getHolder returns the corresponding SurfaceHolder object for managing the SurfaceView, and SurfaceHolder method addCallback stores the object that implements SurfaceHolder.Callback.

Lines 99–101 create the blocker and target as Lines and the cannonball as a Point. Next, we create boolean array hitStates to keep track of which of the target’s seven pieces have been hit (and thus should not be drawn).

Lines 107–116 configure the sounds that we use in the app. First, we create the SoundPool that’s used to load and play the app’s sound effects. The constructor’s first argument represents the maximum number of simultaneous sound streams that can play at once. We play only one sound at a time, so we pass 1. The second argument specifies which audio stream will be used to play the sounds. There are seven sound streams identified by constants in class AudioManager, but the documentation for class SoundPool recommends using the stream for playing music (AudioManager.STREAM_MUSIC) for sound in games. The last argument represents the sound quality, but the documentation indicates that this value is not currently used and 0 should be specified as the default value.

Line 110 creates a HashMap (soundMap). Then, lines 111–116 populate it, using the constants at lines 75–77 as keys. The corresponding values are the return values of the SoundPool’s load method, which returns an ID that can be used to play (or unload) a sound. SoundPool method load receives three arguments—the application’s Context, a resource ID representing the sound file to load and the sound’s priority. According to the documentation for this method, the last argument is not currently used and should be specified as 1.

Lines 120–125 create the Paint objects that are used when drawing the game’s objects. We configure these in method onSizeChanged, because some of the Paint settings depend on scaling the game elements based on the device’s screen size.

Overriding View Method onSizeChanged

Figure 7.14 overrides class View’s onSizeChanged method, which is called whenever the View’s size changes, including when the View is first added to the View hierarchy as the layout is inflated. This app always displays in portrait mode, so onSizeChanged is called only once when the activity’s onCreate method inflates the GUI. The method receives the View’s new width and height and its old width and height—when this method is called the first time, the old width and height are 0. The calculations performed here scale the game’s on-screen elements based on the device’s pixel width and height—we arrived at our scaling factors via trial and error. After the calculations, line 173 calls method newGame (Fig. 7.15).


128     // called when the size of this View changes--including when this
129     // view is first added to the view hierarchy
130     @Override
131     protected void onSizeChanged(int w, int h, int oldw, int oldh)
132     {
133        super.onSizeChanged(w, h, oldw, oldh);
134
135        screenWidth = w; // store the width
136        screenHeight = h; // store the height
137        cannonBaseRadius = h / 18; // cannon base radius 1/18 screen height
138        cannonLength = w / 8; // cannon length 1/8 screen width
139
140        cannonballRadius = w / 36; // cannonball radius 1/36 screen width
141        cannonballSpeed = w * 3 / 2; // cannonball speed multiplier
142
143        lineWidth = w / 24; // target and blocker 1/24 screen width
144
145        // configure instance variables related to the blocker
146        blockerDistance = w * 5 / 8; // blocker 5/8 screen width from left
147        blockerBeginning = h / 8; // distance from top 1/8 screen height
148        blockerEnd = h * 3 / 8; // distance from top 3/8 screen height
149        initialBlockerVelocity = h / 2; // initial blocker speed multiplier
150        blocker.start = new Point(blockerDistance, blockerBeginning);
151        blocker.end = new Point(blockerDistance, blockerEnd);
152
153        // configure instance variables related to the target
154        targetDistance = w * 7 / 8; // target 7/8 screen width from left
155        targetBeginning = h / 8; // distance from top 1/8 screen height
156        targetEnd = h * 7 / 8; // distance from top 7/8 screen height
157        pieceLength = (targetEnd - targetBeginning) / TARGET_PIECES;
158        initialTargetVelocity = -h / 4; // initial target speed multiplier
159        target.start = new Point(targetDistance, targetBeginning);
160        target.end = new Point(targetDistance, targetEnd);
161
162        // endpoint of the cannon's barrel initially points horizontally
163        barrelEnd = new Point(cannonLength, h / 2);
164
165        // configure Paint objects for drawing game elements
166        textPaint.setTextSize(w / 20); // text size 1/20 of screen width   
167        textPaint.setAntiAlias(true); // smoothes the text                 
168        cannonPaint.setStrokeWidth(lineWidth * 1.5f); // set line thickness
169        blockerPaint.setStrokeWidth(lineWidth); // set line thickness      
170        targetPaint.setStrokeWidth(lineWidth); // set line thickness       
171        backgroundPaint.setColor(Color.WHITE); // set background color     
172
173        newGame(); // set up and start a new game
174     } // end method onSizeChanged
175


Fig. 7.14. Overridden onSizeChanged method.


176     // reset all the screen elements and start a new game
177     public void newGame()
178     {
179        // set every element of hitStates to false--restores target pieces
180        for (int i = 0; i < TARGET_PIECES; ++i)
181           hitStates[i] = false;
182
183        targetPiecesHit = 0; // no target pieces have been hit
184        blockerVelocity = initialBlockerVelocity; // set initial velocity
185        targetVelocity = initialTargetVelocity; // set initial velocity
186        timeLeft = 10; // start the countdown at 10 seconds
187        cannonballOnScreen = false; // the cannonball is not on the screen
188        shotsFired = 0; // set the initial number of shots fired
189        totalElapsedTime = 0.0; // set the time elapsed to zero
190        blocker.start.set(blockerDistance, blockerBeginning);
191        blocker.end.set(blockerDistance, blockerEnd);
192        target.start.set(targetDistance, targetBeginning);
193        target.end.set(targetDistance, targetEnd);
194
195        if (gameOver)
196        {
197           gameOver = false; // the game is not over
198           cannonThread = new CannonThread(getHolder());
199           cannonThread.start();
200        } // end if
201     } // end method newGame
202


Fig. 7.15. CannonView method newGame.

CannonView Method newGame

Method newGame (Fig. 7.15) resets the initial values of the instance variables that are used to control the game. If variable gameOver is true, which occurs only after the first game completes, line 197 resets gameOver and lines 198–199 create a new CannonThread and start it to begin the new game.

CannonView Method updatePositions

Method updatePositions (Fig. 7.16) is called by the CannonThread’s run method (Fig. 7.23) to update the on-screen elements’ positions and to perform simple collision detection. The new locations of the game elements are calculated based on the elapsed time in milliseconds between the previous frame of the animation and the current frame of the animation. This enables the game to update the amount by which each game element moves based on the device’s refresh rate. We discuss this in more detail when we cover game loops in Fig. 7.23.


203     // called repeatedly by the CannonThread to update game elements
204     private void updatePositions(double elapsedTimeMS)
205     {
206        double interval = elapsedTimeMS / 1000.0; // convert to seconds
207
208        if (cannonballOnScreen) // if there is currently a shot fired
209        {
210           // update cannonball position
211           cannonball.x += interval * cannonballVelocityX;
212           cannonball.y += interval * cannonballVelocityY;
213
214           // check for collision with blocker
215           if (cannonball.x + cannonballRadius > blockerDistance &&
216              cannonball.x - cannonballRadius < blockerDistance &&
217              cannonball.y + cannonballRadius > blocker.start.y &&
218              cannonball.y - cannonballRadius < blocker.end.y)
219           {
220              cannonballVelocityX *= -1; // reverse cannonball's direction
221              timeLeft -= MISS_PENALTY; // penalize the user
222
223              // play blocker sound
224              soundPool.play(soundMap.get(BLOCKER_SOUND_ID), 1, 1, 1, 0, 1f)
225           } // end if
226
227           // check for collisions with left and right walls
228           else if (cannonball.x + cannonballRadius > screenWidth ||
229              cannonball.x - cannonballRadius < 0)
230              cannonballOnScreen = false; // remove cannonball from screen
231
232           // check for collisions with top and bottom walls
233           else if (cannonball.y + cannonballRadius > screenHeight ||
234              cannonball.y - cannonballRadius < 0)
235              cannonballOnScreen = false; // make the cannonball disappear
236
237           // check for cannonball collision with target
238           else if (cannonball.x + cannonballRadius > targetDistance &&
239              cannonball.x - cannonballRadius < targetDistance &&
240              cannonball.y + cannonballRadius > target.start.y &&
241              cannonball.y - cannonballRadius < target.end.y)
242           {
243              // determine target section number (0 is the top)
244              int section =
245                 (int) ((cannonball.y - target.start.y) / pieceLength);
246
247              // check if the piece hasn't been hit yet
248              if ((section >= 0 && section < TARGET_PIECES) &&
249                 !hitStates[section])
250              {
251                 hitStates[section] = true; // section was hit
252                 cannonballOnScreen = false; // remove cannonball
253                 timeLeft += HIT_REWARD; // add reward to remaining time
254
255                 // play target hit sound
256                 soundPool.play(soundMap.get(TARGET_SOUND_ID), 1,
257                    1, 1, 0, 1f);                                
258
259                 // if all pieces have been hit
260                 if (++targetPiecesHit == TARGET_PIECES)
261                 {
262                    cannonThread.setRunning(false);
263                    showGameOverDialog(R.string.win); // show winning dialog
264                    gameOver = true; // the game is over
265                 } // end if
266              } // end if
267           } // end else if
268        } // end if
269
270        // update the blocker's position
271        double blockerUpdate = interval * blockerVelocity;
272        blocker.start.y += blockerUpdate;
273        blocker.end.y += blockerUpdate;
274
275        // update the target's position
276        double targetUpdate = interval * targetVelocity;
277        target.start.y += targetUpdate;
278        target.end.y += targetUpdate;
279
280        // if the blocker hit the top or bottom, reverse direction
281        if (blocker.start.y < 0 || blocker.end.y > screenHeight)
282           blockerVelocity *= -1;
283
284        // if the target hit the top or bottom, reverse direction
285        if (target.start.y < 0 || target.end.y > screenHeight)
286           targetVelocity *= -1;
287
288        timeLeft -= interval; // subtract from time left
289
290        // if the timer reached zero
291        if (timeLeft <= 0)
292        {
293           timeLeft = 0.0;
294           gameOver = true; // the game is over
295           cannonThread.setRunning(false);
296           showGameOverDialog(R.string.lose); // show the losing dialog
297        } // end if
298     } // end method updatePositions
299


Fig. 7.16. CannonView method updatePositions.

Line 206 converts the elapsed time since the last animation frame from milliseconds to seconds. This value is used to modify the positions of various game elements.

Line 208 checks whether the cannonball is on the screen. If it is, we update its position by adding the distance it should have traveled since the last timer event. This is calculated by multiplying its velocity by the amount of time that passed (lines 211–212). Lines 215–218 check whether the cannonball has collided with the blocker. We perform simple collision detection, based on the rectangular boundary of the cannonball. There are four conditions that must be met if the cannonball is in contact with the blocker:

• The cannonball’s x-coordinate plus the cannon ball’s radius must be greater than the blocker’s distance from the left edge of the screen (blockerDistance) (line 215). This means that the cannonball has reached the blocker’s distance from the left edge of the screen.

• The cannonball’s x-coordinate minus the cannon ball’s radius must also be less than the blocker’s distance from the left edge of the screen (line 216). This ensures that the cannonball has not yet passed the blocker.

• Part of the cannonball must be lower than the top of the blocker (line 217).

• Part of the cannonball must be higher than the bottom of the blocker (line 218).

If all these conditions are met, we reverse the cannonball’s direction on the screen (line 220), penalize the user by subtracting MISS_PENALTY from timeLeft, then call soundPool’s play method to play the blocker hit sound—BLOCKER_SOUND_ID is used as the soundMap key to locate the sound’s ID in the SoundPool.

We remove the cannonball if it reaches any of the screen’s edges. Lines 228–230 test whether the cannonball has collided with the left or right wall and, if it has, remove the cannonball from the screen. Lines 233–235 remove the cannonball if it collides with the top or bottom of the screen.

We then check whether the cannonball has hit the target (lines 238–241). These conditions are similar to those used to determine whether the cannonball collided with the blocker. If the cannonball hit the target, we determine which section of the target was hit. Lines 244–245 determine which section has been hit—dividing the distance between the cannonball and the bottom of the target by the length of a piece. This expression evaluates to 0 for the top-most section and 6 for the bottom-most. We check whether that section was previously hit, using the hitStates array (line 249). If it wasn’t, we set the corresponding hitStates element to true and remove the cannonball from the screen. We then add HIT_REWARD to timeLeft, increasing the game’s time remaining, and play the target hit sound (TARGET_SOUND_ID). We increment targetPiecesHit, then determine whether it’s equal to TARGET_PIECES (line 260). If so, the game is over, so we terminate the CannonThread by calling its setRunning method with the argument false, invoke method showGameOverDialog with the String resource ID representing the winning message and set gameOver to true.

Now that all possible cannonball collisions have been checked, the blocker and target positions must be updated. Lines 271–273 change the blocker’s position by multiplying blockerVelocity by the amount of time that has passed since the last update and adding that value to the current x- and y-coordinates. Lines 276–278 do the same for the target. If the blocker has collided with the top or bottom wall, its direction is reversed by multiplying its velocity by -1 (lines 281–282). Lines 285–286 perform the same check and adjustment for the full length of the target, including any sections that have already been hit.

We decrease timeLeft by the time that has passed since the prior animation frame. If timeLeft has reached zero, the game is over—we set timeLeft to 0.0 just in case it was negative; otherwise, we’ll sometimes display a negative final time on the screen). Then we set gameOver to true, terminate the CannonThread by calling its setRunning method with the argument false and call method showGameOverDialog with the String resource ID representing the losing message.

CannonView Method fireCannonball

When the user double taps the screen, the event handler for that event (Fig. 7.10) calls method fireCannonball (Fig. 7.17) to fire a cannonball. If there’s already a cannonball on the screen, the method returns immediately; otherwise, it fires the cannon. Line 306 calls alignCannon to aim the cannon at the double-tap point and get the cannon’s angle. Lines 309–310 “load” the cannon (that is, position the cannonball inside the cannon). Then, lines 313 and 316 calculate the horizontal and vertical components of the cannonball’s velocity. Next, we set cannonballOnScreen to true so that the cannonball will be drawn by method drawGameElements (Fig. 7.19) and increment shotsFired. Finally, we play the cannon’s firing sound (CANNON_SOUND_ID).


300     // fires a cannonball
301     public void fireCannonball(MotionEvent event)
302     {
303        if (cannonballOnScreen) // if a cannonball is already on the screen
304           return; // do nothing
305
306        double angle = alignCannon(event); // get the cannon barrel's angle
307
308        // move the cannonball to be inside the cannon
309        cannonball.x = cannonballRadius; // align x-coordinate with cannon
310        cannonball.y = screenHeight / 2; // centers ball vertically
311
312        // get the x component of the total velocity
313        cannonballVelocityX = (int) (cannonballSpeed * Math.sin(angle));
314
315        // get the y component of the total velocity
316        cannonballVelocityY = (int) (-cannonballSpeed * Math.cos(angle));
317        cannonballOnScreen = true; // the cannonball is on the screen
318        ++shotsFired; // increment shotsFired
319
320        // play cannon fired sound
321        soundPool.play(soundMap.get(CANNON_SOUND_ID), 1, 1, 1, 0, 1f);
322     } // end method fireCannonball
323


Fig. 7.17. CannonView method fireCannonball.

CannonView Method alignCannon

Method alignCannon (Fig. 7.18) aims the cannon at the point where the user double tapped the screen. Line 328 gets the x- and y-coordinates of the double tap from the MotionEvent argument. We compute the vertical distance of the touch from the center of the screen. If this is not zero, we calculate cannon barrel’s angle from the horizontal (line 338). If the touch is on the lower-half of the screen we adjust the angle by Math.PI (line 342). We then use the cannonLength and the angle to determine the x and y coordinate values for the endpoint of the cannon’s barrel—this is used to draw a line from the cannon base’s center at the left edge of the screen to the cannon’s barrel endpoint.


324     // aligns the cannon in response to a user touch
325     public double alignCannon(MotionEvent event)
326     {
327        // get the location of the touch in this view
328        Point touchPoint = new Point((int) event.getX(), (int) event.getY());
329
330        // compute the touch's distance from center of the screen
331        // on the y-axis
332        double centerMinusY = (screenHeight / 2 - touchPoint.y);
333
334        double angle = 0; // initialize angle to 0
335
336        // calculate the angle the barrel makes with the horizontal
337        if (centerMinusY != 0) // prevent division by 0
338           angle = Math.atan((double) touchPoint.x / centerMinusY);
339
340        // if the touch is on the lower half of the screen
341        if (touchPoint.y > screenHeight / 2)
342           angle += Math.PI; // adjust the angle
343
344        // calculate the endpoint of the cannon barrel
345        barrelEnd.x = (int) (cannonLength * Math.sin(angle));
346        barrelEnd.y =
347           (int) (-cannonLength * Math.cos(angle) + screenHeight / 2);
348
349        return angle; // return the computed angle
350     } // end method alignCannon
351


Fig. 7.18. CannonView method alignCannon.

Drawing the Game Elements

The method drawGameElements (Fig. 7.19) draws the cannon, cannonball, blocker and target on the SurfaceView using the Canvas that the CannonThread obtains from the SurfaceView’s SurfaceHolder.


352     // draws the game to the given Canvas
353     public void drawGameElements(Canvas canvas)
354     {
355        // clear the background
356        canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
357           backgroundPaint);
358
359        // display time remaining
360        canvas.drawText(getResources().getString(
361           R.string.time_remaining_format, timeLeft), 30, 50, textPaint);
362
363        // if a cannonball is currently on the screen, draw it
364        if (cannonballOnScreen)
365           canvas.drawCircle(cannonball.x, cannonball.y, cannonballRadius,
366              cannonballPaint);                                           
367
368        // draw the cannon barrel
369        canvas.drawLine(0, screenHeight / 2, barrelEnd.x, barrelEnd.y,
370           cannonPaint);                                              
371
372        // draw the cannon base
373        canvas.drawCircle(0, (int) screenHeight / 2,
374           (int) cannonBaseRadius, cannonPaint);    
375
376        // draw the blocker
377        canvas.drawLine(blocker.start.x, blocker.start.y, blocker.end.x,
378           blocker.end.y, blockerPaint);                                
379
380        Point currentPoint = new Point(); // start of current target section
381
382        // initialize curPoint to the starting point of the target
383        currentPoint.x = target.start.x;
384        currentPoint.y = target.start.y;
385
386        // draw the target
387        for (int i = 1; i <= TARGET_PIECES; ++i)
388        {
389           // if this target piece is not hit, draw it
390           if (!hitStates[i - 1])
391           {
392              // alternate coloring the pieces yellow and blue
393              if (i % 2 == 0)
394                 targetPaint.setColor(Color.YELLOW);
395              else
396                 targetPaint.setColor(Color.BLUE);
397
398              canvas.drawLine(currentPoint.x, currentPoint.y, target.end.x,
399                 (int) (currentPoint.y + pieceLength), targetPaint);       
400           }
401
402           // move curPoint to the start of the next piece
403           currentPoint.y += pieceLength;
404        } // end for
405     } // end method drawGameElements
406


Fig. 7.19. CannonView method drawGameElements.

First, we call Canvas’s drawRect method (lines 356–357) to clear the Canvas so that all the game elements can be displayed in their new positions. The method receives as arguments the rectangle’s upper-left x-y coordinates, the rectangle’s width and height, and the Paint object that specifies the drawing characteristics—recall that backgroundPaint sets the drawing color to white. Next, we call Canvas’s drawText method (lines 360–361) to display the time remaining in the game. We pass as arguments the String to be displayed, the x- and y-coordinates at which to display it and the textPaint (configured in lines 166–167) to describe how the text should be rendered (that is, the text’s font size, color and other attributes).

If the cannonball is on the screen, lines 365–366 use Canvas’s drawCircle method to draw the cannonball in its current position. The first two arguments represent the coordinates of the circle’s center. The third argument is the circle’s radius. The last argument is the Paint object specifying the circle’s drawing characteristics.

We use Canvas’s drawLine method to display the cannon barrel (lines 369–370), the blocker (lines 377–378) and the target pieces (lines 398–399). This method receives five parameters—the first four represent the x-y coordinates of the line’s start and end, and the last is the Paint object specifying the line’s characteristics, such as the line’s thickness.

Lines 373–374 use Canvas’s drawCircle method to draw the cannon’s half-circle base by drawing a circle that’s centered at the left edge of the screen—because a circle is displayed based on its center point, half of this circle is drawn off the left side of the SurfaceView.

Lines 380–404 draw the target sections. We iterate through the target’s sections, drawing each in the correct color—blue for the odd-numbered pieces and yellow for the others. Only those sections that haven’t been hit are displayed.

CannonView Method showGameOverDialog

When the game ends, the showGameOverDialog method (Fig. 7.20) displays an AlertDialog indicating whether the player won or lost, the number of shots fired and the total time elapsed. Lines 419–430 call the Builder’s setPositiveButton method to create a reset button. The onClick method of the button’s listener indicates that the dialog is no longer displayed and calls newGame to set up and start a new game. A dialog must be displayed from the GUI thread, so lines 432–440 call Activity method runOnUiThread and pass it an object of an anonymous inner class that implements Runnable. The Runnable’s run method indicates that the dialog is displayed and then displays it.


407     // display an AlertDialog when the game ends
408     private void showGameOverDialog(int messageId)
409     {
410        // create a dialog displaying the given String
411        final AlertDialog.Builder dialogBuilder =
412           new AlertDialog.Builder(getContext());
413        dialogBuilder.setTitle(getResources().getString(messageId));
414        dialogBuilder.setCancelable(false);
415
416        // display number of shots fired and total time elapsed
417        dialogBuilder.setMessage(getResources().getString(
418           R.string.results_format, shotsFired, totalElapsedTime));
419        dialogBuilder.setPositiveButton(R.string.reset_game,
420           new DialogInterface.OnClickListener()
421           {
422              // called when "Reset Game" Button is pressed
423              @Override
424              public void onClick(DialogInterface dialog, int which)
425              {
426                 dialogIsDisplayed = false;
427                 newGame(); // set up and start a new game
428              } // end method onClick
429           } // end anonymous inner class
430        ); // end call to setPositiveButton
431
432        activity.runOnUiThread(
433           new Runnable() {
434              public void run()
435              {
436                 dialogIsDisplayed = true;
437                 dialogBuilder.show(); // display the dialog
438              } // end method run
439           } // end Runnable
440        ); // end call to runOnUiThread
441     } // end method showGameOverDialog
442


Fig. 7.20. CannonView method showGameOverDialog.

CannonView Methods stopGame and releaseResources

Activity class CannonGame’s onPause and onDestroy methods (Fig. 7.8) call class CannonView’s stopGame and releaseResources methods (Fig. 7.21), respectively. Method stopGame (lines 444–448) is called from the main Activity to stop the game when the Activity’s onPause method is called—for simplicity, we don’t store the game’s state in this example. Method releaseResources (lines 451–455) calls the SoundPool’s release method to release the resources associated with the SoundPool.


443     // pauses the game
444     public void stopGame()
445     {
446        if (cannonThread != null)
447           cannonThread.setRunning(false);
448     } // end method stopGame
449
450     // releases resources; called by CannonGame's onDestroy method
451     public void releaseResources()
452     {
453        soundPool.release(); // release all resources used by the SoundPool
454        soundPool = null;
455     } // end method releaseResources
456


Fig. 7.21. CannonView methods stopGame and releaseResources.

Implementing the SurfaceHolder.Callback Methods

Figure 7.22 implements the surfaceChanged, surfaceCreated and surfaceDestroyed methods of interface SurfaceHolder.Callback. Method surfaceChanged has an empty body in this app because the app is always displayed in portrait view. This method is called when the SurfaceView’s size or orientation changes, and would typically be used to redisplay graphics based on those changes. Method surfaceCreated (lines 465–471) is called when the SurfaceView is created—e.g., when the app first loads or when it resumes from the background. We use surfaceCreated to create and start the CannonThread to begin the game. Method surfaceDestroyed (lines 474–492) is called when the SurfaceView is destroyed—e.g., when the app terminates. We use the method to ensure that the CannonThread terminates properly. First, line 479 calls CannonThread’s setRunning method with false as an argument to indicate that the thread should stop, then lines 481–491 wait for the thead to terminate. This ensures that no attempt is made to draw to the SurfaceView once surfaceDestroyed completes execution.


457     // called when surface changes size
458     @Override
459     public void surfaceChanged(SurfaceHolder holder, int format,
460        int width, int height)
461     {
462     } // end method surfaceChanged
463
464     // called when surface is first created
465     @Override
466     public void surfaceCreated(SurfaceHolder holder)
467     {
468        cannonThread = new CannonThread(holder);           
469        cannonThread.setRunning(true);                     
470        cannonThread.start(); // start the game loop thread
471     } // end method surfaceCreated
472
473     // called when the surface is destroyed
474     @Override
475     public void surfaceDestroyed(SurfaceHolder holder)
476     {
477        // ensure that thread terminates properly
478        boolean retry = true;
479        cannonThread.setRunning(false);
480
481        while (retry)
482        {
483           try
484           {
485              cannonThread.join();
486              retry = false;
487           } // end try
488           catch (InterruptedException e)
489           {
490           } // end catch
491        } // end while
492     } // end method surfaceDestroyed
493


Fig. 7.22. Implementing the SurfaceHolder.Callback methods.

CannonThread: Using a Thread to Create a Game Loop

Figure 7.23 defines a subclass of Thread which updates the game. The thread maintains a reference to the SurfaceView’s SurfaceHolder (line 497) and a boolean indicating whether the thread is running. The class’s run method (lines 514–543) drives the frame-by-frame animations—this is know as the game loop. Each update of the game elements on the screen is performed based on the number of milliseconds that have passed since the last update. Line 518 gets the system’s current time in milliseconds when the thread begins running. Lines 520–542 loop until threadIsRunning is false.


494     // Thread subclass to control the game loop
495     private class CannonThread extends Thread
496     {
497        private SurfaceHolder surfaceHolder; // for manipulating canvas
498        private boolean threadIsRunning = true; // running by default
499
500        // initializes the surface holder
501        public CannonThread(SurfaceHolder holder)
502        {
503           surfaceHolder = holder;
504           setName("CannonThread");
505        } // end constructor
506
507        // changes running state
508        public void setRunning(boolean running)
509        {
510           threadIsRunning = running;
511        } // end method setRunning
512
513        // controls the game loop
514        @Override
515        public void run()
516        {
517           Canvas canvas = null; // used for drawing
518           long previousFrameTime = System.currentTimeMillis();
519
520           while (threadIsRunning)
521           {
522              try
523              {
524                 canvas = surfaceHolder.lockCanvas(null);
525
526                 // lock the surfaceHolder for drawing
527                 synchronized(surfaceHolder)
528                 {
529                    long currentTime = System.currentTimeMillis();
530                    double elapsedTimeMS = currentTime - previousFrameTime;
531                    totalElapsedTime += elapsedTimeMS / 1000.0;
532                    updatePositions(elapsedTimeMS); // update game state
533                    drawGameElements(canvas); // draw                   
534                    previousFrameTime = currentTime; // update previous time
535                 } // end synchronized block
536              } // end try
537              finally
538              {
539                 if (canvas != null)
540                    surfaceHolder.unlockCanvasAndPost(canvas);
541              } // end finally
542           } // end while
543        } // end method run
544     } // end nested class CannonThread
545  } // end class CannonView


Fig. 7.23. Runnable that updates the game every TIME_INTERVAL milliseconds.

First we must obtain the Canvas for drawing on the SurfaceView by calling SurfaceHolder method lockCanvas (line 524). Only one thread at a time can draw to a SurfaceView, so we must first lock the SurfaceHolder, which we do with a synchronized block. Next, we get the current time in milliseconds, then calculate the elapsed time and add that to the total time that has elapsed so far—this will be used to help display the amount of time left in the game. Line 532 calls method updatePositions with the elapsed time in milliseconds as an argument—this moves all the game elements using the elapsed time to help scale the amount of movement. This helps ensure that the game operates at the same speed regardless of how fast the device is. If the time between frames is larger (i.e, the device is slower), the game elements will move further when each frame of the animation is displayed. If the time between frames is smaller (i.e, the device is faster), the game elements will move less when each frame of the animation is displayed. Finally, line 533 draws the game elements using the SurfaceView’s Canvas and line 534 stores the currentTime as the previousFrameTime to prepare to calculate the elapsed time in the next frame of the animation.

7.6. Wrap-Up

In this chapter, you created the Cannon Game app, which challenged the player to destroy a seven-piece target before a 10-second time limit expired. The user aimed the cannon by touching the screen. The cannon fired a cannonball when the user double-tapped the screen.

You learned how to define String resources to represent the format Strings that are used in calls to class Resource’s getString method and class String’s format method, and how to number format specifiers for localization purposes. You created a custom view by extending class SurfaceView and learned that custom component class names must be fully qualified in the XML layout element that represents the component.

We presented additional Activity lifecycle methods. You learned that method onPause is called for the current Activity when another activity receives the focus and that method onDestroy is called when the system shuts down an Activity.

You handled touches and single taps by overriding Activity’s onTouchEvent method. To handle the double taps that fired the cannon, you used a GestureDetector. You responded to the double tap event with a SimpleGestureListener that contained an overridden onDoubleTap method.

You added sound effects to the app’s res/raw folder and managed them with a SoundPool. You also used the system’s AudioManager service to obtain the device’s current music volume and use it as the playback volume.

This app manually performed its animations by updating the game elements on a SurfaceView from a separate thread of execution. To do this, extended class Thread and created a run method that displayed graphics with methods of class Canvas. You used the SurfaceView’s SurfaceHolder to obtain the appropriate Canvas. You also learned how to build a game loop that controls a game based on the amount of time that has elapsed between animation frames, so that the game will operate at the same overall speed on all devices.

The next chapter presents the SpotOn game app—our first Android 3.x app. SpotOn uses Android 3.x’s property animation to animate Views that contain images. The app tests the user’s reflexes by animating multiple spots that must be touched before they disappear.

Self-Review Exercises

7.1. Fill in the blanks in each of the following statements:

a. You can create a custom view by extending class View or __________.

b. To process simple touch events for an Activity, you can override class Activity’s onTouchEvent method then use constants from class __________ (package android.view) to test which type of event occurred and process it accordingly.

c. Each SurfaceView subclass should implement the interface __________, which contains methods that are called when the SurfaceView is created, changed (e.g., its size or orientation changes) or destroyed.

d. The d in a format specifier indicates that you’re formatting a decimal integer and the f in a format specifier indicates that you’re formatting a(n) __________ value.

e. Sound files are stored in the app’s __________ folder.

7.2. State whether each of the following is true or false. If false, explain why.

a. One use of onStop is to suspend a game play so that it does not continue executing when the user cannot interact with it.

b. The Android documentation recommends that games use the music audio stream to play sounds.

c. In Android, it’s important to maximize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.

d. A Canvas draws on a View’s Bitmap.

e. Format Strings that contain multiple format specifiers must number the format specifiers for localization purposes.

f. There are seven sound streams identified by constants in class AudioManager, but the documentation for class SoundPool recommends using the stream for playing music (AudioManager.STREAM_MUSIC) for sound in games.

g. Custom component class names must be fully qualified in the XML layout element that represents the component.

Answers to Self-Review Exercises

7.1.

a. one of its subclasses.

b. MotionEvent.

c. SurfaceHolder.Callback.

d. floating-point.

e. res/raw.

7.2.

a. False. One use of onPause is to suspend a game play so that it does not continue executing when the user cannot interact with it.

b. True.

c. False. In Android, it’s important to minimize the amount of work you do in the GUI thread to ensure that the GUI remains responsive and does not display ANR (Application Not Responding) dialogs.

d. True.

e. True.

f. True.

g. True.

Exercises

7.3. Fill in the blanks in each of the following statements:

a. Method __________ is called for the current Activity when another activity receives the focus, which sends the current activity to the background.

b. When an Activity is shut down, its __________ method is called.

c. A(n) __________ allows an app to react to more sophisticated user interactions such as flings, double-taps, long presses and scrolls.

d. Activity’s __________ method specifies that an app’s volume can be controlled with the device’s volume keys and should be the same as the device’s music playback volume. The method receives a constant from class AudioManager (package android.media).

e. Games often require complex logic that should be performed in separate threads of execution and those threads often need to draw to the screen. For such cases, Android provides class __________—a subclass of View to which any thread can draw.

f. Method __________ is called for the current Activity when another activity receives the focus.

7.4. State whether each of the following is true or false. If false, explain why.

a. Class SurfaceHolder also provides methods that give a thread shared access to the Canvas for drawing, because only one thread at a time can draw to a SurfaceView.

b. A MotionEvent.ACTION_TOUCH indicates that the user touched the screen and indicates that the user moved a finger across the screen (MotionEvent.ACTION_MOVE).

c. When a View is inflated, its constructor is called and passed a Context and an AttributeSet as arguments.

d. SoundPool method start receives three arguments—the application’s Context, a resource ID representing the sound file to load and the sound’s priority.

e. When a game loop controls a game based on the amount of time that has elapsed between animation frames, the game will operate at different speeds as appropriate for each device.

7.5. (Enhanced Cannon Game App) Modify the Cannon Game app as follows:

a. Use images for the cannon base and cannonball.

b. Play a sound when the blocker hits the top or bottom of the screen.

c. Play a sound when the target hits the top or bottom of the screen.

d. Allow the user to aim and fire the cannon using a single tap.

e. Enhance the app to have nine levels. In each level, the target should have the same number of target pieces as the level.

f. Keep score. Increase the user’s score for each target piece hit by 10 times the current level. Decrease the score by 15 times the current level each time the user hits the blocker. Display the highest score on the screen in the upper-left corner.

g. Save the top five high scores in a SharedPreferences file. When the game ends display an AlertDialog with the scores shown in descending order. If the user’s score is one of the top five, highlight that score by displaying an asterisk (*) next to it.

h. Add an explosion animation each time the cannonball hits one of the target pieces.

i. Make the game more difficult as it progresses by increasing the speed of the target and the blocker.

j. Add multiplayer functionality allowing two users to play on the same device.

k. Increase the number of obstacles between the cannon and the target.

l. Allow the user to move the cannon up and down the screen before aiming and firing.

m. Add a bonus round that lasts for four seconds. Change the color of the target and add music to indicate that it is a bonus round. If the user hits a piece of the target during those four seconds, give the user 1000 bonus points.

7.6. (Brick Game App) Create a game similar to the cannon game that shoots pellets at a stationary brick wall. The goal is to destroy enough of the wall to shoot the moving target behind it. The faster you break through the wall and get the target, the higher your score. Vary the color of the bricks and the number of shots required to destroy each—for example, red bricks can be destroyed in three shots, yellow bricks can be destroyed in six shots, etc. Include multiple layers to the wall and a small moving target (e.g., an icon, animal, etc.). Keep score. Increase difficulty with each round by adding more layers to the wall and increasing the speed of the moving target.

7.7. (Tablet App: Multiplayer Horse Race with Cannon Game) One of the most popular carnival or arcade games is the horse race. Each player is assigned a horse. To move the horse, the players must perform a skill—such as shooting a stream of water at a target. Each time a player hits a target, that player’s horse moves forward. The goal is to hit the target as many times as possible and as quickly as possible to move the horse toward the finish line and win the race.

Create a multiplayer tablet app that simulates the Horse Race game with two players. Instead of a stream of water, use the Cannon Game as the skill that will move each horse. Each time a player hits a target piece with the cannonball, move that player’s horse one position to the right.

Set the orientation of the screen to landscape and target API level 11 (Android 3.0) or higher so the game runs on tablets. Split the screen into three sections. The first section should run across the entire width of the top of the screen; this will be the race track. Below the race track, include two sections side-by-side. In each of these sections, include separate Cannon Games. The two players will need to be sitting side-by-side to play this version of the game. (In a later chapter, you’ll learn how to use Bluetooth, which you can then use to allow players to compete from separate devices.)

In the race track, include two horses that start on the left and move right toward a finish line at the right-side of the screen. Number the horses “1” and “2.”

Include the many sounds of a traditional horse race. You can find free audios online at websites such as www.audiomicro.com/ or create your own. Before the race, play an audio of the traditional bugle call—the “Call to Post”—that signifies to the horses to take their mark. Include the sound of the shot to start the race, followed by the announcer saying “And they’re off!”

7.8. (Bouncing Ball Game App) Create a game app in which the user’s goal is to prevent a bouncing ball from falling off the bottom of the screen. When the user presses the start button, a ball bounces off the top, left and right sides (the “walls”) of the screen. A horizontal bar on the bottom of the screen serves as a paddle to prevent the ball from hitting the bottom of the screen. (The ball can bounce off the paddle, but not the bottom of the screen.) Allow the user to drag the paddle left and right. If the ball hits the paddle, it bounces up, and the game continues. If the ball hits the bottom, the game ends. Decrease the paddle’s width every 20 seconds and increase the speed of the ball to make the game more challenging. Consider adding obstacles at random locations.

7.9. (Digital Clock App) Create an app that displays a digital clock on the screen. Include alarmclock functionality.

7.10. (Analog Clock App) Create an app that displays an analog clock with hour, minute and second hands that move appropriately as the time changes.

7.11. (Fireworks Designer App) Create an app that enables the user to create a customized fireworks display. Create a variety of fireworks demonstrations. Then orchestrate the firing of the fireworks for maximum effect. You might synchronize your fireworks with audios or videos. You could overlay the fireworks on a picture.

7.12. (Animated Towers of Hanoi App) Every budding computer scientist must grapple with certain classic problems, and the Towers of Hanoi (see Fig. 7.24) is one of the most famous. Legend has it that in a temple in the Far East, priests are attempting to move a stack of disks from one peg to another. The initial stack has 64 disks threaded onto one peg and arranged from bottom to top by decreasing size. The priests are attempting to move the stack from this peg to a second peg under the constraints that exactly one disk is moved at a time and at no time may a larger disk be placed above a smaller disk. A third peg is available for temporarily holding disks. Supposedly, the world will end when the priests complete their task, so there’s little incentive for us to facilitate their efforts.

Image

Fig. 7.24. 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

7.13. (DivvideAndConquer Game App: Open Source) Check out the open-source Android game, DivideAndConquer, on the Google Code site (apps-for-android.googlecode.com/svn/trunk/DivideAndConquer/). The goal of the game is to contain the bouncing balls by creating walls around them. Possible modifications and enhancements include:

a. Change the graphics.

b. Add sounds.

c. Add bonus rounds when the user hits a certain score.

d. Record the highest scores.

7.14. (Standup Timer App) Standup Timer is an open-source Android app that functions as a stop watch (github.com/jwood/standup-timer). Possible modifications and enhancements include:

a. Change the graphics.

b. Include digital and analog versions of a clock.

c. Allow the user to select from multiple sounds.

d. Give the user a warning signal before time is about to run out (either audible or visual).

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

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