Android:Game Programming
上QQ阅读APP看书,第一时间看更新

Chapter 5. Gaming and Java Essentials

In this chapter, we will cover a diverse and interesting range of topics. We will learn about Java arrays, which allow us to manipulate a potentially huge amount of data in an organized and efficient manner.

Then, we will look at the role threads can play in games, in order to do more than one thing apparently simultaneously.

If you thought that our math game was a bit on the quiet side, then we will look at adding sound effects to our games as well as introducing a cool open source app to generate authentic sound effects.

The last new thing we will learn will be persistence. This is what happens when the player quits our game or even turns off their Android device. What happens to the score then? How will we load the right level the next time they play?

Once we have done all this, we will use all the new techniques and knowledge along with what we already know to create a neat memory game.

In this chapter, we will cover the following topics:

  • Java arrays—an array of variables
  • Timing with threads
  • Creating and using beeps 'n' buzzes—Android sound
  • A look at life after destruction—persistence
  • Building the memory game

Java arrays – an array of variables

You might be wondering what happens when we have a game with lots of variables to keep track of. How about a table of high scores with the top 100 scores? We could declare and initialize 100 separate variables like this:

int topScore1;
int topScore2;
int topScore3;
//96 more lines like the above
int topScore100;

Straightaway, this can seem unwieldy, and what about the case when someone gets a new top score and we have to shift the scores in every variable down one place? A nightmare begins:

topScore100 = topScore99;
topScore99 = topScore98;
topScore98 = topScore97;
//96 more lines like the above
topScore1 = score;

There must be a better way to update the scores. When we have a large set of variables, what we need is a Java array. An array is a reference variable that holds up to a fixed maximum number of elements. Each element is a variable with a consistent type.

The following line of code declares an array that can hold int type variables, even a high score table perhaps:

int [] intArray;

We can also declare arrays of other types, like this:

String [] classNames;
boolean [] bankOfSwitches;
float [] closingBalancesInMarch;

Each of these arrays would need to have a fixed maximum amount of storage space allocated before it is used, like this:

intArray = new int [100];

The preceding line of code allocates up to a maximum of 100 integer-sized storage spaces. Think of a long aisle of 100 consecutive storage spaces in our variable warehouse. The spaces would probably be labeled intArray[0], intArray[1], intArray[2], and so on, with each space holding a single int value. Perhaps the slightly surprising thing here is that the storage spaces start off at 0, not 1. Therefore, in an array of size 100, the storage spaces would run from 0 to 99.

We can actually initialize some of these storage spaces like this:

intArray[0] = 5;
intArray[1] = 6;
intArray[2] = 7;

Note that we can only put the declared type into an array and the type that an array holds can never change:

intArray[3]= "John Carmack";//Won't compile

So when we have an array of int types, what are each of the int variables called? The array notation syntax replaces the name. We can do anything with a variable in an array that we could do with a regular variable with a name:

intArray[3] = 123;

Here is another example of array variables being used like normal variables:

intArray[10] = intArray[9] - intArray[4];

We can also assign a value from an array to a regular variable of the same type, like this:

int myNamedInt = intArray [3];

Note, however, that myNamedInt is a separate and distinct primitive variable, so any changes made to it do not affect the value stored in the intArray reference. It has its own space in the warehouse and is not connected to the array.

Arrays are objects

We said that arrays are reference variables. Think of an array variable as an address to a group of variables of a given type. Perhaps, using the warehouse analogy, someArray is an aisle number. So each of someArray[0], someArray[1], and so on is the aisle number followed by the position number in the aisle.

Arrays are also objects. This means that they have methods and properties that we can use:

int lengthOfSomeArray = someArray.length;

In the previous line of code, we assigned the length of someArray to the int variable called lengthOfSomeArray.

We can even declare an array of arrays. This is an array that, in each of its elements, stores another array, like this:

String[][] countriesAndCities;

In the preceding array, we could hold a list of cities within each country. Let's not go array-crazy just yet. Just remember that an array holds up to a predetermined number of variables of any predetermined type and their values are accessed using this syntax:

someArray[someLocation];

Let's actually use some arrays to try and get an understanding of how to use them in real code and what we might use them for.

A simple example of an array

Let's write a really simple working example of an array by performing the following steps. You can get the complete code for this example in the downloadable code bundle. It's at Chapter5/SimpleArrayExample/MainActivity.java:

  1. Create a project with a blank activity, just as we did in Chapter 2, Getting Started with Android. Also, clean up the code by deleting the unnecessary parts, but this isn't essential.
  2. First, we declare our array, allocate five spaces, and initialize some values to each of the elements:
    //Declaring an array
    int[] ourArray;
    
    //Allocate memory for a maximum size of 5 elements
    ourArray = new int[5];
    
    //Initialize ourArray with values
    //The values are arbitrary as long as they are int
    //The indexes are not arbitrary 0 through 4 or crash!
    
    ourArray[0] = 25;
    ourArray[1] = 50;
    ourArray[2] = 125;
    ourArray[3] = 68;
    ourArray[4] = 47;
  3. We output each of the values to the logcat console. Notice that when we add the array elements together, we are doing so over multiple lines. This is fine because we have omitted a semicolon until the last operation, so the Java compiler treats the lines as one statement:
    //Output all the stored values
    Log.i("info", "Here is ourArray:");
    Log.i("info", "[0] = "+ourArray[0]);
    Log.i("info", "[1] = "+ourArray[1]);
    Log.i("info", "[2] = "+ourArray[2]);
    Log.i("info", "[3] = "+ourArray[3]);
    Log.i("info", "[4] = "+ourArray[4]);
    
    //We can do any calculation with an array element
    //As long as it is appropriate to the contained type
    //Like this:
    int answer = ourArray[0] +
        ourArray[1] +
        ourArray[2] +
        ourArray[3] +
        ourArray[4];
    
    Log.i("info", "Answer = "+ answer);
  4. Run the example on an emulator.

Remember that nothing will happen on the emulator display because the entire output will be sent to our logcat console window in Android Studio. Here is the output of the preceding code:

info﹕ Here is ourArray:
info﹕ [0] = 25
info﹕
 [1] = 50
info﹕ [2] = 125
info﹕ [3] = 68
info﹕ [4] = 47
info﹕ Answer = 315 

In step 2, we declared an array called ourArray to hold int variables, and allocated space for up to five variables of that type.

Next, we assigned a value to each of the five spaces in our array. Remember that the first space is ourArray[0] and the last space is ourArray[4].

In step 3, we simply printed the value in each array location to the console. From the output, we can see that they hold the value we initialized in the previous step. Then we added each of the elements in ourArray and initialized their value to the answer variable. We then printed answer to the console and saw that all the values where added together, just as if they were plain old int types stored in a slightly different manner, which is exactly what they are.

Getting dynamic with arrays

As we discussed at the beginning of all this array stuff, if we need to declare and initialize each element of an array individually, there isn't a huge amount of benefit in an array over regular variables. Let's look at an example of declaring and initializing arrays dynamically.

Dynamic array example

Let's make a really simple dynamic array by performing the following steps. You can find the working project for this example in the download bundle. It is at Chapter5/DynamicArrayExample/MainActivity.java:

  1. Create a project with a blank activity, just as we did in Chapter 2,Getting Started with Android. Also, clean up the code by deleting the unnecessary parts, but this isn't essential.
  2. Type the following between the opening and closing curly braces of onCreate. See if you can work out what the output will be before we discuss it and analyze the code:
    //Declaring and allocating in one step
    int[] ourArray = new int[1000];
    
    //Let's initialize ourArray using a for loop
    //Because more than a few variables is allot of typing!
    for(int i = 0; i < 1000; i++){
       //Put the value of ourValue into our array
       //At the position determined by i.
       ourArray[i] = i*5;
    
                //Output what is going on
                Log.i("info", "i = " + i);
                Log.i("info", "ourArray[i] = " + ourArray[i]);
    }
  3. Run the example on an emulator. Remember that nothing will happen on the emulator display because the entire output will be sent to our logcat console window in Android Studio. Here is the output of the preceding code:
    info﹕ i = 0
    info﹕ ourArray[i] = 0
    info﹕ i = 1
    info﹕ ourArray[i] = 5
    info﹕ i = 2
    info﹕
     ourArray[i] = 10
    

    I have removed 994 iterations of the loop for brevity:

    info﹕ ourArray[i] = 4985
    info﹕ i = 998
    info﹕ ourArray[i] = 4990
    info﹕ i = 999
    info﹕ ourArray[i] = 4995
    

All the action happened in step 2. We declared and allocated an array called ourArray to hold up to 1,000 int values. This time, however, we did the two steps in one line of code:

int[] ourArray = new int[1000];

Then we used a for loop that was set to loop 1,000 times:

(int i = 0; i < 1000; i++){

We initialized the spaces in the array from 0 to 999 with the value of i multiplied by 5, as follows:

ourArray[i] = i*5;

To demonstrate the value of i and the value held in each position of the array, we output the value of i followed by the value held in the corresponding position in the array as follows:

Log.i("info", "i = " + i);
Log.i("info", "ourArray[i] = " + ourArray[i]);

All of this happened 1,000 times, producing the output we saw.

Entering the nth dimension with arrays

We very briefly mentioned that an array can even hold other arrays at each of its positions. Now, if an array holds lots of arrays that hold lots of some other type, how do we access the values in the contained arrays? And why would we ever need this anyway? Take a look at the next example of where multidimensional arrays can be useful.

An example of a multidimensional array

Let's create a really simple multidimensional array by performing the following steps. You can find the working project for this example in the download bundle. It is at Chapter5/MultidimensionalArrayExample/MainActivity.java:

  1. Create a project with a blank activity, just as we did in Chapter 2, Getting Started with Android. Also, clean up the code by deleting the unnecessary methods, but this isn't essential.
  2. After the call to setContentView, declare and initialize a two-dimensional array, like this:
    //A Random object for generating question numbers later
    Random randInt = new Random();
    //And a variable to hold the random value generated
    int questionNumber;
    
    //We declare and allocate in separate stages for clarity
    //but we don't have to
    String[][] countriesAndCities;
    //Here we have a 2 dimensional array
    
    //Specifically 5 arrays with 2 elements each
    //Perfect for 5 "What's the capital city" questions
    countriesAndCities = new String[5][2];
    
    //Now we load the questions and answers into our arrays
    //You could do this with less questions to save typing
    //But don't do more or you will get an exception
    countriesAndCities [0][0] = "United Kingdom";
    countriesAndCities [0][1] = "London";
    
    countriesAndCities [1][0] = "USA";
    countriesAndCities [1][1] = "Washington";
    
    countriesAndCities [2][0] = "India";
    countriesAndCities [2][1] = "New Delhi";
    
    countriesAndCities [3][0] = "Brazil";
    countriesAndCities [3][1] = "Brasilia";
    
    countriesAndCities [4][0] = "Kenya";
    countriesAndCities [4][1] = "Nairobi";
  3. Now we output the contents of the array using a for loop and a Random class object. Note how we ensure that although the question is random, we can always pick the correct answer:
    //Now we know that the country is stored at element 0
    //The matching capital at element 1
    //Here are two variables that reflect this
    int country = 0;
    int capital = 1;
    
    //A quick for loop to ask 3 questions
    for(int i = 0; i < 3; i++){
       //get a random question number between 0 and 4
       questionNumber = randInt.nextInt(5);
    
       //and ask the question and in this case just
       //give the answer for the sake of brevity
      Log.i("info", "The capital of " +countriesAndCities[questionNumber][country]);
    
      Log.i("info", "is " +countriesAndCities[questionNumber][capital]);
    
    }//end of for loop

Run the example on an emulator. Once again, nothing will happen on the emulator display because the output will be sent to our logcat console window in Android Studio. Here is the output of the previous code:

info﹕ The capital of USA
info﹕ is Washington
info﹕ The capital of India
info﹕ is New Delhi
info﹕ The capital of United Kingdom
info﹕ is London

What just happened? Let's go through this chunk by chunk so that we know exactly what is going on.

We make a new object of the Random type, called randInt, ready to generate random numbers later in the program:

Random randInt = new Random();

We declare a simple int variable to hold a question number:

int questionNumber;

Then we declare countriesAndCities, our array of arrays. The outer array holds arrays:

String[][] countriesAndCities;

Now we allocate space within our arrays. The first outer array will be able to hold five arrays and each of the inner arrays will be able to hold two strings:

countriesAndCities = new String[5][2];

Next, we initialize our arrays to hold countries and their corresponding capital cities. Notice that with each pair of initializations, the outer array number stays the same, indicating that each country/capital pair is within one inner array (a string array). Of course, each of these inner arrays is held in one element of the outer array (which holds arrays):

countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";

countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";

countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";

countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";

countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";

To make the upcoming for loop clearer, we declare and initialize int variables to represent the country and the capital from our arrays. If you glance back at the array initialization, all the countries are held in position 0 of the inner array and all the corresponding capital cities are held at position 1:

int country = 0;
int capital = 1;

Now we create a for loop that will run three times. Note that this number does not mean we access the first three elements of our array. It is rather the number of times we go through the loop. We could make it loop one time or a thousand times, but the example would still work:

for(int i = 0; i < 3; i++){

Next, we actually determine which question to ask, or more specifically, which element of our outer array. Remember that randInt.nextInt(5) returns a number between 0 and 4. This is just what we need as we have an outer array with five elements, from 0 to 4:

questionNumber = randInt.nextInt(5);

Now we can ask a question by outputting the strings held in the inner array, which in turn is held by the outer array that was chosen in the previous line by the randomly generated number:

  Log.i("info", "The capital of " +countriesAndCities[questionNumber][country]);

  Log.i("info", "is " +countriesAndCities[questionNumber][capital]);

}//end of for loop

For the record, we will not be using any multidimensional arrays in the rest of this book. So if there is still a little bit of murkiness around these arrays inside arrays, then that doesn't matter. You know they exist and what they can do, so you can revisit them if necessary.

Array-out-of-bounds exceptions

An array-out-of-bounds exception occurs when we attempt to access an element of an array that does not exist. Whenever we try this, we get an error. Sometimes, the compiler will catch it to prevent the error from going into a working game, like this:

int[] ourArray = new int[1000];
int someValue = 1;//Arbitrary value
ourArray[1000] = someValue;//Won't compile as compiler knows this won't work.
//Only locations 0 through 999 are valid

Guess what happens if we write something like this:

int[] ourArray = new int[1000];
int someValue = 1;//Arbitrary value
int x = 999;
if(userDoesSomething){x++;//x now equals 1000
}
ourArray[x] = someValue;
//Array out of bounds exception if userDoesSomething evaluates to true! This is because we end up referencing position 1000 when the array only has positions 0 through 999
//Compiler can't spot it and game will crash on player - yuck!

The only way we can avoid this problem is to know the rule. The rule is that arrays start at zero and go up to the number obtained by subtracting one from the allocated number. We can also use clear, readable code where it is easy to evaluate what we have done and spot the problems.

Timing with threads

So what is a thread? You can think of threads in Java programming just like threads in a story. In one thread of a story, we have the primary character battling the enemy on the front line, and in another thread, the soldier's family are getting by, day to day. Of course, a story doesn't have to have just two threads. We could introduce a third thread. Perhaps the story also tells of the politicians and military commanders making decisions. These decisions subtly, or not so subtly, affect what happens in the other threads.

Threads in programming are just like this. We create parts/threads in our program and they control different aspects for us. We introduce threads to represent these different aspects because of the following reasons:

  • They make sense from an organizational point of view
  • They are a proven way of structuring a program that works
  • The nature of the system we are working on forces us to use them

In Android, we use threads for all of these reasons simultaneously. It makes sense, it works, and we have to use it because of the design of the system.

In gaming, think about a thread that receives the player's button taps for "left", "right", and "shoot", a thread that represents the alien thinking where to move next, and yet another thread that draws all the graphics on the screen.

Programs with multiple threads can have problems. Like the threads of a story, if proper synchronization does not occur, then things go wrong. What if our soldier went into battle before the battle or even the war existed? Weird!

What if we have a variable, int x, that represents a key piece of data that say three threads of our program use? What happens if one thread gets slightly ahead of itself and makes the data "wrong" for the other two? This problem is the problem of correctness, caused by multiple threads racing to completion, oblivious of each other—because they are just dumb code after all.

The problem of correctness can be solved by close oversight of the threads and locking. Locking means temporarily preventing execution in one thread to ensure that things are working in a synchronized manner. It's like freezing the soldier from boarding a ship to war until the ship has actually docked and the plank has been lowered, avoiding an embarrassing splash.

The other problem with programs with multiple threads is the problem of deadlock, where one or more threads become locked, waiting for the right moment to access x, but that moment never comes and the entire program eventually grinds to a halt.

You might have noticed that it was the solution to the first problem (correctness) that is the cause of the second problem (deadlock). Now consider all that we have just been discussing and mix it in with the Android Activity lifecycle. It's possible that you start to feel a little nauseous with the complexity.

Fortunately, the problem has been solved for us. Just as we use the Activity class and override its methods to interact with the Android lifecycle, we can also use other classes to create and manage our threads. Just as with Activity, we only need to know how to use them, not how they work.

So why tell me all this stuff about threads when I didn't need to know, you would rightly ask. It's simply because we will be writing code that looks different and is structured in an unfamiliar manner. We will have no sweat writing our Java code to create and work within our threads if we can do the following:

  • Accept that the new concepts we will introduce are what we need to work with in order to create an Android-specific solution to the problems related to working with threads
  • Understand the general concept of a thread, which is mostly the same as a story thread that happens almost simultaneously
  • Learn the few rules of using some of the Android thread classes

Notice that I said classes, plural, in the third bullet. Different thread classes work best in different situations. You could write a whole book on just threads in Android. We will use two thread classes in this book. In this chapter, we will use Handler. In Chapter 7, Retro Squash Game, and Chapter 8, The Snake Game, we will use the Runnable class. All we need to remember is that we will be writing parts of our program that run at almost the same time as each other.

Tip

What do I mean by "almost"? What is actually happening is that the CPU switches between threads in turn. However, this happens so fast that we will not be able to perceive anything but simultaneity.

A simple thread timer example with the Handler class

After this example, we can heave a sigh of relief when we realize that threads are not as complicated as first feared. When using threads in a real game, we will have to add a bit of extra code alongside the code in this simple example, but it's not much, and we will talk about it when we get to it.

As usual, you can simply use the complete code from the download bundle. This project is located in Chapter5/SimpleThreadTimer/MainActivity.java.

As the name suggests, we will be creating a timer—quite a useful feature in a lot of games:

  1. Create a project with a blank activity, just as we did in Chapter 2, Getting Started with Android. Also, clean up the code by deleting the unnecessary parts, but this isn't essential.
  2. Immediately after the class declaration, enter the three highlighted lines:
    public class MainActivity extends Activity {
    
        private Handler myHandler;
     boolean gameOn;
     long startTime;
    
  3. Enter this code inside the onCreate method. It will create a thread with something else going on in the if(gameOn) block:
    //How many milliseconds is it since the UNIX epoch
            startTime = System.currentTimeMillis();
    
            myHandler = new Handler() {
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
    
                    if (gameOn) {
                        long seconds = ((System.currentTimeMillis() - startTime)) / 1000;
                        Log.i("info", "seconds = " + seconds);
                    }
    
                    myHandler.sendEmptyMessageDelayed(0, 1000);
                }
    
            };
    
            gameOn = true;
            myHandler.sendEmptyMessage(0);
        }
  4. Run the app. Quit with the home or back button on the emulator. Notice that it is still printing to the console. We will deal with this anomaly when we implement our memory game.

