Chapter 5

An Android Game Development Framework

As you may have noticed, we’ve been through four chapters without writing a single line of game code. The reason we’ve put you through all of this boring theory and asked you to implement test programs is simple: if you want to write games, you have to know exactly what’s going on. You can’t just copy and paste code together from all over the Web and hope that it will form the next first-person shooter hit. By now, you should have a firm grasp on how to design a simple game from the ground up, how to structure a nice API for 2D game development, and which Android APIs will provide the functionality you need to implement your ideas.

To make Mr. Nom a reality, we have to do two things: implement the game framework interfaces and classes we designed in Chapter 3 and, based on that, code up Mr. Nom’s game mechanics. Let’s start with the game framework by merging what we designed in Chapter 3 with what we discussed in Chapter 4. Ninety percent of the code should be familiar to you already, since we covered most of it in the test programs in the previous chapter.

Plan of Attack

In Chapter 3, we laid out a minimal design for a game framework that abstracts away all the platform specifics so that we could concentrate on what we are here for: game development. Now, we’ll implement all these interfaces and abstract classes in a bottom-up fashion, from easiest to hardest. The interfaces from Chapter 3 are located in the package com.badlogic.androidgames.framework. We’ll put the implementation from this chapter in the package, com.badlogic.androidgames.framework.impl, and indicate that it holds the actual implementation of the framework for Android. We’ll prefix all our interface implementations with Android so that we can distinguish them from the interfaces. Let’s start off with the easiest part, file I/O.

The code for this chapter and the next will be merged into a single Eclipse project. For now, you can just create a new Android project in Eclipse following the steps in Chapter 4. At this point, it doesn’t matter what you name your default activity.

The AndroidFileIO Class

The original FileIO interface was lean and mean. It contained four methods: one to get an InputStream for an asset, another to get an InputStream for a file in the external storage, a third that returned an OutputStream for a file on the external storage device, and a final one that got the shared preferences for the game. In Chapter 4, you learned how to open assets and files on the external storage using Android APIs. Listing 5-1 presents the implementation of the FileIO interface, based on knowledge from Chapter 4.

Listing 5-1. AndroidFileIO.java; Implementing the FileIO Interface

package com.badlogic.androidgames.framework.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.AssetManager;
import android.os.Environment;
import android.preference.PreferenceManager;
import com.badlogic.androidgames.framework.FileIO;

public class AndroidFileIO implements FileIO {
    Context context;
    AssetManager assets;
    String externalStoragePath;

    public AndroidFileIO(Context context) {
        this.context = context;
        this.assets = context.getAssets();
        this.externalStoragePath = Environment.getExternalStorageDirectory()
                .getAbsolutePath() + File.separator;
    }
    public InputStream readAsset(String fileName) throws IOException {
        return assets.open(fileName);
    }
    public InputStream readFile(String fileName) throws IOException {
        return new FileInputStream(externalStoragePath + fileName);
    }
    public OutputStream writeFile(String fileName) throws IOException {
        return new FileOutputStream(externalStoragePath + fileName);
    }
    public SharedPreferences getPreferences() {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }
}

Everything is straightforward. We implement the FileIO interface, store the Context instance,  which is the gateway to almost everything in Android, store an AssetManager, which we pull from the Context, store the external storage’s path, and implement the four methods based on this path. Finally, we pass through any IOExceptions that get thrown so we’ll know if anything is irregular on the calling side.

Our Game interface implementation will hold an instance of this class and return it via Game.getFileIO(). This also means that our Game implementation will need to pass through the Context in order for the AndroidFileIO instance to work.

Note that we do not check if the external storage is available. If it’s not available, or if we forget to add the proper permission to the manifest file, we’ll get an exception, so checking for errors is implicit. Now, we can move on to the next piece of our framework, which is audio.

AndroidAudio, AndroidSound, and AndroidMusic: Crash, Bang, Boom!

In Chapter 3, we designed three interfaces for all our audio needs: Audio, Sound, and Music. Audio is responsible for creating sound and Music instances from asset files. Sound lets us play back sound effects that are stored in RAM, and Music streams bigger music files from the disk to the audio card. In Chapter 4, you learned which Android APIs are needed to implement this. We will start with the implementation of AndroidAudio, as shown in Listing 5-2, interspersed with explanatory text where appropriate.

Listing 5-2. AndroidAudio.java; Implementing the Audio Interface

package com.badlogic.androidgames.framework.impl;

import java.io.IOException;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;

import com.badlogic.androidgames.framework.Audio;
import com.badlogic.androidgames.framework.Music;
import com.badlogic.androidgames.framework.Sound;
public class AndroidAudio implements Audio {
    AssetManager assets;
    SoundPool soundPool;

The AndroidAudio implementation has an AssetManager and a SoundPool instance. The AssetManager is necessary for loading sound effects from asset files into the SoundPool on a call to AndroidAudio.newSound(). The AndroidAudio instance also manages the SoundPool.

    public AndroidAudio(Activity activity) {
        activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
        this.assets = activity.getAssets();
        this.soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
    }

There are two reasons why we pass our game’s Activity in the constructor: it allows us to set the volume control of the media stream (we always want to do that), and it gives us an AssetManager instance, which we will happily store in the corresponding class member. The SoundPool is configured to play back 20 sound effects in parallel, which is adequate for our needs.

    public Music newMusic(String filename) {
        try {
            AssetFileDescriptor assetDescriptor = assets.openFd(filename);
            return new AndroidMusic(assetDescriptor);
        }catch (IOException e) {
            throw new RuntimeException("Couldn't load music '" + filename + "'");
        }
    }

The newMusic() method creates a new AndroidMusic instance. The constructor of that class takes an AssetFileDescriptor, which it uses to create an internal MediaPlayer (more on that later). The AssetManager.openFd() method throws an IOException in case something goes wrong. We catch it and rethrow it as a RuntimeException. Why not hand the IOException to the caller? First, it would clutter the calling code considerably, so we would rather throw a RuntimeException that does not have to be caught explicitly. Second, we load the music from an asset file. It will only fail if we actually forget to add the music file to the assets/directory, or if our music file contains false bytes. False bytes constitute unrecoverable errors since we need that Music instance for our game to function properly. To avoid such an occurrence, we throw RuntimeExceptions instead of checked exceptions in a few more places in the framework of our game.

    public Sound newSound(String filename) {
        try {
            AssetFileDescriptor assetDescriptor = assets.openFd(filename);
            int soundId = soundPool.load(assetDescriptor, 0);
            return new AndroidSound(soundPool, soundId);
        }catch (IOException e) {
            throw new RuntimeException("Couldn't load sound '" + filename + "'");
        }
    }
}

Finally, the newSound() method loads a sound effect from an asset into the SoundPool and returns an AndroidSound instance. The constructor of that instance takes a SoundPool and the ID of the sound effect assigned to it by the SoundPool. Again, we catch any IOException and rethrow it as an unchecked RuntimeException.

Note  We do not release the SoundPool in any of the methods. The reason for this is that there will always be a single Game instance holding a single Audio instance that holds a single SoundPool instance. The SoundPool instance will, thus, be alive as long as the activity (and with it our game) is alive. It will be destroyed automatically as soon as the activity ends.

Next, we will discuss the AndroidSound class, which implements the Sound interface. Listing 5-3 presents its implementation.

Listing 5-3. Implementing the Sound Interface Using AndroidSound.java

package com.badlogic.androidgames.framework.impl;

import android.media.SoundPool;

import com.badlogic.androidgames.framework.Sound;

public class AndroidSoundimplements Sound {
    int soundId;
    SoundPool soundPool;

    public AndroidSound(SoundPool soundPool, int soundId) {
        this.soundId = soundId;
        this.soundPool = soundPool;
    }

    public void play(float volume) {
        soundPool.play(soundId, volume, volume, 0, 0, 1);
    }

    public void dispose() {
        soundPool.unload(soundId);
    }
}

There are no surprises here. Via the play() and dispose() methods, we simply store the SoundPool and the ID of the loaded sound effect for later playback and disposal. It doesn’t get any easier than this, thanks to the Android API.

Finally, we have to implement the AndroidMusic class returned by AndroidAudio.newMusic(). Listing 5-4 shows that class’s code, which looks a little more complex than before. This is due to the state machine that the MediaPlayer uses, which will continuously throw exceptions if we call methods in certain states. Note that the listing is broken up again, with commentary inserted where appropriate.

Listing 5-4. AndroidMusic.java; Implementing the Music Interface

package com.badlogic.androidgames.framework.impl;

import java.io.IOException;

import android.content.res.AssetFileDescriptor;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;

import com.badlogic.androidgames.framework.Music;

public class AndroidMusic implements Music, OnCompletionListener {
    MediaPlayer mediaPlayer;
    boolean isPrepared = false ;

The AndroidMusic class stores a MediaPlayer instance along with a Boolean called isPrepared. Remember, we can only call MediaPlayer.start()/stop()/pause() when the MediaPlayer is prepared. This member helps us keep track of the MediaPlayer’s state.

The AndroidMusic class implements the Music interface as well as the OnCompletionListener interface. In Chapter 4, we briefly defined this interface as a means of informing ourselves about when a MediaPlayer has stopped playing back a music file. If this happens, the MediaPlayer needs to be prepared again before we can invoke any of the other methods. The method OnCompletionListener.onCompletion() might be called in a separate thread, and since we set the isPrepared member in this method, we have to make sure that it is safe from concurrent modifications.

    public AndroidMusic(AssetFileDescriptor assetDescriptor) {
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource(assetDescriptor.getFileDescriptor(),
                    assetDescriptor.getStartOffset(),
                    assetDescriptor.getLength());
            mediaPlayer.prepare();
            isPrepared = true ;
            mediaPlayer.setOnCompletionListener(this );
        }catch (Exception e) {
            throw new RuntimeException("Couldn't load music");
        }
    }

In the constructor, we create and prepare the MediaPlayer from the AssetFileDescriptor that is passed in, and we set the isPrepared flag, as well as register the AndroidMusic instance as an OnCompletionListener with the MediaPlayer. If anything goes wrong, we throw an unchecked RuntimeException once again.

    public void dispose() {
        if (mediaPlayer.isPlaying())
            mediaPlayer.stop();
        mediaPlayer.release();
    }

The dispose() method checks if the MediaPlayer is still playing and, if so, stops it. Otherwise, the call to MediaPlayer.release() will throw a RuntimeException.

    public boolean isLooping() {
        return mediaPlayer.isLooping();
    }
    public boolean isPlaying() {
        return mediaPlayer.isPlaying();
    }
    public boolean isStopped() {
        return !isPrepared;
    }

The methods isLooping(), isPlaying(), and isStopped() are straightforward. The first two use methods provided by the MediaPlayer; the last one uses the isPrepared flag, which indicates if the MediaPlayer is stopped. This is something MediaPlayer.isPlaying() does not necessarily tell us since it returns false if the MediaPlayer is paused but not stopped.

    public void pause() {
        if (mediaPlayer.isPlaying())
            mediaPlayer.pause();
    }

The pause() method simply checks whether the MediaPlayer instance is playing and calls its pause() method if it is.

    public void play() {
        if (mediaPlayer.isPlaying())
            return ;
        try {
            synchronized (this ) {
                if (!isPrepared)
                    mediaPlayer.prepare();
                mediaPlayer.start();
            }
        }catch (IllegalStateException e) {
            e.printStackTrace();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

The play() method is a little more involved. If we are already playing, we simply return from the function. Next we have a mighty try. . .catch block within which we check to see if the MediaPlayer is already prepared based on our flag; we prepare it if needed. If all goes well, we call the MediaPlayer.start() method, which will start the playback. This is conducted in a synchronized block, since we are using the isPrepared flag, which might get set on a separate thread because we are implementing the OnCompletionListener interface. In case something goes wrong, we throw an unchecked RuntimeException.

    public void setLooping(boolean isLooping) {
        mediaPlayer.setLooping(isLooping);
    }
    public void setVolume(float volume) {
        mediaPlayer.setVolume(volume, volume);
    }

The setLooping() and setVolume() methods can be called in any state of the MediaPlayer and delegated to the respective MediaPlayer methods.

    public void stop() {
        mediaPlayer.stop();
        synchronized (this ) {
            isPrepared = false ;
        }
    }

The stop() method stops the MediaPlayer and sets the isPrepared flag in a synchronized block.

    public void onCompletion(MediaPlayer player) {
        synchronized (this ) {
            isPrepared = false ;
        }
    }
}

Finally, there’s the OnCompletionListener.onCompletion() method that is implemented by the AndroidMusic class. All it does is set the isPrepared flag in a synchronized block so that the other methods don’t start throwing exceptions out of the blue. Next, we’ll move on to our input-related classes.

AndroidInput and AccelerometerHandler

Using a couple of convenient methods, the Input interface we designed in Chapter 3 grants us access to the accelerometer, the touchscreen, and the keyboard in polling and event modes. The idea of putting all the code for an implementation of that interface into a single file is a bit nasty, so we outsource all the input event handling to handler classes. The Input implementation will use those handlers to pretend that it is actually performing all the work.

AccelerometerHandler: Which Side Is Up?

Let’s start with the easiest of all handlers, the AccelerometerHandler. Listing 5-5 shows its code.

Listing 5-5. AccelerometerHandler.java; Performing All the Accelerometer Handling

package com.badlogic.androidgames.framework.impl;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

public class AccelerometerHandler implements SensorEventListener {
    float accelX;
    float accelY;
    float accelZ;
    public AccelerometerHandler(Context context) {
        SensorManager manager = (SensorManager) context
                .getSystemService(Context.SENSOR_SERVICE);
        if (manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() ! = 0) {
            Sensor accelerometer = manager.getSensorList(
                    Sensor.TYPE_ACCELEROMETER).get(0);
            manager.registerListener(this , accelerometer,
                    SensorManager.SENSOR_DELAY_GAME);
        }
    }
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // nothing to do here
    }
    public void onSensorChanged(SensorEvent event) {
        accelX = event.values[0];
        accelY = event.values[1];
        accelZ = event.values[2];
    }
    public float getAccelX() {
        return accelX;
    }
    public float getAccelY() {
        return accelY;
    }
    public float getAccelZ() {
        return accelZ;
    }
}

Unsurprisingly, the class implements the SensorEventListener interface that we used in Chapter 4. The class stores three members by holding the acceleration on each of the three accelerometers’ axes.

The constructor takes a Context, from which it gets a SensorManager instance to set up the event listening. The rest of the code is equivalent to what we did in Chapter 4. Note that if no accelerometer is installed, the handler will happily return zero acceleration on all axes throughout its life. Therefore, we don’t need any extra error-checking or exception-throwing code.

The next two methods, onAccuracyChanged() and onSensorChanged(), should be familiar. In the first method, we don’t do anything, so there’s nothing much to report. In the second one, we fetch the accelerometer values from the provided SensorEvent and store them in the handler’s members. The final three methods simply return the current acceleration for each axis.

Note that we do not need to perform any synchronization here, even though the onSensorChanged() method might be called in a different thread. The Java memory model guarantees that writes and reads, to and from, primitive types such as Boolean, int, or byte are atomic. In this case, it’s OK to rely on this fact since we aren’t doing anything more complex than assigning a new value. We’d need to have proper synchronization if this were not the case (for example, if we did something with the member variables in the onSensorChanged() method).

CompassHandler

Just for fun, we’re going to provide an example that is similar to the AccelerometerHandler, but this time we’ll give you the compass values along with the pitch and roll of the phone, as shown in Listing 5-6. We call the compass value yaw, since that’s a standard orientation term that nicely defines the value we’re seeing.

Android handles all sensors through the same interfaces, so this example shows you how to cope with that. The only difference between Listing 5-6 and the previous accelerometer example is the change of the sensor type to TYPE_ORIENTATION and the renaming of the fields from accel to yaw, pitch, and roll. Otherwise, it works in the same way, and you can easily swap this code into the game as the control handler!

Listing 5-6. CompassHandler.java; Performing All the Compass Handling

package com.badlogic.androidgames.framework.impl;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

public class CompassHandler implements SensorEventListener {
    float yaw;
    float pitch;
    float roll;
    public CompassHandler(Context context) {
        SensorManager manager = (SensorManager) context
                .getSystemService(Context.SENSOR_SERVICE);
        if (manager.getSensorList(Sensor.TYPE_ORIENTATION).size() ! = 0) {
            Sensor compass = manager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
            manager.registerListener(this , compass,
                    SensorManager.SENSOR_DELAY_GAME);
        }
    }
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // nothing to do here
    }
    @Override
    public void onSensorChanged(SensorEvent event) {
        yaw = event.values[0];
        pitch = event.values[1];
        roll = event.values[2];
    }
    public float getYaw() {
        return yaw;
    }
    public float getPitch() {
        return pitch;
    }
    public float getRoll() {
        return roll;
    }
}

We won’t use the compass in any of the games in this book, but if you are going to reuse the framework we develop, this class might come in handy.

The Pool Class: Because Reuse Is Good for You!

What’s the worst thing that can happen to us as Android developers? World-stopping garbage collection! If you look at the Input interface definition in Chapter 3, you’ll find the getTouchEvents() and getKeyEvents() methods. These methods return TouchEvent and KeyEvent lists. In our keyboard and touch event handlers, we constantly create instances of these two classes and store them in lists that are internal to the handlers. The Android input system fires many of these events when a key is pressed or a finger touches the screen, so we constantly create new instances that are collected by the garbage collector in short intervals. In order to avoid this, we implement a concept known as instance pooling. Instead of repeatedly creating new instances of a class, we simply reuse previously created instances. The Pool class is a convenient way to implement that behavior. Let’s have a look at its code in Listing 5-7, which is broken up again, containing appropriate commentary.

Listing 5-7. Pool.java; Playing Well with the Garbage Collector

package com.badlogic.androidgames.framework;

import java.util.ArrayList;
import java.util.List;

public class Pool < T > {

Here are the generics: the first thing to recognize is that this is a generically typed class, much like collection classes such as ArrayList or HashMap. Generics allow us to store any type of object in our Pool without having to cast continuously. So what does the Pool class do?

    public interface PoolObjectFactory < T > {
        public T createObject();
    }

An interface called PoolObjectFactory is the first thing defined and is, once again, generic. It has a single method, createObject(), that will return a new object with the generic type of the Pool/PoolObjectFactory instance.

    private final List < T > freeObjects;
    private final PoolObjectFactory < T > factory;
    private final int maxSize;

The Pool class has three members. These include an ArrayList to store pooled objects, a PoolObjectFactory that is used to generate new instances of the type held by the class, and a member that stores the maximum number of objects the Pool can hold. The last bit is needed so our Pool does not grow indefinitely; otherwise, we might run into an out-of-memory exception.

    public Pool(PoolObjectFactory < T > factory, int maxSize) {
        this.factory = factory;
        this.maxSize = maxSize;
        this.freeObjects = new ArrayList < T > (maxSize);
    }

The constructor of the Pool class takes a PoolObjectFactory and the maximum number of objects it should store. We store both parameters in the respective members and instantiate a new ArrayList with the capacity set to the maximum number of objects.