When you run the example on an emulator, remember that nothing will happen on the emulator display because all of the output will be sent to our logcat console window in Android Studio. Here is the output of the previous code:

info﹕ seconds = 1
info﹕ seconds = 2
info﹕ seconds = 3
info﹕ seconds = 4
info﹕ seconds = 5
info﹕ seconds = 6

So what just happened? After 1-second intervals, the number of seconds elapsed was printed to the console. Let's learn how this happened.

First, we declare a new object, called myHandler, of the Handler type. We then declare a Boolean variable called gameOn. We will use this to keep track of when our game is running. Finally, the last line of this block of code declares a variable of the long type. You might remember the long type from Chapter 3, Speaking Java – Your First Game. We can use long variables to store very large whole numbers, and this is what we do here with startTime:

private Handler myHandler;
boolean gameOn;
long startTime;

Next, we initialized startTime using currentTimeMillis, a method of the System class. This method holds the number of milliseconds since January 1, 1970. We will see how we use this value in the next line of code.

startTime = System.currentTimeMillis();

Next is the important code. Everything up to if(gameOn) marks the code to define our thread. Certainly, the code is a bit of a mouthful, but it is not as bad as it looks at first glance. Also, remember that we only need to use the threads; we don't need to understand every aspect of how they do their work.

Let's dissect the preceding code to demystify it a bit. The myHandler = new Handler() line simply initializes our myHandler object. What is different from what we have seen before is that we go on to customize the object immediately afterwards. We override the handleMessage method (which is where we put our code that runs in the thread) and then we call super.handleMessage, which calls the default version of handleMessage before it runs our custom code. This is much like we do for the onCreate method every time we call super.onCreate.

Then we have the if(gameOn) block. Everything in that if block is the code that we want to run in the thread. The if(gameOn) block simply gives us a way to control whether we want to run our code at all. For example, we might want the thread up and running but only sometimes run our code. The if statement gives us the power to easily choose. Take a look at the code now. We will analyze what is happening in the if block later:

myHandler = new Handler() {
     public void handleMessage(Message msg) {
       super.handleMessage(msg);
         
       if (gameOn) {
         long seconds = ((System.currentTimeMillis() - startTime)) / 1000;
             Log.i("info", "seconds = " + seconds);
         }

       myHandler.sendEmptyMessageDelayed(0, 1000);
      }

 };

Inside the if block, we declare and initialize another long variable called seconds, and do a little bit of math with it:

long seconds = ((System.currentTimeMillis() - startTime)) / 1000;

First, we get the current number of milliseconds since January 1, 1970, and then subtract startTime from it. This gives us the number of milliseconds since we first initialized startTime. Then we divide the answer by 1000 and get a value in seconds. We print this value to the console with the following line:

Log.i("info", "seconds = " + seconds);

Next, just after our if block, we have this line:

myHandler.sendEmptyMessageDelayed(0, 1000);

The previous line tells the Android system that we want to run the code in the handleMessage method once every 1000 milliseconds (once a second).

Back in onCreate, after the closing curly braces of the handleMessage method and the Handler class, we finally set gameOn to true so that it is possible to run the code in the if block:

gameOn = true;

Then, this last line of the code starts the flow of messages between our thread and the Android system:

myHandler.sendEmptyMessage(0);

It is worth pointing out that the code inside the if block can be as minimal or as extensive as we need. When we implement our memory game, we will see much more code in our if block.

All we really need to know is that the somewhat elaborate setup we have just seen allows us to run the contents of the if block in a new thread. That's it! Perhaps apart from brushing over that System class a bit quickly.

Note

The System class has many uses. In this case, we use it to get the number of milliseconds since January 1, 1970. This is a common system used to measure time in a computer. It is known as Unix time, and the first millisecond of January 1, 1970, is known as the Unix Epoch. We will bump into this concept a few more times throughout the book.

Enough on threads, let's make some noise!

Beeps n buzzes – Android sound

This section will be divided into two parts—creating and using sound FX. So let's get on with it.

Creating sound FX

Years ago, whenever I made a game, I would spend many hours trawling websites offering royalty-free sound FX. Although there are many good ones out there, the really great ones are always costly, and no matter how much you pay, they are never exactly what you want. Then a friend pointed out a simple open source app called Bfxr, and I have never wasted another moment looking for sound effects since. We can make our own.

Here is a very fast guide to making your own sound effects using Bfxr. Grab a free copy of Bfxr from www.bfxr.net.

Follow the simple instructions on the website to set it up. Try out a few of these examples to make cool sound effects:

Tip

This is a seriously condensed tutorial. You can do much more with Bfxr. To learn more, read the tips on the website at the previous URL.

  1. Run bfxr.exe:
  2. Try out all the preset types, which generate a random sound of that type. When you have a sound that is close to what you want, move to the next step:
  3. Use the sliders to fine-tune the pitch, duration, and other aspects of your new sound:
  4. Save your sound by clicking on the Export Wav button. Despite the name of this button, as we will see, we can save in formats other than .wav.
  5. Android likes to work with sounds in the OGG format, so when asked to name your file, use the .ogg extension on the end of whatever you decide to call it.
  6. Repeat steps 2 to 5 as often as required.

Tip

Every project in this book that requires sound samples comes with the sound samples provided, but as we have seen, it is much more fun to make our own samples. All you need to do is to save them with the same filename as the provided samples.

Playing sounds in Android

To complete this brief example, you will need three sound effects saved in the .ogg format. So if you don't have them to hand, go back to the Creating sound FX section to make some. Alternatively, you can use the sounds provided in the Chapter5/ PlayingSounds/assets folder of the code bundle. As usual, you can view or use the already completed code at Chapter5/PlayingSounds/java/MainActivity.java and Chapter5/PlayingSounds/layout/activity_main.xml. Now perform the following steps:

  1. Create a project with a blank activity, just as we did in Chapter 2, Getting Started with Android. Also, clean up the code by deleting the unnecessary parts, although this isn't essential.
  2. Create three sound files and save them as sample1.ogg, sample2.ogg, and sample3.ogg.
  3. In the main folder in the Project Explorer window, we need to add a folder called assets. So in the Project Explorer window, right-click on the main folder and navigate to New | Directory. Type assets in the New Directory dialog box.
  4. Now copy and paste the three sound files to the newly created assets folder. Alternatively, select the three files, right-click on them, and click on Copy. Then click on the assets folder in the Android Studio Project Explorer. Now right-click on the assets folder and click on Paste.
  5. Open activity_main.xml in the editor window and drag three button widgets onto your UI. It doesn't matter where they are or how they are aligned. When you look at the id property in the Properties window for any of our three new buttons, you will notice that they have automatically been assigned id properties. They are button, button2, and button3. As we will see, this is just what we need.
  6. Let's enable our activity to listen to the buttons being clicked by implementing onClickListener as we have done in all our other examples with buttons. Open MainActivity.java in the editor window. Replace the public class MainActivity extends Activity { line with the following line of code:
    public class MainActivity extends Activity implements View.
        OnClickListener {
  7. As before, we get an unsightly red underline on our new line of code. The last time this happened, we typed in the empty body of the onClick method that we must implement and all was well. This time, because we already know what is going on here, we will learn a shortcut. Hover your mouse cursor over the error and right-click on it. Now click on Generate... and then select Implement methods.... In the Select Methods To Implement dialog box, onClick(View):void will already be selected:
  8. Select this option by clicking on OK. Now scroll to the bottom of your code and see that Android Studio has very kindly implemented the onClick method for you and the error is also gone.
  9. Type this code after the MainActivity declaration to declare some variables for our sound effects:
    private SoundPool soundPool;
    int sample1 = -1;
    int sample2 = -1;
    int sample3 = -1;
  10. Type this code in the onCreate method to load our sounds into memory:
    soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);
      try{
        //Create objects of the 2 required classes
              AssetManager assetManager = getAssets();
              AssetFileDescriptor descriptor;
    
              //create our three fx in memory ready for use
              descriptor = assetManager.openFd("sample1.ogg");
              sample1 = soundPool.load(descriptor, 0);
    
              descriptor = assetManager.openFd("sample2.ogg");
              sample2 = soundPool.load(descriptor, 0);
    
    
              descriptor = assetManager.openFd("sample3.ogg");
              sample3 = soundPool.load(descriptor, 0);
    
    
            }catch(IOException e){
                //catch exceptions here
            }
  11. Now add the code to grab a reference to the buttons in our UI and listen to clicks on them:
      //Make a button from each of the buttons in our layout
         Button button1 =(Button) findViewById(R.id.button);
         Button button2 =(Button) findViewById(R.id.button2);
         Button button3 =(Button) findViewById(R.id.button3);
    
         //Make each of them listen for clicks
         button1.setOnClickListener(this);
         button2.setOnClickListener(this);
         button3.setOnClickListener(this);
  12. Finally, type this code in the onClick method that we autogenerated:
    switch (view.getId()) {
    
      case R.id.button://when the first button is pressed
        //Play sample 1
              soundPool.play(sample1, 1, 1, 0, 0, 1);
              break;
    
              //Now the other buttons
              case R.id.button2:
              soundPool.play(sample2, 1, 1, 0, 0, 1);
              break;
    
              case R.id.button3:
              soundPool.play(sample3, 1, 1, 0, 0, 1);
              break;
            }

Run the example on an emulator or on a real Android device. Notice that by clicking on a button, you can play any of your three sound samples at will. Of course, sounds can be played at almost any time, not just on button presses. Perhaps they can be played from a thread as well. We will see more sound samples when we implement the memory game later in the chapter.

This is how the code works. We started off by setting up a new project in the usual way. In steps 2 to 5, however, we created some sounds with Bfxr, created an assets folder, and placed the files within it. This is the folder where Android expects to find sound files. So when we write the code in the next steps that refers to the sound files, the Android system will be able to find them.

In steps 6 to 8, we enabled our activity to listen to button clicks as we have done several times before. Only this time, we got Android Studio to autogenerate the onClick method.

Then we saw this code:

private SoundPool soundPool;

First, we create an object of the SoundPool type, called soundPool. This object will be the key to making noises with our Android device. Next, we have this code:

int sample1 = -1;
int sample2 = -1;
int sample3 = -1;

The preceding code is very simple; we declared three int variables. However, they serve a slightly deeper purpose than a regular int variable. As we will see in the next block of code we analyze, they will be used to hold a reference to a sound file that is loaded into memory. In other words, the Android system will assign a number to each variable that will refer to a place in memory where our sound file will reside.

We can think of this as a location in our variable warehouse. So we know the name of the int variable, and contained within it is what Android needs to find our sound. Here is how we load our sounds into memory and use the references we've just been discussing.

Let's break the code in step 10 into a few parts. Take a close look and then we will examine what is going on:

soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);

Here, we initialize our soundPool object and request up to 10 simultaneous streams of sound. We should be able to really mash the app buttons and get a sound every time. AudioManager.STREAM_MUSIC describes the type of stream. This is typical for applications of this type. Finally, the 0 argument indicates we would like default quality sound.

Now we see something new. Notice that the next chunk of code is wrapped into two blocks, try and catch. This means that if the code in the try block fails, we want the code in the catch block to run. As you can see, there is nothing but a comment in the catch block.

We must do this because of the way the SoundPool class is designed. If you try to write the code without the try and catch blocks, it won't work. This is typical of Java classes involved in reading from files. It is a fail-safe process to check whether the file is readable or even whether it exists. You could put a line of code to output to the console that an error has occurred.

Tip

If you want to experiment with try/catch, then put a line of code to output a message in the catch block and remove one of the sound files from the assets folder. When you run the app, the loading will fail and the code in the catch block will be triggered.

We will throw caution to the wind because we are quite sure that the files will be there and will work . Let's examine what is inside the try block. Take a close look at the following code and then we will dissect it:

  try{
    //Create objects of the 2 required classes
          AssetManager assetManager = getAssets();
          AssetFileDescriptor descriptor;

          //create our three fx in memory ready for use
          descriptor = assetManager.openFd("sample1.ogg");
          sample1 = soundPool.load(descriptor, 0);

          descriptor = assetManager.openFd("sample2.ogg");
          sample2 = soundPool.load(descriptor, 0);


          descriptor = assetManager.openFd("sample3.ogg");
          sample3 = soundPool.load(descriptor, 0);


        }catch(IOException e){
            //catch exceptions here
        }

First, we create an object called assetManager of the AssetManager type and an AssetFileDescriptor object called descriptor. We then use these two objects combined to load our first sound sample like this:

          descriptor = assetManager.openFd("sample1.ogg");
          sample1 = soundPool.load(descriptor, 0);

We now have a sound sample loaded in memory and its location saved in our int variable called sample1. The first sound file, sample1.ogg, is now ready to use. We perform the same procedure for sample2 and sample3 and we are ready to make some noise!

In step 11, we set up our buttons, which we have seen several times before. In step 12, we have our switch block ready to perform a different action depending upon which button is pressed. You can probably see that the single action each button takes is the playing of a sound. For example, Button1 does this:

soundPool.play(sample1, 1, 1, 0, 0, 1);

This line of code plays the sound that is loaded in memory at the location referred to by int sample1.

Note

The arguments of the method from left to right define the following: the sample to play, left volume, right volume, priority over other playing sounds, loop or not, rate of playback. You can have some fun with these if you like. Try setting the loop argument to 3 and the rate argument to perhaps 1.5.

We handle each button in the same way. Now let's learn something serious.

Life after destruction – persistence

Okay, this is not as heavy as it sounds, but it is an important topic when making games. You have probably noticed that the slightest thing can reset our math game, such as an incoming phone call, a battery that ran flat, or even tilting the device to a different orientation.

When these events occur, we might like our game to remember the exact state it was in so that when the player comes back, it is in exactly the same place as they left off. If you were using a word-processing app, you would definitely expect this type of behavior.

We are not going to go to that extent with our game, but as a bare minimum, shouldn't we at least remember the high score? This gives the player something to aim for, and most importantly, a reason to come back to our game.

An example of persistence

Android and Java have many different ways to achieve persistence of data, from reading and writing to files to setting up and using whole databases through our code. However, the neatest, simplest, and most suitable way for the examples in this book is by using the SharedPreferences class.

In this example, we will use the SharedPreferences class to save data. Actually, we will be reading and writing to files, but the class hides all of the complexity from us and allows us to focus on the game.

We will see a somewhat abstract example of persistence so that we are familiar with the code before we use something similar to save the high score in our memory game. The complete code for this example can be found in the code bundle at Chapter5/Persistence/java/MainActivity.java and Chapter5/Persistence/layout/activity_main.xml:

  1. Create a project with a blank activity, just as we did in Chapter 2, Getting Started with Android. Also, clean up the code by deleting the unnecessary parts, but this isn't essential.
  2. Open activity_main.xml in the editor window and click and drag one button from the palette to the design. The default ID of the button that is assigned is perfect for our uses, so no further work is required on the UI.
  3. Open MainActivity.java in the editor window. Implement View.OnClickListener and autogenerate the required onClick method, just as we did in steps 6 and 7 of the Playing sound in Android example previously.
  4. Type the following code just after the MainActivity declaration. This declares our two objects that will do all the complex stuff behind the scenes: a bunch of strings that will be useful and a button:
    SharedPreferences prefs;
    SharedPreferences.Editor editor;
    String dataName = "MyData";
    String stringName = "MyString";
    String defaultString = ":-(";
    String currentString = "";//empty
    Button button1;
  5. Add the next block of code to the onCreate method after the call to setContentView. We initialize our objects and set up our button. We will look closely at this code once the example is done:
    //initialize our two SharedPreferences objects
    prefs = getSharedPreferences(dataName,MODE_PRIVATE);
    editor = prefs.edit();
    
    //Either load our string or
    //if not available our default string
    currentString = prefs.getString(stringName, defaultString);
    
     //Make a button from the button in our layout
     button1 =(Button) findViewById(R.id.button);
    
     //Make each it listen for clicks
     button1.setOnClickListener(this);
    
     //load currentString to the button
     button1.setText(currentString);
  6. Now the action takes place in our onClick method. Add this code, which generates a random number and adds it to the end of currentString. Then it saves the string and sets the value of the string to the button as well:
    //we don't need to switch here!
    //There is only one button
    //so only the code that actually does stuff
    
    //Get a random number between 0 and 9
    Random randInt = new Random();
    int ourRandom = randInt.nextInt(10);
    
    //Add the random number to the end of currentString
    currentString = currentString + ourRandom;
    
    //Save currentString to a file in case the user 
    //suddenly quits or gets a phone call
    editor.putString(stringName, currentString);
    editor.commit();
    
     //update the button text
     button1.setText(currentString);