    public T newObject() {
       T object = null ;
       if (freeObjects.isEmpty())
           object = factory.createObject();
       else
           object = freeObjects.remove(freeObjects.size() - 1);
       return object;
    }

The newObject() method is responsible for either handing us a brand-new instance of the type held by the Pool, via the PoolObjectFactory.newObject() method, or returning a pooled instance in case there’s one in the freeObjectsArrayList. If we use this method, we get recycled objects as long as the Pool has some stored in the freeObjects list. Otherwise, the method creates a new one via the factory.

    public void free(T object) {
        if (freeObjects.size() < maxSize)
            freeObjects.add(object);
    }
}

The free() method lets us reinsert objects that we no longer use. It simply inserts the object into the freeObjects list if it is not yet filled to capacity. If the list is full, the object is not added, and it is likely to be consumed by the garbage collector the next time it executes.

So, how can we use that class? We’ll look at some pseudocode usage of the Pool class in conjunction with touch events.

PoolObjectFactory <TouchEvent> factory = new PoolObjectFactory <TouchEvent> () {
    @Override
    public TouchEvent createObject() {
        return new TouchEvent();
    }
};
Pool <TouchEvent> touchEventPool = new Pool <TouchEvent> (factory, 50);
TouchEvent touchEvent = touchEventPool.newObject();
. . . do something here . . .
touchEventPool.free(touchEvent);

First, we define a PoolObjectFactory that creates TouchEvent instances. Next, we instantiate the Pool by telling it to use our factory and that it should maximally store 50 TouchEvents. When we want a new TouchEvent from the Pool, we call the Pool’s newObject() method. Initially, the Pool is empty, so it will ask the factory to create a brand-new TouchEvent instance. When we no longer need the TouchEvent, we reinsert it into the Pool by calling the Pool’s free() method. The next time we call the newObject() method, we get the same TouchEvent instance and recycle it to avoid problems with the garbage collector. This class is useful in a couple of places. Please note that you must be careful to fully reinitialize reused objects when they’re fetched from the Pool.

KeyboardHandler: Up, Up, Down, Down, Left, Right . . .

The KeyboardHandler must fulfill a couple of tasks. First, it must connect with the View from which keyboard events are to be received. Next, it must store the current state of each key for polling. It must also keep a list of KeyEvent instances that we designed in Chapter 3 for event-based input handling. Finally, it must properly synchronize everything since it will receive events on the UI thread while being polled from our main game loop, which is executed on a different thread. This is a lot of work! As a refresher, we’ll show you the KeyEvent class that we defined in Chapter 3 as part of the Input interface.

public static class KeyEvent {
    public static final int KEY_DOWN = 0;
    public static final int KEY_UP = 1;

    public int type;
    public int keyCode;
    public char keyChar;
}

This class simply defines two constants that encode the key event type along with three members while holding the type, key code, and Unicode character of the event. With this, we can implement our handler.

Listing 5-8 shows the implementation of the handler with the Android APIs discussed earlier and our new Pool class. The listing is broken up by commentary.

Listing 5-8. KeyboardHandler.java: Handling Keys Since 2010

package com.badlogic.androidgames.framework.impl;

import java.util.ArrayList;
import java.util.List;

import android.view.View;
import android.view.View.OnKeyListener;

import com.badlogic.androidgames.framework.Input.KeyEvent;
import com.badlogic.androidgames.framework.Pool;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;

public class KeyboardHandler implements OnKeyListener {
    boolean [] pressedKeys = new boolean [128];
    Pool <KeyEvent> keyEventPool;
    List <KeyEvent> keyEventsBuffer = new ArrayList <KeyEvent> ();
    List <KeyEvent> keyEvents = new ArrayList <KeyEvent> ();

The KeyboardHandler class implements the OnKeyListener interface so that it can receive key events from a View. The members are next.

The first member is an array holding 128 Booleans. We store the current state (pressed or not) of each key in this array. It is indexed by the key’s key code. Luckily for us, the android.view.KeyEvent.KEYCODE_XXX constants (which encode the key codes) are all between 0 and 127, so we can store them in a garbage collector–friendly form. Note that by an unlucky accident, our KeyEvent class shares its name with the Android KeyEvent class, of which instances get passed to our OnKeyEventListener.onKeyEvent() method. This slight confusion is only limited to this handler code. As there’s no better name for a key event than “KeyEvent,” we chose to live with this short-lived confusion.

The next member is a Pool that holds the instances of our KeyEvent class. We don’t want to make the garbage collector angry, so we recycle all the KeyEvent objects we create.

The third member stores the KeyEvent instances hat have not yet been consumed by our game. Each time we get a new key event on the UI thread, we add it to this list.

The last member stores the KeyEvents that we return by calling the KeyboardHandler.getKeyEvents(). In the following sections, we’ll see why we have to double-buffer the key events.

    public KeyboardHandler(View view) {
        PoolObjectFactory <KeyEvent> factory = new PoolObjectFactory <KeyEvent> () {
            public KeyEvent createObject() {
                return new KeyEvent();
            }
        };
        keyEventPool = new Pool < KeyEvent > (factory, 100);
        view.setOnKeyListener(this );
        view.setFocusableInTouchMode(true );
        view.requestFocus();
    }

The constructor has a single parameter consisting of the View from which we want to receive key events. We create the Pool instance with a proper PoolObjectFactory, register the handler as an OnKeyListener with the View, and, finally, make sure that the View will receive key events by making it the focused View.

    public boolean onKey(View v, int keyCode, android.view.KeyEvent event) {
        if (event.getAction() == android.view.KeyEvent.ACTION_MULTIPLE)
            return false ;

        synchronized (this ) {
            KeyEvent keyEvent = keyEventPool.newObject();
            keyEvent.keyCode = keyCode;
            keyEvent.keyChar = (char ) event.getUnicodeChar();
            if (event.getAction() == android.view.KeyEvent.ACTION_DOWN) {
                keyEvent.type = KeyEvent.KEY_DOWN;
                if (keyCode > 0 && keyCode < 127)
                    pressedKeys[keyCode] = true ;
            }
            if (event.getAction() == android.view.KeyEvent.ACTION_UP) {
                keyEvent.type = KeyEvent.KEY_UP;
                if (keyCode > 0 && keyCode < 127)
                    pressedKeys[keyCode] = false ;
            }
            keyEventsBuffer.add(keyEvent);
        }
        return false ;
    }

Next, we will discuss our implementation of the OnKeyListener.onKey() interface method, which is called each time the View receives a new key event. We start by ignoring any (Android) key events that encode a KeyEvent.ACTION_MULTIPLE event. These are not relevant in our context. This is followed by a synchronized block. Remember, the events are received on the UI thread and read on the main loop thread, so we have to make sure that none of our members are accessed in parallel.

Within the synchronized block, we first fetch a KeyEvent instance (of our KeyEvent implementation) from the Pool. This will either get us a recycled instance or a brand-new one, depending on the state of the Pool. Next, we set the KeyEvent’s keyCode and keyChar members based on the contents of the Android KeyEvent that were passed to the method. Then, we decode the Android KeyEvent type and set the type of our KeyEvent, as well as the element in the pressedKey array, accordingly. Finally, we add our KeyEvent to the previously defined keyEventBuffer list.

    public boolean isKeyPressed(int keyCode) {
        if (keyCode < 0 || keyCode > 127)
            return false ;
        return pressedKeys[keyCode];
    }

The next method of our handler is the isKeyPressed() method, which implements the semantics of Input.isKeyPressed(). First, we pass in an integer that specifies the key code (one of the Android KeyEvent.KEYCODE_XXX constants) and returns whether that key is pressed or not. We do this by looking up the state of the key in the pressedKey array after some range checking. Remember, we set the elements of this array in the previous method, which gets called on the UI thread. Since we are working with primitive types again, there’s no need for synchronization.

    public List <KeyEvent> getKeyEvents() {
        synchronized (this ) {
            int len = keyEvents.size();
            for (int i = 0; i < len; i++) {
                keyEventPool.free(keyEvents.get(i));
            }
            keyEvents.clear();
            keyEvents.addAll(keyEventsBuffer);
            keyEventsBuffer.clear();
            return keyEvents;
        }
    }
}

The last method of our handler is called getKeyEvents(), and it implements the semantics of the Input.getKeyEvents() method. Once again, we start with a synchronized block and remember that this method will be called from a different thread.

Next, we loop through the keyEvents array and insert all of its KeyEvents into our Pool. Remember, we fetch instances from the Pool in the onKey() method on the UI thread. Here, we reinsert them into the Pool. But isn’t the keyEvents list empty? Yes, but only the first time we invoke that method. To understand why, you have to grasp the rest of the method.

After our mysterious Pool insertion loop, we clear the keyEvents list and fill it with the events in our keyEventsBuffer list. Finally, we clear the keyEventsBuffer list and return the newly filled keyEvents list to the caller. What is happening here?

We’ll use a simple example to illustrate this. First, we’ll examine what happens to the keyEvents and the keyEventsBuffer lists, as well as to our Pool, each time a new event arrives on the UI thread or the game fetches the events in the main thread:

UI thread: onKey() ->
           keyEvents = { }, keyEventsBuffer = {KeyEvent1}, pool = { }
Main thread: getKeyEvents() ->
           keyEvents = {KeyEvent1}, keyEventsBuffer = { }, pool { }
UI thread: onKey() ->
           keyEvents = {KeyEvent1}, keyEventsBuffer = {KeyEvent2}, pool { }
Main thread: getKeyEvents() ->
           keyEvents = {KeyEvent2}, keyEventsBuffer = { }, pool = {KeyEvent1}
UI thread: onKey() ->

           keyEvents = {KeyEvent2}, keyEventsBuffer = {KeyEvent1}, pool = { }

  1. We get a new event in the UI thread. There’s nothing in the Pool yet, so a new KeyEvent instance (KeyEvent1) is created and inserted into the keyEventsBuffer list.
  2. We call getKeyEvents() on the main thread. getKeyEvents() takes KeyEvent1 from the keyEventsBuffer list and puts it into the keyEvents list that is returns to the caller.
  3. We get another event on the UI thread. We still have nothing in the Pool, so a new KeyEvent instance (KeyEvent2) is created and inserted into the keyEventsBuffer list.
  4. The main thread calls getKeyEvents() again. Now, something interesting happens. Upon entry into the method, the keyEvents list still holds KeyEvent1. The insertion loop will place that event into our Pool. It then clears the keyEvents list and inserts any KeyEvent into the keyEventsBuffer, in this case, KeyEvent2. We just recycled a key event.
  5. Another key event arrives on the UI thread. This time, we have a free KeyEvent in our Pool, which we happily reuse. Incredibly, there’s no garbage collection!

This mechanism comes with one caveat, which is that we have to call KeyboardHandler.getKeyEvents() frequently or the keyEvents list fills up quickly, and no objects are returned to the Pool. Problems can be avoided as long as we remember this.

Touch Handlers

Now it is time to consider fragmentation. In Chapter 4, we revealed that multitouch is only supported on Android versions greater than 1.6. All the nice constants we used in our multitouch code (for example, MotionEvent.ACTION_POINTER_ID_MASK) are not available to us on Android 1.5 or 1.6. We can use them in our code if we set the build target of our project to an Android version that has this API; however, the application will crash on any device running Android 1.5 or 1.6. We want our games to run on all currently available Android versions, so how do we solve this problem?

We employ a simple trick. We write two handlers, one using the single-touch API in Android 1.5, and another using the multitouch API in Android 2.0 and above. This is safe as long as we don’t execute the multitouch handler code on an Android device lower than version 2.0. The VM won’t load the code, and it won’t throw exceptions continuously. All we need to do is find out which Android version the device is running and instantiate the proper handler. You’ll see how this works when we discuss the AndroidInput class. For now, let’s concentrate on the two handlers.

The TouchHandler Interface

In order to use our two handler classes interchangeably, we need to define a common interface. Listing 5-9 presents the TouchHandler interface.

Listing 5-9. TouchHandler.java, to Be Implemented for Android 1.5 and 1.6

package com.badlogic.androidgames.framework.impl;

import java.util.List;

import android.view.View.OnTouchListener;

import com.badlogic.androidgames.framework.Input.TouchEvent;

public interface TouchHandlerextends OnTouchListener {
    public boolean isTouchDown(int pointer);

    public int getTouchX(int pointer);

    public int getTouchY(int pointer);

    public List <TouchEvent> getTouchEvents();

}

All TouchHandlers must implement the OnTouchListener interface, which is used to register the handler with a View. The methods of the interface correspond to the respective methods of the Input interface defined in Chapter 3. The first three are for polling the state of a specific pointer ID, and the last is for getting TouchEvents with which to perform event-based input handling. Note that the polling methods take pointer IDs that can be any number and are given by the touch event.

The SingleTouchHandler Class

In the case of our single-touch handler, we ignore any IDs other than zero. To recap, we’ll recall the TouchEvent class defined in Chapter 3 as part of the Input interface.

public static class TouchEvent {
    public static final int TOUCH_DOWN = 0;
    public static final int TOUCH_UP = 1;
    public static final int TOUCH_DRAGGED = 2;

    public int type;
    public int x, y;
    public int pointer;
}

Like the KeyEvent class, the TouchEvent class defines a couple of constants that echo the touch event’s type, along with the x and y coordinates in the coordinate system of the View and the pointer ID. Listing 5-10 shows the implementation of the TouchHandler interface for Android 1.5 and 1.6, broken up by commentary.

Listing 5-10. SingleTouchHandler.java; Good with Single Touch, Not So Good with Multitouch

package com.badlogic.androidgames.framework.impl;

import java.util.ArrayList;
import java.util.List;

import android.view.MotionEvent;
import android.view.View;
import com.badlogic.androidgames.framework.Pool;

import com.badlogic.androidgames.framework.Input.TouchEvent;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;

public class SingleTouchHandler implements TouchHandler {
    boolean isTouched;
    int touchX;
    int touchY;
    Pool <TouchEvent> touchEventPool;
    List <TouchEvent> touchEvents = new ArrayList <TouchEvent> ();
    List <TouchEvent> touchEventsBuffer = new ArrayList <TouchEvent> ();
    float scaleX;
    float scaleY;

We start by letting the class implement the TouchHandler interface, which also means that we must implement the OnTouchListener interface. Next, we have three members that store the current state of the touchscreen for one finger, followed by a Pool and two lists that hold the TouchEvents. This is the same as in the KeyboardHandler. We also have two members, scaleX and scaleY. We’ll address these in the following sections and use them to cope with different screen resolutions.

Note  Of course, we could make this more elegant by deriving the KeyboardHandler and SingleTouchHandler from a base class that handles all matters regarding pooling and synchronization. However, it would have complicated the explanation even more, so instead, we’ll write a few more lines of code.

    public SingleTouchHandler(View view, float scaleX, float scaleY) {
       PoolObjectFactory <TouchEvent> factory = new PoolObjectFactory <TouchEvent> () {
            @Override
            public TouchEvent createObject() {
                return new TouchEvent();
            }
        };
        touchEventPool = new Pool <TouchEvent> (factory, 100);
        view.setOnTouchListener(this );
        this.scaleX = scaleX;
        this.scaleY = scaleY;
    }

In the constructor, we register the handler as an OnTouchListener and set up the Pool that we use to recycle TouchEvents. We also store the scaleX and scaleY parameters that are passed to the constructor (ignore them for now).

    public boolean onTouch(View v, MotionEvent event) {
        synchronized (this ) {
            TouchEvent touchEvent = touchEventPool.newObject();
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchEvent.type = TouchEvent.TOUCH_DOWN;
                isTouched = true ;
                break ;
            case MotionEvent.ACTION_MOVE:
                touchEvent.type = TouchEvent.TOUCH_DRAGGED;
                isTouched = true ;
                break ;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                touchEvent.type = TouchEvent.TOUCH_UP;
                isTouched = false ;
                break ;
            }
            touchEvent.x = touchX = (int )(event.getX() * scaleX);
            touchEvent.y = touchY = (int )(event.getY() * scaleY);
            touchEventsBuffer.add(touchEvent);
            return true ;
        }
    }

The onTouch() method achieves the same outcome as our KeyboardHandler’s onKey() method; the only difference is that now we handle TouchEvents instead of KeyEvents. All the synchronization, pooling, and MotionEvent handling are already known to us. The only interesting thing is that we multiply the reported x and y coordinates of a touch event by scaleX and scaleY. This is important to remember because we’ll return to it in the following sections.

    public boolean isTouchDown(int pointer) {
        synchronized (this ) {
            if (pointer == 0)
                return isTouched;
            else
                return false ;
        }
    }
    public int getTouchX(int pointer) {
        synchronized (this ) {
            return touchX;
        }
    }
    public int getTouchY(int pointer) {
        synchronized (this ) {
            return touchY;
        }
    }

The methods isTouchDown(), getTouchX(), and getTouchY() allow us to poll the state of the touchscreen based on the members that we set in the onTouch() method. The only noticeable thing about them is that they only return useful data for a pointer ID with a value of zero, since this class only supports single-touch screens.

    public List <TouchEvent> getTouchEvents() {
        synchronized (this ) {
            int len = touchEvents.size();
            for (int i = 0; i < len; i++ )
                touchEventPool.free(touchEvents.get(i));
            touchEvents.clear();
            touchEvents.addAll(touchEventsBuffer);
            touchEventsBuffer.clear();
            return touchEvents;
        }
    }
}

The final method, SingleTouchHandler.getTouchEvents(), should be familiar to you, and is similar to the KeyboardHandler.getKeyEvents() methods. Remember that we call this method frequently so that the touchEvents list doesn’t fill up.

The MultiTouchHandler

For multitouch handling, we use a class called MultiTouchHandler, as shown in Listing 5-11.

Listing 5-11. MultiTouchHandler.java (More of the Same)

package com.badlogic.androidgames.framework.impl;

import java.util.ArrayList;
import java.util.List;

import android.view.MotionEvent;
import android.view.View;

import com.badlogic.androidgames.framework.Input.TouchEvent;
import com.badlogic.androidgames.framework.Pool;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;

@TargetApi(5)
public class MultiTouchHandler implements TouchHandler {
    private static final int MAX_TOUCHPOINTS = 10;
    boolean [] isTouched = new boolean [MAX_TOUCHPOINTS];
    int [] touchX = new int [MAX_TOUCHPOINTS];
    int [] touchY = new int [MAX_TOUCHPOINTS];
    int [] id = new int [MAX_TOUCHPOINTS];
    Pool <TouchEvent> touchEventPool;
    List <TouchEvent> touchEvents = new ArrayList <TouchEvent> ();
    List <TouchEvent> touchEventsBuffer = new ArrayList <TouchEvent> ();
    float scaleX;
    float scaleY;