Run the example on an emulator or a device. Notice that each time you press the button, a random number is appended to the text of the button. Now quit the app, or even shut down the device if you like. When you restart the app, our cool SharedPreferences class simply loads the last saved string.

Here is how the code works. There is nothing we haven't seen several times before until step 4:

SharedPreferences prefs;
SharedPreferences.Editor editor;

Here, we declare two types of SharedPreferences objects called prefs and editor. We will see exactly how we use them in a minute.

Next, we declare the dataName and stringName strings. We do this because to use the facilities of SharedPreferences, we need to refer to our collection of data, as well as any individual pieces of data within it, using a consistent name. By initializing dataName and stringName, we can use them as a name for our data store as well as a specific item within that data store, respectively. The sad face in defaultString gets used any time the SharedPreferences object needs a default because either nothing has been previously saved or the loading process fails for some reason. The currentString variable will hold the value of the string we will be saving and loading as well as displaying to the user of our app. Our button is button1:

String dataName = "MyData";
String stringName = "MyString";
String defaultString = ":-(";
String currentString = "";//empty
Button button1;

In step 5, the real action starts with this code:

prefs = getSharedPreferences(dataName,MODE_PRIVATE);
editor = prefs.edit();

currentString = prefs.getString(stringName, defaultString);

The previous code does stuff that would take a lot more code if we didn't have the useful SharedPreferences class. The first two lines initialize the objects and the third loads the value from our data store item, whose name is contained in stringName, to our currentString variable. The first time this happens, it uses the defaultString value because nothing is stored there yet, but once there is a value stored, this single line of code that will load up our saved string.

At the end of step 5, we set up our button as we have done many times before. Moving on to step 6 in the onClick method, there is no switch block because there is only one button. So if a click is detected, it must be our button. Here are the first three lines from onClick:

Random randInt = new Random();
int ourRandom = randInt.nextInt(10);
currentString = currentString + ourRandom;

We generate a random number and append it to the currentString variable. Next, still in onClick, we do this:

editor.putString(stringName, currentString);
editor.commit();

This is like the opposite of the code that loaded our string in onCreate. The first of the previous two lines identifies the place in the data store to write the value to (stringName) and the value to be written there (currentString). The next line, editor.commit();, simply says, "go ahead and do it."

The following line displays currentString as text on our button so that we can see what is going on:

button1.setText(currentString);

Tip

For more on persistence, take a look at the second question of the Self-test questions section at the end of this chapter.

The memory game

The code in the memory game shouldn't challenge us too much because we have done the background research on threads, arrays, sound, and persistence. There will be some new-looking code and we will examine it in detail when it crops up.

Here is a screenshot of our finished game:

This is the home screen. It shows the high score, which persists between play sessions and when the device is shut down. It also shows a Play button, which will take the player to the main game screen. Take a look at the following screenshot:

The game screen itself will play a sequence of sounds and numbers. The corresponding button will wobble in time with the corresponding sound. Then the player will be able to interact with the buttons and attempt to copy the sequence. For every part of the sequence that the player gets right, they will be awarded points.

If the sequence is copied in its entirety, then a new and longer sequence will be played and again the player will attempt to repeat the sequence. This continues until the player gets a part of a sequence wrong.

As the score increases, it is displayed in the relevant TextView, and when a sequence is copied correctly, the level is increased and displayed below the score.

The player can start a new game by pressing the Replay button. If a high score is achieved, it will be saved to a file and displayed on the home screen.

The implementation of the game is divided into five phases. The end of a phase would be a good place to take a break. Here are the different phases of the game:

  • Phase 1: This implements the UI and some basics.
  • Phase 2: This prepares our variables and presents the pattern (to be copied) to the player.
  • Phase 3: In this phase, we will handle the player's response when they try to copy the pattern.
  • Phase 4: Here, we will use what we just learned about persistence to maintain the player's high score when they quit the game or turn off their device.
  • Phase 5: At the end of phase 4, we will have a fully working memory game. However, to add to our repertoire of Android skills, after we have discussed Android UI animations near the end of this chapter, we will complete this phase, which will enhance our memory game.

All the files containing the complete code and the sound files after all five stages can be found in the download bundle in the Chapter5/MemoryGame folder. In this project, however, there is a lot to be learned from going through each of the stages.

Phase 1 – the UI and the basics

Here, we will lay out a home menu screen UI and a UI for the game itself. We will also configure some IDs for some of the UI elements so that we can control them in our Java code later:

  1. Create a new application called Memory Game and clean up the code if you wish.
  2. Now we create a new activity and call it GameActivity. So right-click on the java folder in Project Explorer, navigate to New | Activity, then click on Next, name the activity as GameActivity, and click on Finish. For clarity, clean up this activity in the same way as we cleaned up all our others.
  3. Make the game fullscreen and lock the orientation as we did in the Going fullscreen and locking orientation tutorial at the end of Chapter 4, Discovering Loops and Methods.
  4. Open the activity_main.xml file from the res/layout folder.

Let's quickly create our home screen UI by performing the following steps:

  1. Open activity_main.xml in the editor and delete the Hello World TextView.
  2. Click and drag the following: Large Text to the top center (to create our title text), Image just below that, another LargeText below that (for our high score), and a Button (for our player to click to play). Your UI should look a bit like what is shown in the following screenshot:
  3. Adjust the text properties of the two TextViews and the Button element to make it plain what each will be used for. As usual, you can replace the Android icon in the ImageView with any image you choose (as we did in Chapter 4, Discovering Loops and Methods, in the Adding a custom image tutorial).
  4. Tweak the sizes of the elements in the usual way to suit the emulator or device you will be running the game on.
  5. Let's make the ID for our Hi Score TextView more relevant to its purpose. Left-click to select the Hi Score TextView, find its id property in the Properties window, and change it to textHiScore. The IDs of the image and the title are not required, and the existing ID of the play button is button, which seems appropriate already. So there is nothing else to change here.

Let's wire up the Play button to create a link between the home and the game screens, as follows:

  1. Open MainActivity.java in the editor.
  2. Add implements View.onClickListener to the end of the MainActivity declaration so that it now looks like this:
      public class MainActivity extends Activity implements View.OnClickListener {
  3. Now hover your mouse over the line you just typed and right-click on it. Now click on Generate, then on Implement methods..., and then on OK to have Android Studio autogenerate the onClick method we must implement.
  4. At the end of our onCreate method, before the closing curly brace, enter the following code to get a reference to our Play button and listen to clicks:
      //Make a button from the button in our layout
       Button button =(Button) findViewById(R.id.button);
    
       //Make each it listen for clicks
       button.setOnClickListener(this);
  5. Scroll down to our onClick method and enter the following code in its body to have the Play button take the player to our GameActivity, which we will design soon:
      Intent i;
       i = new Intent(this, GameActivity.class);
       startActivity(i);

At this point, the app will run and the player can click on the Play button to take them to our game screen. So let's quickly create our game screen UI:

  1. Open activity_game.xml in the editor and delete the Hello World TextView.
  2. Drag three Large Text elements one below the other and center them horizontally. Below them, add four buttons stacked one on top of the other, and finally, add another button below that but offset it to the right-hand side so that it looks like what is shown in the next screenshot. I have also adjusted the text properties for the UI elements to make it clear what each will be used for, but this is optional because our Java code will do all of the work for us. You can also tweak the sizes of the elements in the usual way to suit the emulator or device you will be running the game on.
  3. Now let's assign some useful IDs to our UI elements so that we can do some Java magic with them in the next tutorial. Here is a table that matches the UI elements shown in the last screenshot with the id property value that you need to assign. Assign the following id property values to the corresponding UI elements:

Now that we have our game menu and actual game UI ready to go, we can start to make it work.

Phase 2 – preparing our variables and presenting the pattern

Here, we will set up a whole load of variables and objects for us to use, both in this phase and in the later phases. We will also implement the parts of the code that present a pattern to the player. We will add code that enables the player to respond in a later phase:

  1. Open GameActivity.java in the editor window.
  2. I made the sounds by finding a pleasing one then slowly increasing the Frequency slider for each subsequent sample. You can use my sound from the assets folder in the MemoryGame project or create your own sound using Bfxr.
  3. In the main folder in the project explorer window, we need to add a folder called assets. So in the project explorer window, right-click on the main folder and navigate to New | Directory. Type assets in the New Directory dialog box.
  4. Now copy and paste the four sound files to the newly created assets folder. You can do so like this: select the files, right-click on them, and then click on Copy. Then click on the assets folder in the Android Studio project explorer. Now right-click on the assets folder and click on Paste.

Let's prepare GameActivity to listen to button clicks just as we did for MainActivity, as follows:

  1. Add implementsView.onClickListener to the end of the GameActivity declaration so that it now looks like this:
      public class GameActivity extends Activity implements View.OnClickListener {
  2. Now hover your mouse over the line you just typed and right-click on it. Now click on Generate, then on Implement methods..., and then on OK to have Android Studio autogenerate the onClick method that we will use shortly.
  3. Let's declare some objects that we need to reference our UI and our int references for the sound effects we will load soon. Write the code just after the declaration for GameActivity. By putting them here, they will be available to all parts of our code in GameActivity.java. Here is the code in context:
    public class GameActivity extends Activity implements View.OnClickListener {
    
    //Prepare objects and sound references
    
        //initialize sound variables
        private SoundPool soundPool;
        int sample1 = -1;
        int sample2 = -1;
        int sample3 = -1;
        int sample4 = -1;
    
        //for our UI
        TextView textScore;
        TextView textDifficulty;
        TextView textWatchGo;
    
        Button button1;
        Button button2;
        Button button3;
        Button button4;
        Button buttonReplay;
  4. Now, after the last line of code from the previous step, enter the following code snippet, which will declare and initialize some variables for use in our thread. Notice that at the end, we also declare myHandler, which will be our thread, and gameOn to control whether our code within the thread is executed:
    //Some variables for our thread
    int difficultyLevel = 3;
    //An array to hold the randomly generated sequence
    int[] sequenceToCopy = new int[100];
    
    private Handler myHandler;
    //Are we playing a sequence at the moment?
    boolean playSequence = false;
    //And which element of the sequence are we on
    int elementToPlay = 0;
    
    //For checking the players answer
    int playerResponses;
    int playerScore;
    boolean isResponding;
  5. Just after our call to setContentView in the onCreate method, we make our sound effects ready to be played:
    soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);
    try{
      //Create objects of the 2 required classes
      AssetManager assetManager = getAssets();
      AssetFileDescriptor descriptor;
    
      //create our three fx in memory ready for use
      descriptor = assetManager.openFd("sample1.ogg");
      sample1 = soundPool.load(descriptor, 0);
    
            descriptor = assetManager.openFd("sample2.ogg");
            sample2 = soundPool.load(descriptor, 0);
    
    
            descriptor = assetManager.openFd("sample3.ogg");
            sample3 = soundPool.load(descriptor, 0);
    
            descriptor = assetManager.openFd("sample4.ogg");
            sample4 = soundPool.load(descriptor, 0);
    
    
            }catch(IOException e){
                //catch exceptions here
            }
  6. Just after the code in the last step and still within the onCreate method, we initialize our objects and set click listeners for the buttons:
    //Reference all the elements of our UI 
    //First the TextViews
    textScore = (TextView)findViewById(R.id.textScore);
    textScore.setText("Score: " + playerScore);
    textDifficulty = (TextView)findViewById(R.id.textDifficulty);
    
    textDifficulty.setText("Level: " + difficultyLevel);
    textWatchGo = (TextView)findViewById(R.id.textWatchGo);
            
    //Now the buttons
    button1 = (Button)findViewById(R.id.button);
    button2 = (Button)findViewById(R.id.button2);
    button3 = (Button)findViewById(R.id.button3);
    button4 = (Button)findViewById(R.id.button4);
    buttonReplay = (Button)findViewById(R.id.buttonReplay);
    
    //Now set all the buttons to listen for clicks
    button1.setOnClickListener(this);
    button2.setOnClickListener(this);
    button3.setOnClickListener(this);
    button4.setOnClickListener(this);
    buttonReplay.setOnClickListener(this);
  7. Now, after the last line of the code from the previous step, enter the code that will create our thread. We will add the details in the next step within the if(playSequence) block. Notice that the thread is run every nine-tenths of a second (900 milliseconds). Notice that we start the thread but do not set playSequence to true. So it will not do anything yet:
    //This is the code which will define our thread
    myHandler = new Handler() {
      public void handleMessage(Message msg) {
        super.handleMessage(msg);
    
              if (playSequence) {
              //All the thread action will go here
    
              }
    
              myHandler.sendEmptyMessageDelayed(0, 900);
      }
    };//end of thread
    
    myHandler.sendEmptyMessage(0);
  8. Before we look at the code that will run in our thread, we need a way to generate a random sequence appropriate for the difficulty level. This situation sounds like a candidate for a method. Enter this method just before the closing curly brace of the GameActivity class:
    public void createSequence(){
      //For choosing a random button
       Random randInt = new Random();
       int ourRandom;
       for(int i = 0; i < difficultyLevel; i++){
       //get a random number between 1 and 4
             ourRandom = randInt.nextInt(4);
             ourRandom ++;//make sure it is not zero
             //Save that number to our array
             sequenceToCopy[i] = ourRandom;
       }
    
    }
  9. We also need a method to prepare and start our thread. Type the following method after the closing curly brace of createSequence:

    Tip

    Actually, the order of implementation of the methods is unimportant. However, following along in order will mean our code will look the same. Even if you are referring to the downloaded code, the order will be the same.

    public void playASequence(){
        createSequence();
        isResponding = false;
        elementToPlay = 0;
        playerResponses = 0;
        textWatchGo.setText("WATCH!");
        playSequence = true;
    }
  10. Just before we look at the details of the thread code, we need a method to tidy up our variables after the sequence has been played. Enter this method after the closing curly brace of playASequence:
    public void sequenceFinished(){
            playSequence = false;
            //make sure all the buttons are made visible
            button1.setVisibility(View.VISIBLE);
            button2.setVisibility(View.VISIBLE);
            button3.setVisibility(View.VISIBLE);
            button4.setVisibility(View.VISIBLE);
            textWatchGo.setText("GO!");
            isResponding = true;
        }
  11. Finally, we will implement our thread. There is some new code in this part, which we will go through in detail after we finish this phase of the project. Enter this code between the opening and closing curly braces of the if(playSequence){ } block:
    if (playSequence) {
      //All the thread action will go here
      //make sure all the buttons are made visible
      button1.setVisibility(View.VISIBLE);
      button2.setVisibility(View.VISIBLE);
      button3.setVisibility(View.VISIBLE);
      button4.setVisibility(View.VISIBLE);
    
      switch (sequenceToCopy[elementToPlay]){
        case 1:
          //hide a button 
    button1.setVisibility(View.INVISIBLE);
           //play a sound
           soundPool.play(sample1, 1, 1, 0, 0, 1);
           break;
    
        case 2:
          //hide a button 
    button2.setVisibility(View.INVISIBLE)
          //play a sound
          soundPool.play(sample2, 1, 1, 0, 0, 1);
          break;
    
        case 3:
          //hide a button button3.setVisibility(View.INVISIBLE);
          //play a sound
          soundPool.play(sample3, 1, 1, 0, 0, 1);
          break;
    
      case 4:
          //hide a button 
    button4.setVisibility(View.INVISIBLE);
          //play a sound
          soundPool.play(sample4, 1, 1, 0, 0, 1);
             break;
       }
    
       elementToPlay++;
       if(elementToPlay == difficultyLevel){
       sequenceFinished();
       }
    }
    
        myHandler.sendEmptyMessageDelayed(0, 900);
    }
    
    };

Tip

Just before the closing curly brace of onCreate, we could initiate a sequence by calling our playASequence method, like this:

playASequence();

We could then run our app, click on Play on the home screen, and watch as a sequence of four random buttons and their matching sounds begins, with the sounds being played. In the next phase, we will wire up the Replay button so that the player can start the sequence when they are ready.

Phew! That was a long one. Actually, there is not much new there, but we did cram in just about everything we ever learned about Java and Android into one place, and we used it in new ways too. So we will look at it step by step and give extra focus to the parts that might seem tricky.

Let's look at each new piece of code in turn.

From steps 1 to 7, we initialized our variables, set up our buttons, and loaded our sounds as we have done before. We also put in the outline of the code for our thread.

In step 8, we implemented the createSequence method. We used a Random object to generate a sequence of random numbers between 1 and 4. We did this in a for loop, which loops until a sequence the length of difficultyLevel has been created. The sequence is stored in an array called sequenceToCopy, which we can later use to compare to the player's response:

public void createSequence(){
        //For choosing a random button
        Random randInt = new Random();
        int ourRandom;
        for(int i = 0; i < difficultyLevel; i++){
            //get a random number between 1 and 4
            ourRandom = randInt.nextInt(4);
            ourRandom ++;//make sure it is not zero
            //Save that number to our array
            sequenceToCopy[i] = ourRandom;
        }

    }