    public MultiTouchHandler(View view, float scaleX, float scaleY) {
        PoolObjectFactory <TouchEvent> factory = new PoolObjectFactory <TouchEvent> () {
            public TouchEvent createObject() {
                return new TouchEvent();
            }
        };
        touchEventPool = new Pool <TouchEvent> (factory, 100);
        view.setOnTouchListener(this );
        this.scaleX = scaleX;
        this.scaleY = scaleY;
    }
    public boolean onTouch(View v, MotionEvent event) {
        synchronized (this ) {
            int action = event.getAction() & MotionEvent.ACTION_MASK;
            int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) > > MotionEvent.ACTION_POINTER_ID_SHIFT;
            int pointerCount = event.getPointerCount();
            TouchEvent touchEvent;
            for (int i = 0; i < MAX_TOUCHPOINTS; i++) {
                if (i >= pointerCount) {
                    isTouched[i] = false ;
                    id[i] = -1;
                    continue ;
                }
                int pointerId = event.getPointerId(i);
                if (event.getAction() != MotionEvent.ACTION_MOVE&& i != pointerIndex) {
                    // if it's an up/down/cancel/out event, mask the id to see if we should process it for this touch
                    // point
                    continue ;
                }
                switch (action) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_POINTER_DOWN:
                    touchEvent = touchEventPool.newObject();
                    touchEvent.type = TouchEvent.TOUCH_DOWN;
                    touchEvent.pointer = pointerId;
                    touchEvent.x = touchX[i] = (int ) (event.getX(i) * scaleX);
                    touchEvent.y = touchY[i] = (int ) (event.getY(i) * scaleY);
                    isTouched[i] = true ;
                    id[i] = pointerId;
                    touchEventsBuffer.add(touchEvent);
                    break ;

                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_POINTER_UP:
                case MotionEvent.ACTION_CANCEL:
                    touchEvent = touchEventPool.newObject();
                    touchEvent.type = TouchEvent.TOUCH_UP;
                    touchEvent.pointer = pointerId;
                    touchEvent.x = touchX[i] = (int ) (event.getX(i) * scaleX);
                    touchEvent.y = touchY[i] = (int ) (event.getY(i) * scaleY);
                    isTouched[i] = false ;
                    id[i] = -1;
                    touchEventsBuffer.add(touchEvent);
                    break ;

                case MotionEvent.ACTION_MOVE:
                    touchEvent = touchEventPool.newObject();
                    touchEvent.type = TouchEvent.TOUCH_DRAGGED;
                    touchEvent.pointer = pointerId;
                    touchEvent.x = touchX[i] = (int ) (event.getX(i) * scaleX);
                    touchEvent.y = touchY[i] = (int ) (event.getY(i) * scaleY);
                    isTouched[i] = true ;
                    id[i] = pointerId;
                    touchEventsBuffer.add(touchEvent);
                    break ;
                }
            }
            return true ;
        }
    }
    public boolean isTouchDown(int pointer) {
        synchronized (this ) {
            int index = getIndex(pointer);
            if (index < 0 || index >=MAX_TOUCHPOINTS)
                return false ;
            else
                return isTouched[index];
        }
    }
    public int getTouchX(int pointer) {
        synchronized (this ) {
            int index = getIndex(pointer);
            if (index < 0 || index >=MAX_TOUCHPOINTS)
                return 0;
            else
                return touchX[index];
        }
    }
    public int getTouchY(int pointer) {
        synchronized (this ) {
            int index = getIndex(pointer);
            if (index < 0 || index >=MAX_TOUCHPOINTS)
                return 0;
            else
                return touchY[index];
        }
    }
    public List <TouchEvent> getTouchEvents() {
        synchronized (this ) {
            int len = touchEvents.size();
            for (int i = 0; i < len; i++)
                touchEventPool.free(touchEvents.get(i));
            touchEvents.clear();
            touchEvents.addAll(touchEventsBuffer);
            touchEventsBuffer.clear();
            return touchEvents;
        }
    }
    // returns the index for a given pointerId or −1 if no index.
    private int getIndex(int pointerId) {
        for (int i = 0; i < MAX_TOUCHPOINTS; i++) {
            if (id[i] == pointerId) {
                return i;
            }
        }
        return -1;
    }
}

We start off with another TargetApi annotation to tell the compiler that we know what we are doing. In this case, we have set the minimum API level to 3, but the code in the multitouch-handler requires API level 5. The compiler would complain without this annotation.

The onTouch() method looks as intimidating as our test example in Chapter 4. However, all we need to do is marry that test code with our event pooling and synchronization, which we’ve already talked about in detail. The only real difference from the SingleTouchHandler.onTouch() method is that we handle multiple pointers and set the TouchEvent.pointer member accordingly (instead of using a value of zero).

The polling methods, isTouchDown(), getTouchX(), and getTouchY(), should look familiar as well. We perform some error checking and then fetch the corresponding pointer state for the corresponding pointer index from one of the member arrays that we fill in the onTouch() method.

The final public method, getTouchEvents(), is exactly the same as the corresponding method in SingleTouchHandler.getTouchEvents(). Now that we are equipped with all these handlers, we can implement the Input interface.

The last method in the class is a helper method that we use to find the index to a pointer ID.

AndroidInput: The Great Coordinator

The Input implementation of our game framework ties together all the handlers we have developed. Any method calls are delegated to the corresponding handler. The only interesting part of this implementation is choosing which TouchHandler implementation to use, based on the Android version the device is running. Listing 5-12 shows an implementation called AndroidInput, with commentary.

Listing 5-12. AndroidInput.java; Handling the Handlers with Style

package com.badlogic.androidgames.framework.impl;

import java.util.List;

import android.content.Context;
import android.os.Build.VERSION;
import android.view.View;

import com.badlogic.androidgames.framework.Input;

public class AndroidInput implements Input {
    AccelerometerHandler accelHandler;
    KeyboardHandler keyHandler;
    TouchHandler touchHandler;

We start by letting the class implement the Input interface defined in Chapter 3. This leads us to three members: an AccelerometerHandler, a KeyboardHandler, and a TouchHandler.

    public AndroidInput(Context context, View view, float scaleX, float scaleY) {
        accelHandler = new AccelerometerHandler(context);
        keyHandler = new KeyboardHandler(view);
        if (Integer.parseInt(VERSION.SDK) < 5)
            touchHandler = new SingleTouchHandler(view, scaleX, scaleY);
        else
            touchHandler = new MultiTouchHandler(view, scaleX, scaleY);
    }

These members are initialized in the constructor, which takes a Context, a View, and the scaleX and scaleY parameters, which we can ignore again. The AccelerometerHandler is instantiated via the Context parameter, as the KeyboardHandler needs the View that is passed in.

To decide which TouchHandler to use, we simply check the Android version that the application uses to run. This can be done using the VERSION.SDK string, which is a constant provided by the Android API. It is unclear why this is a string, since it directly encodes the SDK version numbers we use in our manifest file. Therefore, we need to make it into an integer in order to do some comparisons. The first Android version to support the multitouch API was version 2.0, which corresponds to SDK version 5. If the current device runs a lower Android version, we instantiate the SingleTouchHandler; otherwise, we use the MultiTouchHandler. At an API level, this is all the fragmentation we need to care about. When we start rendering OpenGL, we’ll hit a few more fragmentation issues, but there is no need to worry—they are easily resolved, just like the touch API problems.

    public boolean isKeyPressed(int keyCode) {
        return keyHandler.isKeyPressed(keyCode);
    }
    public boolean isTouchDown(int pointer) {
        return touchHandler.isTouchDown(pointer);
    }
    public int getTouchX(int pointer) {
        return touchHandler.getTouchX(pointer);
    }
    public int getTouchY(int pointer) {
        return touchHandler.getTouchY(pointer);
    }
    public float getAccelX() {
        return accelHandler.getAccelX();
    }
    public float getAccelY() {
        return accelHandler.getAccelY();
    }
    public float getAccelZ() {
        return accelHandler.getAccelZ();
    }
    public List <TouchEvent> getTouchEvents() {
        return touchHandler.getTouchEvents();
    }
    public List <KeyEvent> getKeyEvents() {
        return keyHandler.getKeyEvents();
    }
}

The rest of this class is self-explanatory. Each method call is delegated to the appropriate handler, which does the actual work. With this, we have finished the input API of our game framework. Next, we’ll discuss graphics.

AndroidGraphics and AndroidPixmap: Double Rainbow

It’s time to get back to our most beloved topic, graphics programming. In Chapter 3, we defined two interfaces called Graphics and Pixmap. Now, we’re going to implement them based on what you learned in Chapter 4. However, there’s one thing we have yet to consider: how to handle different screen sizes and resolutions.

Handling Different Screen Sizes and Resolutions

Android has supported different screen resolutions since version 1.6. It can handle resolutions ranging from 240×320 pixels to a full HDTV resolution of 1920×1080. In Chapter 4, we discussed the effect of different screen resolutions and physical screen sizes. For instance, drawing with absolute coordinates and sizes given in pixels will produce unexpected results. Figure 5-1 shows what happens when we render a 100×100-pixel rectangle with the upper-left corner at (219,379) on 480×800 and 320×480 screens.

9781430246770_Fig05-01.jpg

Figure 5-1.  A 100×100-pixel rectangle drawn at (219,379) on a 480×800 screen (left) and a 320×480 screen (right)

This difference is problematic for two reasons. First, we can’t draw our game and assume a fixed resolution. The second reason is more subtle: in Figure 5-1, we assumed that both screens have the same density (that is, each pixel has the same physical size on both devices), but this is rarely the case in reality.

Density

Density is usually specified in pixels per inch or pixels per centimeter (sometimes you’ll hear about dots per inch, which is not technically correct). The Nexus One has a 480×800-pixel screen with a physical size of 8×4.8 centimeters. The older HTC Hero has a 320×480-pixel screen with a physical size of 6.5×4.5 centimeters. That’s 100 pixels per centimeter on both axes on the Nexus One, and roughly 71 pixels per centimeter on both axes on the Hero. We can easily calculate the pixels per centimeter using the following equation:

pixels per centimeter (on x-axis) = width in pixels / width in centimeters

Or:

pixels per centimeter (on y-axis) = height in pixels / height in centimeters

Usually, we only need to calculate this on a single axis since the physical pixels are square (they’re actually three pixels, but we’ll ignore that here).

How big would a 100×100-pixel rectangle be in centimeters? On the Nexus One, we have a 1×1-centimeter rectangle, while the Hero has a 1.4×1.4-centimeter rectangle. This is something we need to account for if, for example, we are trying to provide buttons that are big enough for the average thumb on all screen sizes. This example implies that this is a major issue that could present huge problems; however, it usually doesn’t. We need to make sure that our buttons are a decent size on high-density screens (for example, the Nexus One) since they will automatically be big enough on low-density screens.

Aspect Ratio

Aspect ratio is another problem to consider. The aspect ratio of a screen is the ratio between the width and height, in either pixels or centimeters. We can calculate aspect ratio using the following equation:

pixel aspect ratio = width in pixels / height in pixels

Or:

physical aspect ratio = width in centimeters / height in centimeters

Here, width and height usually mean the width and height in landscape mode. The Nexus One has a pixel and physical aspect ratio of ∼1.66. The Hero has a pixel and physical aspect ratio of 1.5. What does this mean? On the Nexus One, we have more pixels available on the x axis in landscape mode relative to height than we have available on the Hero. Figure 5-2 illustrates this with screenshots from Replica Island on both devices.

Note  This book uses the metric system. We know this might be an inconvenience if you are familiar with inches and pounds. However, as we will be considering some physics problems in the following chapters, it’s best to get used to it now since physics problems are usually defined in the metric system. Remember that 1 inch is roughly 2.54 centimeters.

9781430246770_Fig05-02.jpg

Figure 5-2.  Replica Island on the Nexus One (top) and the HTC Hero (bottom)

The Nexus One displays a bit more on the x axis. However, everything is identical on the y axis. What did the creator of Replica Island do in this case?

Coping with Different Aspect Ratios

Replica Island serves as a very useful example of the aspect ratio problem. The game was originally designed to fit on a 480×320-pixel screen, including all “the sprites,” such as the robot and the doctor, the tiles of “the world,” and the UI elements (the buttons at the bottom left and the status info at the top of the screen). When the game is rendered on a Hero, each pixel in the sprite bitmaps maps to exactly one pixel on the screen. On a Nexus One, everything is scaled up while rendering, so one pixel of a sprite actually takes up 1.5 pixels on the screen. In other words, a 32×32-pixel sprite will be 48×48 pixels on the screen. This scaling factor is easily calculated using the following equations:

scaling factor (on x-axis) = screen width in pixels / target width in pixels

scaling factor (on y-axis) = screen height in pixels / target height in pixels

The target width and height are equal to the screen resolution for which the graphical assets were designed; in Replica Island, the dimensions are 480×320 pixels. For the Nexus One, there is a scaling factor of 1.66 on the x axis and a scaling factor of 1.5 on the y axis. Why are the scaling factors on the two axes different?

This is due to the fact that two screen resolutions have different aspect ratios. If we simply stretch a 480×320-pixel image to an 800×480-pixel image, the original image is stretched on the x axis. For most games, this will be insignificant, so we can simply draw our graphical assets for a specific target resolution and stretch them to the actual screen resolution while rendering (remember the Bitmap.drawBitmap() method).

However, for some games, you might want to use a more complicated method. Figure 5-3 shows Replica Island scaled up from 480×320 to 800×480 pixels and overlaid with a faint image of how it actually looks.

9781430246770_Fig05-03.jpg

Figure 5-3.  Replica Island stretched from 480×320 to 800×480 pixels, overlaid with a faint image of how it is rendered on an 800×480-pixel display

Replica Island performs normal stretching on the y axis using the scaling factor we just calculated (1.5), but instead of using the x-axis scaling factor (1.66), which would squish the image, it uses the y-axis scaling factor. This trick allows all objects on the screen to keep their aspect ratio. A 32×32-pixel sprite becomes 48×48 pixels instead of 53×48 pixels. However, this also means that our coordinate system is no longer bounded between (0,0) and (479,319); instead, it ranges from (0,0) to (533,319). This is why we see more of Replica Island on a Nexus One than on an HTC Hero.

Note, however, that using this fancy method might be inappropriate for some games. For example, if the world size depends on the screen aspect ratio, players with wider screens could have an unfair advantage. This would be the case for a game like StarCraft 2. Finally, if you want the entire game to fit onto a single screen, like in Mr. Nom, it is better to use the simpler stretching method; if we use the second version, there will be blank space left over on wider screens.

A Simpler Solution

One advantage of Replica Island is that it does all this stretching and scaling via OpenGL ES, which is hardware accelerated. So far, we’ve only discussed how to draw to a Bitmap and a View via the Canvas class, which, on older Android versions, involves slow number-crunching on the CPU and doesn’t involve hardware acceleration on the GPU.

With this in mind, we perform a simple trick by creating a framebuffer in the form of a Bitmap instance with our target resolution. This way, we don’t have to worry about the actual screen resolution when we design our graphical assets or render them via code. Instead, we pretend that the screen resolution is the same on all devices, and all our draw calls target this “virtual” framebuffer Bitmap via a Canvas instance. When we’re done rendering a frame, we simply draw this framebuffer Bitmap to our SurfaceView via a call to the Canvas.drawBitmap() method, which allows us to draw a stretched Bitmap.

If we want to use the same technique as Replica Island, we need to adjust the size of our framebuffer on the bigger axis (that is, on the x axis in landscape mode and on the y axis in portrait mode). We also have to make sure to fill the extra pixels to avoid blank space.

The Implementation

Let’s summarize everything in a work plan:

  • We design all our graphic assets for a fixed target resolution (320×480 in the case of Mr. Nom).
  • We create a Bitmap that is the same size as our target resolution and direct all our drawing calls to it, effectively working in a fixed-coordinate system.
  • When we are done drawing a frame, we draw our framebuffer Bitmap that is stretched to the SurfaceView. On devices with a lower screen resolution, the image is scaled down; on devices with a higher resolution, it is scaled up.
  • When we do our scaling trick, we make sure that all the UI elements with which the user interacts are big enough for all screen densities. We can do this in the graphic asset–design phase using the sizes of actual devices in combination with the previously mentioned formulas.

Now that we know how to handle different screen resolutions and densities, we can explain the scaleX and scaleY variables we encountered when we implemented the SingleTouchHandler and MultiTouchHandler in the previous sections.

All of our game code will work with our fixed target resolution (320×480 pixels). If we receive touch events on a device that has a higher or lower resolution, the x and y coordinates of those events will be defined in the View’s coordinate system, but not in our target resolution coordinate system. Therefore, it is necessary to transform the coordinates from their original system to our system, which is based on the scaling factors. To do this, we use the following equations:

transformed touch x = real touch x * (target pixels on x axis / real pixels on x axis)

transformed touch y = real touch y * (target pixels on y axis / real pixels on y axis)

Let’s calculate a simple example for a target resolution of 320×480 pixels and a device with a resolution of 480×800 pixels. If we touch the middle of the screen, we receive an event with the coordinates (240,400). Using the two preceding formulas, we arrive at the following equations, which are exactly in the middle of our target coordinate system:

transformed touch x = 240 * (320 / 480) = 160

transformed touch y = 400 * (480 / 800) = 240

Let’s do another one, assuming a real resolution of 240×320, again touching the middle of the screen, at (120,160):

transformed touch x = 120 * (320 / 240) = 160

transformed touch y = 160 * (480 / 320) = 240

This works in both directions. If we multiply the real touch event coordinates by the target factor divided by the real factor, we don’t have to worry about transforming our actual game code. All the touch coordinates will be expressed in our fixed-target coordinate system.

With that issue out of our way, we can implement the last few classes of our game framework.

AndroidPixmap: Pixels for the People

According to the design of our Pixmap interface from Chapter 3, there’s not much to implement. Listing 5-13 presents the code.

Listing 5-13. AndroidPixmap.java, a Pixmap Implementation Wrapping a Bitmap

package com.badlogic.androidgames.framework.impl;

import android.graphics.Bitmap;

import com.badlogic.androidgames.framework.Graphics.PixmapFormat;
import com.badlogic.androidgames.framework.Pixmap;

public class AndroidPixmapimplements Pixmap {
    Bitmap bitmap;
    PixmapFormat format;

    public AndroidPixmap(Bitmap bitmap, PixmapFormat format) {
        this.bitmap = bitmap;
        this.format = format;
    }
    public int getWidth() {
        return bitmap.getWidth();
    }
    public int getHeight() {
        return bitmap.getHeight();
    }
    public PixmapFormat getFormat() {
        return format;
    }
    public void dispose() {
        bitmap.recycle();
    }
}

All we need to do is store the Bitmap instance that we wrap, along with its format, which is stored as a PixmapFormat enumeration value, as defined in Chapter 3. Additionally, we implement the required methods of the Pixmap interface so that we can query the width and height of the Pixmap, as well as its format, and ensure that the pixels can be dumped from RAM. Note that the Bitmap member is package private, so we can access it in AndroidGraphics, which we’ll implement now.

AndroidGraphics: Serving Our Drawing Needs

The Graphics interface we designed in Chapter 3 is also lean and mean. It will draw pixels, lines, rectangles, and Pixmaps to the framebuffer. As discussed, we’ll use a Bitmap as our framebuffer and direct all drawing calls to it via a Canvas. It is also responsible for creating Pixmap instances from asset files. Therefore, we’ll also need another AssetManager. Listing 5-14 shows the code for our implementation of the interface, AndroidGraphics, with commentary.

Listing 5-14. AndroidGraphics.java; Implementing the Graphics Interface

package com.badlogic.androidgames.framework.impl;

import java.io.IOException;
import java.io.InputStream;

import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;

import com.badlogic.androidgames.framework.Graphics;
import com.badlogic.androidgames.framework.Pixmap;