In step 9, we implemented playASequence. First, we call createSequence to load our sequenceToCopy array. Then we set isResponding to false because we don't want the player to bash buttons while the sequence is still playing. We set elementToPlay to 0 as this is the first element of our array. We also set playerResponses to 0, ready to count the player's responses. Next, we set some text on the UI to "WATCH!" to make it clear to the player that the sequence is playing. Finally, we set playSequence to true, which allows the code in our thread to run once every 900 milliseconds. Here is the code we have just analyzed:

public void playASequence(){
        createSequence();
        isResponding = false;
        elementToPlay = 0;
        playerResponses = 0;
        textWatchGo.setText("WATCH!");
        playSequence = true;

    }

In step 10, we handle sequenceFinished. We set playSequence to false, which prevents the code in our thread from running. We set all the buttons back to visible because, as we will see in the thread code, we set them to invisible to emphasize which button comes next in the sequence. We set our UI text to GO! to make it clear. It is time for the player to try and copy the sequence. For the code in the checkElement method to run, we set isResponding to true. We will look at the code in the checkElement method in the next phase:

public void sequenceFinished(){
        playSequence = false;
        //make sure all the buttons are made visible
        button1.setVisibility(View.VISIBLE);
        button2.setVisibility(View.VISIBLE);
        button3.setVisibility(View.VISIBLE);
        button4.setVisibility(View.VISIBLE);
        textWatchGo.setText("GO!");
        isResponding = true;
    }

In step 11, we implement our thread. It's quite long but not too complicated. First, we set all the buttons to visible as this is quicker than checking which one of them is currently invisible and setting just that one:

if (playSequence) {
  //All the thread action will go here
  //make sure all the buttons are made visible
  button1.setVisibility(View.VISIBLE);
  button2.setVisibility(View.VISIBLE);
  button3.setVisibility(View.VISIBLE);
  button4.setVisibility(View.VISIBLE);

Then we switch based on what number is next in our sequence, hide the appropriate button, and play the appropriate sound. Here is the first case in the switch block for reference. The other case elements perform the same function but on a different button and with a different sound:

switch (sequenceToCopy[elementToPlay]){
  case 1:
    //hide a buttonbutton1.setVisibility(View.INVISIBLE);
         //play a sound
         soundPool.play(sample1, 1, 1, 0, 0, 1);
         break;

    //case 2, 3 and 4 go here

Now we increment elementToPlay, ready to play the next part of the sequence when the thread runs again in approximately 900 milliseconds:

   elementToPlay++;

Next, we check whether we have played the last part of the sequence. If we have, we call our sequenceFinished method to set things up for the player to attempt their answer:

   if(elementToPlay == difficultyLevel){
   sequenceFinished();
   }
}

Finally, we tell the thread when we would like to run our code again:

    myHandler.sendEmptyMessageDelayed(0, 900);
}

};

When you ran a sequence (see the previous tip), did you notice an imperfection/bug with our game operation? This has to do with the way the last element of the sequence is animated. It is because our sequenceFinished method makes all the buttons visible so soon after the button has just been made invisible that looks like the button is never made invisible at all. We will solve the problem of the button that doesn't stay invisible long enough when we learn about UI animation in phase 5.

Now let's handle the player's response.

Phase 3 – the player's response

We now have an app that plays a random sequence of button flashes and matching sounds. It also stores that sequence in an array. So what we have to do now is enable the player to attempt to replicate the sequence and score points if successful.

We can do all of this in two phases. First, we need to handle the button presses, which can pass all the hard work to a method that will do everything else.