public class AndroidGraphics implements Graphics {
    AssetManager assets;
    Bitmap frameBuffer;
    Canvas canvas;
    Paint paint;
    Rect srcRect = new Rect();
    Rect dstRect = new Rect();

The class implements the Graphics interface. It contains an AssetManager member that we use to load Bitmap instances, a Bitmap member that represents our artificial framebuffer, a Canvas member that we use to draw to the artificial framebuffer, a Paint member we need for drawing, and two Rect members we need for implementing the AndroidGraphics.drawPixmap() methods. These last three members are there so we don’t have to create new instances of these classes on every draw call. That would create a number of problems for the garbage collector.

    public AndroidGraphics(AssetManager assets, Bitmap frameBuffer) {
        this.assets = assets;
        this.frameBuffer = frameBuffer;
        this.canvas = new Canvas(frameBuffer);
        this.paint = new Paint();
    }

In the constructor, we get an AssetManager and Bitmap that represent our artificial framebuffer from the outside. We store these in the respective members and create the Canvas instance that will draw the artificial framebuffer, as well as the Paint, which we use for some of the drawing methods.

    public Pixmap newPixmap(String fileName, PixmapFormat format) {
        Config config = null ;
        if (format == PixmapFormat.RGB565)
            config = Config.RGB_565;
        else if (format == PixmapFormat.ARGB4444)
            config = Config.ARGB_4444;
        else
            config = Config.ARGB_8888;

        Options options = new Options();
        options.inPreferredConfig = config;

        InputStream in = null ;
        Bitmap bitmap = null ;
        try {
            in = assets.open(fileName);
            bitmap = BitmapFactory.decodeStream(in);
            if (bitmap ==null )
                throw new RuntimeException("Couldn't load bitmap from asset '"
                        + fileName + "'");
        }catch (IOException e) {
            throw new RuntimeException("Couldn't load bitmap from asset '"
                    + fileName + "'");
        }finally {
            if (in != null ) {
                try {
                    in.close();
                }catch (IOException e) {
                }
            }
        }
        if (bitmap.getConfig() == Config.RGB_565)
            format = PixmapFormat.RGB565;
        else if (bitmap.getConfig() == Config.ARGB_4444)
            format = PixmapFormat.ARGB4444;
        else
            format = PixmapFormat.ARGB8888;

        return new AndroidPixmap(bitmap, format);
    }

The newPixmap() method tries to load a Bitmap from an asset file, using the specified PixmapFormat. We start by translating the PixmapFormat into one of the constants of the Android Config class used in Chapter 4. Next, we create a new Options instance and set our preferred color format. Then, we try to load the Bitmap from the asset via the BitmapFactory, and throw a RuntimeException if something goes wrong. Otherwise, we check what format the BitmapFactory used to load the Bitmap and translate it into a PixmapFormat enumeration value. Remember that the BitmapFactory might decide to ignore our desired color format, so we have to check to determine what it used to decode the image. Finally, we construct a new AndroidBitmap instance based on the Bitmap we loaded, as well as its PixmapFormat, and return it to the caller.

    public void clear(int color) {
        canvas.drawRGB((color & 0xff0000) >> 16, (color & 0xff00) >> 8,
                (color & 0xff));
    }

The clear() method extracts the red, green, and blue components of the specified 32-bit ARGB color parameter and calls the Canvas.drawRGB() method, which clears our artificial framebuffer with that color. This method ignores any alpha value of the specified color, so we don’t have to extract it.

    public void drawPixel(int x, int y, int color) {
        paint.setColor(color);
        canvas.drawPoint(x, y, paint);
    }

The drawPixel() method draws a pixel of our artificial framebuffer via the Canvas.drawPoint() method. First, we set the color of our Paint member variable and pass it to the drawing method in addition to the x and y coordinates of the pixel.

    public void drawLine(int x, int y, int x2, int y2, int color) {
        paint.setColor(color);
        canvas.drawLine(x, y, x2, y2, paint);
    }

The drawLine() method draws the given line of the artificial framebuffer, using the Paint member to specify the color when calling the Canvas.drawLine() method.

    public void drawRect(int x, int y, int width, int height, int color) {
        paint.setColor(color);
        paint.setStyle(Style.FILL);
        canvas.drawRect(x, y, x + width - 1, y + width - 1, paint);
    }

The drawRect() method sets the Paint member’s color and style attributes so that we can draw a filled, colored rectangle. In the actual Canvas.drawRect() call, we have to transform the x, y, width, and height parameters of the coordinates in the top-left and bottom-right corners of the rectangle. For the top-left corner, we simply use the x and y parameters. For the bottom-right corner, we add the width and height to x and y and subtract 1. For example, if we render a rectangle with an x and y of (10,10) and a width and height of 2 and 2 and we don’t subtract 1, the resulting rectangle on the screen will be 3×3 pixels in size.

    public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY,
            int srcWidth, int srcHeight) {
        srcRect.left = srcX;
        srcRect.top = srcY;
        srcRect.right = srcX + srcWidth - 1;
        srcRect.bottom = srcY + srcHeight - 1;

        dstRect.left = x;
        dstRect.top = y;
        dstRect.right = x + srcWidth - 1;
        dstRect.bottom = y + srcHeight - 1;

        canvas.drawBitmap(((AndroidPixmap) pixmap).bitmap, srcRect, dstRect, null );
    }

The drawPixmap() method, which allows us to draw a portion of a Pixmap, sets up the source and destination of the Rect members that are used in the actual drawing call. As with drawing a rectangle, we have to translate the x and y coordinates together with the width and height to the top-left and bottom-right corners. Again, we have to subtract 1, or else we will overshoot by 1 pixel. Next, we perform the actual drawing via the Canvas.drawBitmap() method, which will automatically do the blending if the Pixmap we draw has a PixmapFormat.ARGB4444 or a PixmapFormat.ARGB8888 color depth. Note that we have to cast the Pixmap parameter to an AndroidPixmap in order to fetch the bitmap member for drawing with the Canvas. That’s a bit complicated, but we can be sure that the Pixmap instance that is passed in will be an AndroidPixmap.

    public void drawPixmap(Pixmap pixmap, int x, int y) {
        canvas.drawBitmap(((AndroidPixmap)pixmap).bitmap, x, y, null );
    }

The second drawPixmap() method draws the complete Pixmap to the artificial framebuffer at the given coordinates. Again, we must do some casting to get to the Bitmap member of the AndroidPixmap.

    public int getWidth() {
        return frameBuffer.getWidth();
    }
    public int getHeight() {
        return frameBuffer.getHeight();
    }
}

Finally, we have the methods getWidth() and getHeight(), which simply return the size of the artificial framebuffer stored by the AndroidGraphics class to which it renders internally.

AndroidFastRenderView is the last class we need in order to implement.

AndroidFastRenderView: Loop, Stretch, Loop, Stretch

The name of this class should give away what lies ahead. In Chapter 4, we discussed using a SurfaceView to perform continuous rendering in a separate thread that could also house our game’s main loop. We developed a very simple class called FastRenderView, which was derived from the SurfaceView class, we made sure we play nice with the activity life cycle, and we set up a thread in order to constantly render the SurfaceView via a Canvas. Here, we’ll reuse this FastRenderView class and augment it to do a few more things:

  • It keeps a reference to a Game instance from which it can get the active Screen. We constantly call the Screen.update() and Screen.present() methods from within the FastRenderView thread.
  • It keeps track of the delta time between frames that is passed to the active Screen.

It takes the artificial framebuffer to which the AndroidGraphics instance draws, and draws it to the SurfaceView, which is scaled if necessary.

Listing 5-15 shows the implementation of the AndroidFastRenderView class, with commentary where appropriate.

Listing 5-15. AndroidFastRenderView.java, a Threaded SurfaceView Executing Our Game Code

package com.badlogic.androidgames.framework.impl;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class AndroidFastRenderViewextends SurfaceView implements Runnable {
    AndroidGame game;
    Bitmap framebuffer;
    Thread renderThread = null ;
    SurfaceHolder holder;
    volatile boolean running = false ;

This should look familiar. We just need to add two more members—an AndroidGame instance and a Bitmap instance that represent our artificial framebuffer. The other members are the same as in our FastRenderView from Chapter 3.

    public AndroidFastRenderView(AndroidGame game, Bitmap framebuffer) {
        super (game);
        this.game = game;
        this.framebuffer = framebuffer;
        this.holder = getHolder();
    }

In the constructor, we simply call the base class’s constructor with the AndroidGame parameter (which is an Activity; this will be discussed in the following sections) and store the parameters in the respective members. Once again, we get a SurfaceHolder, as in previous sections.

    public void resume() {
        running = true ;
        renderThread = new Thread(this );
        renderThread.start();
    }

The resume() method is an exact copy of the FastRenderView.resume() method, so we won’t discuss it again. In short, the method makes sure that our thread interacts nicely with the activity life cycle.

    public void run() {
        Rect dstRect = new Rect();
        long startTime = System.nanoTime();
        while (running) {
            if (!holder.getSurface().isValid())
                continue ;

            float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f;
            startTime = System.nanoTime();

            game.getCurrentScreen().update(deltaTime);
            game.getCurrentScreen().present(deltaTime);

            Canvas canvas = holder.lockCanvas();
            canvas.getClipBounds(dstRect);
            canvas.drawBitmap(framebuffer, null , dstRect, null );
            holder.unlockCanvasAndPost(canvas);
        }
    }

The run() method has a few more features. The first addition is its ability to track delta time between each frame. For this, we use System.nanoTime(), which returns the current time in nanoseconds as a long.

Note  A nanosecond is one-billionth of a second.

In each loop iteration, we start by taking the difference between the last loop iteration’s start time and the current time. To make it easier to work with that delta, we convert it into seconds. Next, we save the current timestamp, which we’ll use in the next loop iteration, to calculate the next delta time. With the delta time at hand, we call the current Screen instance’s update() and present() methods, which will update the game logic and render things to the artificial framebuffer. Finally, we get a hold of the Canvas for the SurfaceView and draw the artificial framebuffer. The scaling is performed automatically in case the destination rectangle we pass to the Canvas.drawBitmap() method is smaller or bigger than the framebuffer.

Note that we’ve used a shortcut here to get a destination rectangle that stretches over the whole SurfaceView via the Canvas.getClipBounds() method. It will set the top and left members of dstRect to 0 and 0, respectively, and the bottom and right members to the actual screen dimensions (480×800 in portrait mode on a Nexus One). The rest of the method is exactly the same as what we had in our FastRenderView test in the last chapter. The method simply makes sure that the thread stops when the activity is paused or destroyed.

    public void pause() {
        running = false ;
        while (true ) {
            try {
                renderThread.join();
                return ;
            }catch (InterruptedException e) {
                // retry
            }
        }
    }
}

The last method of this class, pause(), is also the same as in the FastRenderView.pause() method—it simply terminates the rendering/main loop thread and waits for it to die completely before returning.

We are nearly done with our framework. The last piece of the puzzle is the implementation of the Game interface.

AndroidGame: Tying Everything Together

Our game development framework is nearly complete. All we need to do is tie the loose ends together by implementing the Game interface we designed in Chapter 3. To do this, we will use the classes we created in the previous sections of this chapter. The following is a list of responsibilities:

  • Perform window management. In our context, this means setting up an activity and an AndroidFastRenderView, and handling the activity life cycle in a clean way.
  • Use and manage a WakeLock so that the screen does not dim.
  • Instantiate and hand out references to Graphics, Audio, FileIO, and Input to interested parties.
  • Manage Screens and integrate them with the activity life cycle.
  • Our general goal is it to have a single class called AndroidGame from which we can derive. We want to implement the Game.getStartScreen() method later on to start our game in the following way.
public class MrNomextends AndroidGame {
    public Screen getStartScreen() {
        return new MainMenu(this );
    }
}

We hope you can see why it is beneficial to design a workable framework before diving headfirst into programming the actual game. We can reuse this framework for all future games that are not too graphically intensive. Now, let’s discuss Listing 5-16, which shows the AndroidGame class, split up by commentary.

Listing 5-16. AndroidGame.java; Tying Everything Together

package com.badlogic.androidgames.framework.impl;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.view.Window;
import android.view.WindowManager;

import com.badlogic.androidgames.framework.Audio;
import com.badlogic.androidgames.framework.FileIO;
import com.badlogic.androidgames.framework.Game;
import com.badlogic.androidgames.framework.Graphics;
import com.badlogic.androidgames.framework.Input;
import com.badlogic.androidgames.framework.Screen;

public abstract class AndroidGameextends Activity implements Game {
    AndroidFastRenderView renderView;
    Graphics graphics;
    Audio audio;
    Input input;
    FileIO fileIO;
    Screen screen;
    WakeLock wakeLock;

The class definition starts by letting AndroidGame extend the Activity class and implement the Game interface. Next, we define a couple of members that should already be familiar. The first member is AndroidFastRenderView, to which we’ll draw, and which will manage our main loop thread for us. Of course, we set the Graphics, Audio, Input, and FileIO members to instances of AndroidGraphics, AndroidAudio, AndroidInput, and AndroidFileIO. The next member holds the currently active Screen. Finally, there’s a member that holds a WakeLock that we use to keep the screen from dimming.

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
        int frameBufferWidth = isLandscape ? 480 : 320;
        int frameBufferHeight = isLandscape ? 320 : 480;
        Bitmap frameBuffer = Bitmap.createBitmap(frameBufferWidth,
                frameBufferHeight, Config.RGB_565);
        float scaleX = (float ) frameBufferWidth
                / getWindowManager().getDefaultDisplay().getWidth();
        float scaleY = (float ) frameBufferHeight
                / getWindowManager().getDefaultDisplay().getHeight();

        renderView = new AndroidFastRenderView(this , frameBuffer);
        graphics = new AndroidGraphics(getAssets(), frameBuffer);
        fileIO = new AndroidFileIO(this );
        audio = new AndroidAudio(this );
        input = new AndroidInput(this , renderView, scaleX, scaleY);
        screen = getStartScreen();
        setContentView(renderView);

        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "GLGame");
    }

The onCreate() method, which is the familiar startup method of the Activity class, starts by calling the base class’s onCreate() method, as required. Next, we make the Activity full-screen, as we did in a couple of other tests in Chapter 4. In the next few lines, we set up our artificial framebuffer. Depending on the orientation of the activity, we want to use a 320×480 framebuffer (portrait mode) or a 480×320 framebuffer (landscape mode). To determine the Activity’s screen orientations, we fetch the orientation member from a class called Configuration, which we obtain via a call to getResources().getConfiguration(). Based on the value of that member, we then set the framebuffer size and instantiate a Bitmap, which we’ll hand to the AndroidFastRenderView and AndroidGraphics instances in the following chapters.

Note  The Bitmap instance has an RGB565 color format. This way, we don’t waste memory, and our drawing is completed a little faster.

Note  For our first game, Mr. Nom, we will use a target resolution of 320×480 pixels. The AndroidGame class has these values hard-coded. If you want to use a different target resolution, modify AndroidGame accordingly!

We also calculate the scaleX and scaleY values that the SingleTouchHandler and the MultiTouchHandler classes will use to transform the touch event coordinates in our fixed-coordinate system.

Next, we instantiate the AndroidFastRenderView, AndroidGraphics, AndroidAudio, AndroidInput, and AndroidFileIO with the necessary constructor arguments. Finally, we call the getStartScreen() method, which our game will implement, and set the AndroidFastRenderView as the content view of the Activity. Of course, all the previously instantiated helper classes will do some more work in the background. For example, the AndroidInput class tells the selected touch handler to communicate with the AndroidFastRenderView.

    @Override
    public void onResume() {
        super.onResume();
        wakeLock.acquire();
        screen.resume();
        renderView.resume();
    }

Next is the onResume() method of the Activity class, which we override. As usual, the first thing we do is call the superclass method. Next, we acquire the WakeLock and make sure the current Screen is informed that the game, and thereby the activity, has been resumed. Finally, we tell the AndroidFastRenderView to resume the rendering thread, which will also kick off our game’s main loop, where we tell the current Screen to update and present itself in each iteration.

    @Override
    public void onPause() {
        super.onPause();
        wakeLock.release();
        renderView.pause();
        screen.pause();
        if (isFinishing())
            screen.dispose();
    }

First, the onPause() method calls the superclass method again. Next, it releases the WakeLock and makes sure that the rendering thread is terminated. If we don’t terminate the thread before calling the current Screen’s onPause() method, we may run into concurrency issues since the UI thread and the main loop thread will both access the Screen at the same time. Once we are sure the main loop thread is no longer alive, we tell the current Screen that it should pause itself. In case the Activity is going to be destroyed, we also inform the Screen so that it can do any necessary cleanup work.

    public Input getInput() {
        return input;
    }
    public FileIO getFileIO() {
        return fileIO;
    }
    public Graphics getGraphics() {
        return graphics;
    }
    public Audio getAudio() {
        return audio;
    }

The getInput(), getFileIO(), getGraphics(), and getAudio() methods need no explanation. We simply return the respective instances to the caller. Later, the caller will always be one of the Screen implementations of our game.

    public void setScreen(Screen screen) {
        if (screen ==null )
            throw new IllegalArgumentException("Screen must not be null");
        this.screen.pause();
        this.screen.dispose();
        screen.resume();
        screen.update(0);
        this.screen = screen;
    }

At first, the setScreen() method we inherit from the Game interface looks simple. We start with some traditional null-checking, since we can’t allow a null Screen. Next, we tell the current Screen to pause and dispose of itself so that it can make room for the new Screen. The new Screen is asked to resume itself and update itself once with a delta time of zero. Finally, we set the Screen member to the new Screen.

Let’s think about who will call this method and when. When we designed Mr. Nom, we identified all the transitions between various Screen instances. We’ll usually call the AndroidGame.setScreen() method in the update() method of one of these Screen instances.

For example, let’s assume we have a main menu Screen where we check to see if the Play button is pressed in the update() method. If that is the case, we will transition to the next Screen by calling the AndroidGame.setScreen() method from within the MainMenu.update() method with a brand-new instance of that next Screen. The MainMenu screen will regain control after the call to AndroidGame.setScreen(), and should immediately return to the caller as it is no longer the active Screen. In this case, the caller is the AndroidFastRenderView in the main loop thread. If you check the portion of the main loop responsible for updating and rendering the active Screen, you’ll see that the update() method will be called on the MainMenu class, but the present() method will be called on the new current Screen. This would be problematic, as we defined the Screen interface in a way that guarantees that the resume() and update() methods will be called at least once before the Screen is asked to present itself. That’s why we call these two methods in the AndroidGame.setScreen() method on the new Screen. The AndroidGame class takes care of everything.

    public Screen getCurrentScreen() {
        return screen;
    }
}

The last method is the getCurrentScreen() method, which simply returns the currently active Screen.

Finally, remember that AndroidGame derives from Game, which has another method called getStartScreen(). This is the method we have to implement to get things going for our game!

Now, we’ve created an easy-to-use Android game development framework. All we need to do is implement our game’s Screens. We can also reuse the framework for any future games, as long as they do not need immense graphics power. If that is necessary, we have to use OpenGL ES. However, to do this, we only need to replace the graphics part of our framework. All the other classes for audio, input, and file I/O can be reused.

Summary

In this chapter, we implemented a full-fledged 2D Android game development framework from scratch that can be reused for all future games (as long as they are graphically modest). Great care was taken to achieve a good, extensible design. We could take the code and replace the rendering portions with OpenGL ES, thus making Mr. Nom 3D.

With all this boilerplate code in place, let’s concentrate on what we are here for: writing games!

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

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