Let's write the code and look at it as we go. Afterwards, we will closely examine the less obvious parts:

  1. Here is how we handle the button presses. We have the empty body of the switch statement with an extra if statement that checks whether there is a sequence currently being played. If there is a sequence, then no input is accepted. We will start to fill the code in the empty body in the next step:
    if(!playSequence) {//only accept input if sequence not playing
                switch (view.getId()) {
                    //case statements here...
                }
    }
  2. Now, here is the code that handles button1. Notice that it just plays the sound related to button1 and then calls the checkElement method, passing a value of 1. This is all we have to do for the buttons 1 through 4: play a sound and then tell our new method (checkElement) which numbered button was pressed, and checkElement will do the rest:
    case R.id.button:
      //play a sound
       soundPool.play(sample1, 1, 1, 0, 0, 1);
       checkElement(1);
       break;
  3. Here is the near-identical code for buttons 2 through 4. Notice that the value passed to checkElement and the sound sample that is played are the only differences from the previous step. Enter this code directly after the code in the previous step:
    case R.id.button2:
      //play a sound
       soundPool.play(sample2, 1, 1, 0, 0, 1);
       checkElement(2);
       break;
    
    case R.id.button3:
       //play a sound
       soundPool.play(sample3, 1, 1, 0, 0, 1);
       checkElement(3);
       break;
    
    case R.id.button4:
       //play a sound
       soundPool.play(sample4, 1, 1, 0, 0, 1);
       checkElement(4);
       break;
  4. Here is the last part of the code in our onClick method. This handles the Restart button. The code just resets the score and the difficulty level and then calls our playASequence method, which does the rest of the work of starting the game again. Enter this code directly after the code in the previous step:
    case R.id.buttonReplay:
       difficultyLevel = 3;
       playerScore = 0;
       textScore.setText("Score: " + playerScore);
       playASequence();
       break;
  5. Finally, here is our do-everything method. This is quite a long method compared to most of our previous methods, but it helps to see its entire structure. We will break this down line by line in a minute. Enter the following code, after which you will actually be able to play the game and get a score:
    public void checkElement(int thisElement){
    
    if(isResponding) {
      playerResponses++;
       if (sequenceToCopy[playerResponses-1] == thisElement) { //Correct
       playerScore = playerScore + ((thisElement + 1) * 2);
       textScore.setText("Score: " + playerScore);
       if (playerResponses == difficultyLevel) {//got the whole sequence
       //don't checkElement anymore
       isResponding = false;
       //now raise the difficulty
       difficultyLevel++;
       //and play another sequence
       playASequence();
        }
    
    } else {//wrong answer
      textWatchGo.setText("FAILED!");
        //don't checkElement anymore
        isResponding = false;
    }
    }

We covered the methods fairly comprehensively as we went through the tutorial. The one elephant in the room, however, is the apparent sprawl of code in the checkElement method. So let's go through all of the code in step 6, line by line.

First, we have the method signature. Notice that it does not return a value but it receives an int value. Remember that it is the onClick method that calls this method and it passes a 1, 2, 3, or 4, depending upon which button was clicked:

public void checkElement(int thisElement){

Next, we wrap the rest of this code into an if statement. Here is the if statement. We enter the block when the isResponding Boolean is true, and isResponding is set to true when the sequenceFinnished method completes, which is just what we need so that the player can't mash the buttons until it is time to do so and our game is ready to listen:

if(isResponding) {

Here is what happens inside the if block. We increment the number of the player's responses received in the playerResponses variable:

playerResponses++;

Now we check whether the number passed to the checkElement method and stored in thisElement matches the appropriate part of the sequence the player is trying to copy. If it matches, we increase playerScore by an amount relative to the number of correctly matched parts of the sequence so far. Then we set the score on the screen. Notice that if the response does not match, there is an else block to go with this if block that we will explain soon:

   if (sequenceToCopy[playerResponses-1] == thisElement) {  //Correct
      playerScore = playerScore + ((thisElement + 1) * 2);
      textScore.setText("Score: " + playerScore);
      

Next, we have another if block. Note that this if block is nested inside the if block we just described. So it will only be tested and potentially run if the player's response was correct. This if statement checks whether it is the last part of the sequence, like this:

      if (playerResponses == difficultyLevel) {

If it is the last part of the sequence, it executes the following lines:

//got the whole sequence
         //don't checkElement anymore
         isResponding = false;
         //now raise the difficulty
         difficultyLevel++;
         //and play another sequence
         playASequence();
   }

What is happening inside the nested if statement, which checks whether the whole sequence has been correctly copied, is the following: It sets isResponding to false, so the player gets no response from the buttons. It then raises the difficulty level by 1 so that the sequence is a bit tougher next time. Finally, it calls the playSequence method to play another sequence and the whole process starts again.

Here is the else block, which runs if the player gets part of the sequence wrong:

} else {
  //wrong answer
  textWatchGo.setText("FAILED!");
  //don't checkElement anymore
  isResponding = false;
  }
}

Here, we set some text on the screen and set isResponding to false.

Now let's use what we learned about the SharedPreferences class to preserve the high scores.

Phase 4 – preserving the high score

This phase is nice and short. We will use what we learned earlier in the chapter to save the player's score if it is a new high score, and then display the best score in the hi-score TextView in our MainActivity:

  1. Open MainActivity.java in the editor window.
  2. Then we declare our objects used to read from a file just after the class declaration, like this:
    public class MainActivity extends Activity implements View.OnClickListener{
    
        //for our hiscore (phase 4)
     SharedPreferences prefs;
     String dataName = "MyData";
     String intName = "MyInt";
     int defaultInt = 0;
     //both activities can see this
     public static int hiScore;
    
  3. Now, just after our call to setContentView in the onCreate method, we initialize our objects, read from our file, and set the result to our hiScore variable. We then display it to the player:
    //for our high score (phase 4)
    //initialize our two SharedPreferences objects
    prefs = getSharedPreferences(dataName,MODE_PRIVATE);
    
    //Either load our High score or
    //if not available our default of 0
    hiScore = prefs.getInt(intName, defaultInt);
    
    //Make a reference to the Hiscore textview in our layout
    TextView textHiScore =(TextView) findViewById(R.id.textHiScore);
    //Display the hi score
    textHiScore.setText("Hi: "+ hiScore);
  4. Next, we need to go back to the GameActivity.java file.
  5. We declare our objects to edit our file, this time like this:
    //for our hiscore (phase 4)
    SharedPreferences prefs;
    SharedPreferences.Editor editor;
    String dataName = "MyData";
    String intName = "MyInt";
    int defaultInt = 0;
    int hiScore;
  6. Just after the call to setContentView in the onCreate method, we instantiate our objects and assign a value to hiScore:
    //phase 4
    //initialize our two SharedPreferences objects
    prefs = getSharedPreferences(dataName,MODE_PRIVATE);
    editor = prefs.edit();
    hiScore = prefs.getInt(intName, defaultInt);
  7. The only thing that is different to what we have already learned is that we need to consider where we put the code to test for a high score and where to write to our file if appropriate. Consider this: eventually, every player must fail. Furthermore, the point at which they fail is the point when their score is at its highest, yet before it is reset when they try again. Place the following code in the else block, which handles a wrong answer from the player. The highlighted code is the new code; the rest is there to help you with the context:
    } else {//wrong answer
    
      textWatchGo.setText("FAILED!");
        //don't checkElement anymore
        isResponding = false;
    
     //for our high score (phase 4)
     if(playerScore > hiScore) {
     hiScore = playerScore;
     editor.putInt(intName, hiScore);
     editor.commit();
     Toast.makeText(getApplicationContext(), "New Hi-score", Toast.LENGTH_LONG).show();
     }
    
    }

Play the game and get a high score. Now quit the app or even restart the phone. When you come back to the app, your high score is still there.

The code we added in this phase is nearly the same as the code we wrote in our previous example of persistence, the only difference being that we wrote to the data store when a new high score was achieved instead of when a button was pressed. In addition, we used the editor.putInt method because we were saving an integer instead of editor.putString when we were saving a string.

Animating our game

Before we go ahead, let's just think about animation. What is it exactly? The word probably conjures up images of moving cartoon characters and in-game characters of a video game.

We need to animate our buttons (make them move) to make it clear when they are part of the sequence. We saw that simply making one disappear and then reappear was inadequate.

The thought of controlling the movement of UI elements might make us imagine complex for loops and per-pixel calculations.

Fortunately, Android provides us with the Animation class, which allows us to animate UI objects without any such per-pixel awkwardness. Here is how it works.

Note

Of course, to fully control the shape and size of in-game objects, we must eventually learn to manipulate individual pixels and lines. We will do so from Chapter 7, Retro Squash Game, onwards, when we make a retro pong-style squash game.

UI animation in Android

Animations in the Android UI can be divided into three phases:

  • Describing the animation in a file using a special syntax we will see shortly
  • Referencing that animation by creating an object of it in our Java code
  • Applying the animation to a UI element when the animation is required to run

Let's take a look at some code that describes an animation. We will soon be reusing this same code in our memory game. The purpose of showing it is not so much that we understand each and every line of it. After all, learning Java should be enough of an accomplishment without mastering this too. Moreover, the purpose is to demonstrate that whatever animation you can describe can then be used in our games using the same Java.

We can quickly search the Web to find the code to perform the following:

  • Fading in and out
  • Sliding
  • Rotating
  • Expanding or shrinking
  • Morphing color

Here is some code that causes a wobble effect. We will use it on a button, but you can also use it on any UI element or even the whole screen:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromDegrees="-5"
    android:pivotX="50%"
    android:pivotY="50%"
    android:repeatCount="8"
    android:repeatMode="reverse"
    android:toDegrees="5" />

The first line simply states that this is a file written in XML format. The next states that we will be performing a rotation. Then we state that the duration will be 100 milliseconds, the rotation will be from -5 degrees, the pivot will be on the x and y axes by 50 percent, repeat eight times, and reverse to positive 5 degrees.

This is quite a mouthful, but the point is that it is easy to grab a template that works and then customize it to fit our situation. We could save the preceding code with a filename like wobble.xml.

Then we could simply reference it as follows:

Animation wobble = AnimationUtils.loadAnimation(this, R.anim.wobble);

Now we can play the animation like this on our chosen UI object, in this case our button1 object:

button1.startAnimation(wobble);

Phase 5 – animating the UI

Let's add an animation that causes a button to wobble when a button sound is played. At the same time, we can remove the code that makes the button invisible and the code that makes it reappear. That wasn't the best way to do it, but it served a purpose while developing the game:

  1. We need to add a new folder to our project, called anim. So right-click on the res folder in the Project Explorer window. Navigate to New | Android resource directory and click on OK to create the new anim folder.
  2. Now right-click on the anim folder and navigate to New | Animation resource file. Enter wobble in the File name field and click on OK. We now have a new file called wobble.xml open in the editor window.
  3. Replace all but the first line of wobble.xml with this code:
    <?xml version="1.0" encoding="utf-8"?>
    <rotate xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="100"
     android:fromDegrees="-5"
     android:pivotX="50%"
     android:pivotY="50%"
     android:repeatCount="8"
     android:repeatMode="reverse"
     android:toDegrees="5" />
    
  4. Now switch to GameActivity.java.
  5. Add the following code just after the declaration of our GameActivity class:
    //phase 5 - our animation object
    Animation wobble;
  6. Just after the call to setContentView in our onCreate method, add this piece of code:
    //phase5 - animation
    wobble = AnimationUtils.loadAnimation(this, R.anim.wobble);
  7. Now, near the start of our thread code, find the calls to make our buttons reappear. Comment them out like this:
    //code not needed as using animations
    //make sure all the buttons are made visible
    //button1.setVisibility(View.VISIBLE);
    //button2.setVisibility(View.VISIBLE);
    //button3.setVisibility(View.VISIBLE);
    //button4.setVisibility(View.VISIBLE);
  8. Next, directly after our code in the previous step, within each of the four case statements, we need to comment out the lines that call setVisibility and replace them with our wobble animation. The following code is slightly abbreviated but shows exactly where to comment and where to add the new lines:
    switch (sequenceToCopy[elementToPlay]){
      case 1:
        //hide a button - not any more
        //button1.setVisibility(View.INVISIBLE);
     button1.startAnimation(wobble);
       ...
       ...
      case 2:
        //hide a button - not any more
        //button2.setVisibility(View.INVISIBLE);
     button2.startAnimation(wobble);
       ...
       ...
      case 3:
        //hide a button - not any more
        //button3.setVisibility(View.INVISIBLE);
     button3.startAnimation(wobble);
       ...
       ...
      case 4:
        //hide a button - not any more
        //button4.setVisibility(View.INVISIBLE);
     button4.startAnimation(wobble);
    
  9. Finally, in our sequenceFinished method, we can comment out all the setVisibility calls, just as we did in our thread, like this:
    //button1.setVisibility(View.VISIBLE);
    //button2.setVisibility(View.VISIBLE);
    //button3.setVisibility(View.VISIBLE);
    //button4.setVisibility(View.VISIBLE);

That was not too tough. We added the wobble animation to the anim folder, declared an animation object, and initialized it. Then we used it whenever it was required on the appropriate button.

There are obviously loads of improvements we could make to this game, especially to its appearance. I'm sure you can think of more. And certainly, if this was to be your app, you were trying to make it big on the Play Store. That is exactly what you should do.

Constantly improve all aspects and strive to be the best in your genre. If you feel the urge, then why not improve upon it?

Here are a few self-test questions that look at ways we could do more with some of the examples from this chapter.

Self-test questions

Q1) Suppose that we want to have a quiz where the question could be to name the president as well as capital city. How can we do this with multidimensional arrays?

Q2) In our Persistence example section, we saved a continually updating string to a file so that it persisted after the app had been shut down and restarted. This is like asking the user to click on a Save button. Summoning all your knowledge of Chapter 2, Getting Started with Android, can you think of a way to save the string without saving it in the button click but just when the user quits the app?

Q3) Other than increasing the difficulty level, how could we increase the challenge of our memory game for our players?

Q4) Using the plain Android UI with the dull grey buttons isn't very exciting. Take a look at the UI elements in the visual designer and try and work out how we could quickly improve the visual appearance of our UI.

Summary

That was a bit of a hefty chapter, but we learned lots of new techniques such as storing and manipulating with arrays, creating and using sound effects, and saving important data such as a high score, in our game. We also took a very brief look at the powerful but simple-to-use Animation class.

In the next chapter, we will be taking a more theoretical approach, but we will have plenty of working samples too. We will finally be opening the black box of Java classes so that we can gain an understanding of what is going on when we declare and use objects of a class.