• Home
  • Tutorials
    • Game Development Tutorial>
      • Unit 1: Beginning Java>
        • Before you begin...
        • Day 1: Setting Up
        • Day 2: Java Basics
        • Day 3: More Basics
        • Day 4: Java Math
        • Day 5: More Math
        • Day 6: If... else...
        • Day 7: More Control Flow
        • Day 8: Looping
        • Day 9: More on Looping
        • Day 10: Inheritance, Interface
        • Day 11: Threads and Graphics
      • Unit 2: Creating a Game I>
        • Day 1: Foundations
        • Day 2: Basic Framework
        • Day 3: Taking User Input
        • Day 4: Enter the Robot
        • Day 5: Background and Sprites
        • Day 6: Adding Enemies
        • Day 7: Shooting Bullets
        • Day 8: Animations
        • Day 9: 2D-Arrays
        • Day 10: Painting the Tilemap
      • Unit 3: Creating a Game II>
        • Day 1: Level Creation - Part 1
        • Day 2: Level Creation - Part 2
        • Day 3: Level Creation - Part 3
        • Collision Detection Basics
        • Day 4: Collision Detection Part 1
        • Day 5: Collision Detection Part 2
        • Day 6: Collision Detection Part 3
        • Day 7: Health System & Death
        • Day 8: Basic AI & Final Touches
      • Unit 4: Android Game Development>
        • Day 1: Introduction to Android
        • Day 2: Setting up for Development
        • Day 3: Creating our First Android Application
        • Day 4: Parts of an Android Application
        • Day 5: The Android Game Framework: Part I
        • Day 6: The Android Game Framework: Part II
        • Create an Android Game From Scratch (or port your existing game)
        • Day 7: Creating an Android Game (From Start to Finish)
      • Reference Sheet
    • Zombie Bird Tutorial (Flappy Bird Remake)>
      • Unit 1: Building the Game>
        • Introduction
        • Day 1: Flappy Bird - An In-depth Analysis
        • Day 2: Setting up libGDX
        • Day 3: Understanding the libGDX Framework
        • Day 4: GameWorld and GameRenderer and the Orthographic Camera
        • Day 5: The Flight of the Dead - Adding the Bird
        • Day 6: Adding Graphics - Welcome to the Necropolis
        • Day 7: The Grass, the Bird and the Skull Pipe
        • Day 8: Collision Detection and Sound Effects
        • Day 9: Finishing Gameplay and Basic UI
        • Day 10: GameStates and High Score
        • Day 11: Supporting iOS/Android + SplashScreen, Menus and Tweening
        • Day 12: Completed UI & Source Code
    • Android Application Development Tutorial>
      • Unit 1: Writing Basic Android Apps>
        • Before you begin...
        • Day 1: Android 101
        • Day 2: Getting to Know the Android Project
        • Day 3: The Development Machine
        • Day 4: Building a Music App - Part 1: Building Blocks
        • Day 5: Building a Music App - Part 2: Intents
        • Day 6: Building a Music App - Part 3: Activity Lifecycles
  • New Forum
  • About Us
    • Contact Us
  • Our Games
    • TUMBL: FallDown
  • Facebook
  • Twitter

      GAME DEVELOPMENT TUTORIAL: DAY 4-6: The Android game Framework part Ii: 

Welcome to Day 6 of Unit 4: Android Game Development.

In the last lesson, we have created the interface for our AndroidGame project. We will move on to the second part of the Game Architecture: the implementation.

Using the "skeleton" that we have created in Day 5's lesson, we will now create the bridge between Android code and your Game.

More about the Implementation

In the implementation, we will literally implement (using the built-in word "implements") each of the interfaces from Day 5. In terms of the code, we will be importing many Android classes and calling various Android methods. 

There will be 9 classes for implementation and 3 helper classes for touch. 

This is one of the most important lessons in the entire tutorial series, so take your time with this one!

Picture
Note: The framework developed below is from a great book on game development called Beginning Android Games Development (by Mario Zechner and Robert Green). If you would like to explore the topics covered here in much more depth, I highly recommend that you pick up this book. It is very informative and well-written.

Here's the link to the book!

The writers of the book graciously provided this code under Apache 2.0 license, so you can use it in both commercial and non-commercial projects. More on that here.

Creating the Implementation

0. Creating the Package

In your AndroidGame project's src folder, create a new Package. 
Name it com.yourname.framework.implementation.
Picture
This is where our implementation (all 10 classes) will be stored.


Remember: All the code that follow will assume that your package is com.jamescho.framework.implementation. 

If you have a different package name, you will have to correct your "package" declaration (and all subsequent import statements)!

1. "AndroidGame" class

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidGame.
This class is the backbone of your game that holds everything together.
package com.jamescho.framework.implementation;

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.jamescho.framework.Audio;
import com.jamescho.framework.FileIO;
import com.jamescho.framework.Game;
import com.jamescho.framework.Graphics;
import com.jamescho.framework.Input;
import com.jamescho.framework.Screen;

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

    @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 isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
        int frameBufferWidth = isPortrait ? 800: 1280;
        int frameBufferHeight = isPortrait ? 1280: 800;
        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 = getInitScreen();
        setContentView(renderView);
       
        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "MyGame");
    }

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

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

        if (isFinishing())
            screen.dispose();
    }

    @Override
    public Input getInput() {
        return input;
    }

    @Override
    public FileIO getFileIO() {
        return fileIO;
    }

    @Override
    public Graphics getGraphics() {
        return graphics;
    }

    @Override
    public Audio getAudio() {
        return audio;
    }

    @Override
    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;
    }
   
    public Screen getCurrentScreen() {

        return screen;
    }
}
Upon creating this class, you will have multiple errors. Please ignore these errors for now! We will fix them one at a time, so do not worry. 

Before we create the other implementations, let's first talk about the important objects and methods in this code.
Look at the code as I describe the important pieces of it below. You do not need to type anything.

1. The Android libraries - If you look at the first 10 import statements, you will see they all begin with "android." This means that these are Android based classes stored in the SDK Platform. 

I will touch upon a few you should be familiar with. You can learn about each package in detail at: http://developer.android.com/reference

- An Activity is usually an interactive window. For a typical application, you might have multiple activities (usually one for each screen). For example, you might have an activity for the login screen, another activity for the settings page, and so on.

 Activities are made up of components called Views (elements on the screen such as images or text).

- Bundle lets you pass information between multiple Activities. Going back to the example of a login screen, a Bundle might transmit your login information to another activity that checks it to grant access.

- PowerManager.WakeLock is used to prevent the phone from going to sleep while our game is running.

2. Notice that we are importing our interfaces in the second set of imports. Using them, we create objects in the variable declaration section. Also notice that our class AndroidGame extends Activity and implements Game (This is the implementation of the Game interface).


3. In the code, we first see an onCreate method. To understand this, you must first understand the Android Activity Lifecycle.  
Picture
- Since Android is a mobile platform, your App's current Activity (the current page on your Application) can be paused or resumed (or created, stopped, destroyed, or restarted) depending on what happens to the Android device. For example, if the user receives a phone call, your activity will be Stopped (it will no longer be visible). Similarly, if the user presses the home button (taking your activity off of the screen into the "stack") and navigates back to your application to reopen your Activity, your Activity will have been stopped and restarted.

Therefore, your application must call methods to handle these transitions without errors, crashing, or loss of progress.

- Let's go back to the onCreate method. The onCreate method is called when the activity is created for the first time. 

Notice that it has the @Override annotation, meaning that this method belongs to the superclass (remember we extended the Activity superclass).

Typically, in the onCreate method, you will set the layout (appearance) of the activity. As we will in each stage of the Activity Lifecycle, we first call the super method (that is the default onCreate method in the Activity class), remove the Title of our Application (applications have a title bar by default), and set our application to "full screen." 

4. We then check the current orientation of the device and set the Width and Height of our Game. 
The syntax here uses a question mark: int frameBufferWidth = isPortrait ? 800: 1280;

This question mark is a way of evaluating a boolean. If the boolean isPortrait is true, frameBufferWidth takes the value of 800. If it is false, it takes the value of 1280.

This is where we would change the resolution of our game. We will be using 800x480 (to support lower end devices). If you wanted to dynamically adjust our game's resolution (so that the game always runs on the native resolution), you could use if statements to only call the game classes designed for that resolution, but we will be taking a one-size-fits-all approach for the purposes of this tutorial.

5. We then proceed by creating floats to scale and adjust everything to the device's aspect ratio.

6. Next, we define each of the interfaces by creating new instances of the implementation classes that we will be creating.

7. Notice that we also use the PowerManager to define the wakeLock variable and we acquire and release wakelock in the onResume and onPause methods, respectively. 

Why only these two states and not the onStart and onStopped methods? 

If you refer back to the Android Activity Lifecycle, you will notice that the onResume and onPause methods are always called before and after an activity becomes visible. Therefore, we can, for the most part, ignore onStart, onStopped, and etc.

8. The rest of the AndroidGame class defines additional methods to return various interfaces, and we also define the setScreen and getCurrentScreen methods from the Game interface.

A lengthy explanation, but this should give you a slightly better understanding of how our Game Architecture (interface, implementation, game code) works.

Before moving on to the other implementations, I suggest that you study the relationship between the AndroidGame implementation and its Game interface, because the rest of the lesson will basically be variations on the same theme.

2. The "AndroidGraphics" class

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidGraphics.
 
Here, you will implement the many abstract methods that you have created in the Graphics interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

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.jamescho.framework.Graphics;
import com.jamescho.framework.Image;

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

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

    @Override
    public Image newImage(String fileName, ImageFormat format) {
        Config config = null;
        if (format == ImageFormat.RGB565)
            config = Config.RGB_565;
        else if (format == ImageFormat.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, null, options);
            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 = ImageFormat.RGB565;
        else if (bitmap.getConfig() == Config.ARGB_4444)
            format = ImageFormat.ARGB4444;
        else
            format = ImageFormat.ARGB8888;

        return new AndroidImage(bitmap, format);
    }

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


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

    @Override
    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 + height - 1, paint);
    }
   
    @Override
    public void drawARGB(int a, int r, int g, int b) {
        paint.setStyle(Style.FILL);
       canvas.drawARGB(a, r, g, b);
    }
   
    @Override
    public void drawString(String text, int x, int y, Paint paint){
        canvas.drawText(text, x, y, paint);

       
    }
   

    public void drawImage(Image Image, int x, int y, int srcX, int srcY,
            int srcWidth, int srcHeight) {
        srcRect.left = srcX;
        srcRect.top = srcY;
        srcRect.right = srcX + srcWidth;
        srcRect.bottom = srcY + srcHeight;
       
       
        dstRect.left = x;
        dstRect.top = y;
        dstRect.right = x + srcWidth;
        dstRect.bottom = y + srcHeight;

        canvas.drawBitmap(((AndroidImage) Image).bitmap, srcRect, dstRect,
                null);
    }
   
    @Override
    public void drawImage(Image Image, int x, int y) {
        canvas.drawBitmap(((AndroidImage)Image).bitmap, x, y, null);
    }
   
    public void drawScaledImage(Image Image, int x, int y, int width, int height, int srcX, int srcY, int srcWidth, int srcHeight){
       
       
     srcRect.left = srcX;
        srcRect.top = srcY;
        srcRect.right = srcX + srcWidth;
        srcRect.bottom = srcY + srcHeight;
       
       
        dstRect.left = x;
        dstRect.top = y;
        dstRect.right = x + width;
        dstRect.bottom = y + height;
       
   
       
        canvas.drawBitmap(((AndroidImage) Image).bitmap, srcRect, dstRect, null);
       
    }
   
    @Override
    public int getWidth() {
        return frameBuffer.getWidth();
    }

    @Override
    public int getHeight() {
        return frameBuffer.getHeight();
    }
}
 
The graphics class is pretty straight forward. The three important classes here are Bitmap, Canvas, and Paint.

1. Bitmap just allows you to create image objects.
2. Canvas is really a canvas for your images. You draw images onto the canvas, which will appear on the screen.
3. Paint is used for styling what you draw to the screen.

When you draw an image to the screen, you first store it to memory, and you just call the same file from memory each time that this image is used. No matter how many GBs are in a device's RAM, only a small chunk is dedicated to each app. This "heap" can be as little as 16MB, so you really have to be careful with memory management. So how do you conserve memory?

To do so, you need a good understanding of ImageFormats. Before we discuss ImageFormats, let's first see how much memory a typical image will take up. Each pixel in an image takes up a bit (1/8 of a byte) of memory. So you can calculate the total memory usage by multiplying the width and height; however, there is a third dimension to consider: depth. This is where ImageFormats come in.

You will recognize the following ImageFormats from the Graphics interface (RGB565, ARGB4444, ARGB8888) in the code above. These are three formats that can be used when storing images to memory.

The first (RGB565) takes up the least memory (at least in practice). Red, Green, and Blue (RGB) have depths of 5, 6, and 5, respectively. However, there is no Alpha value (opacity/transparency) to consider.

The second (ARGB4444) has a total depth of 16. (Use this when you need transparency in your image).

The third (ARGB8888) has a total depth of 32. (You should almost never need to use this format. ARGB4444 usually is enough).

To simplify, the quality of your image will improve if you use the 3rd format as opposed to the 1st, but you will take up memory much faster. A single 1000x1000 image with depth of 32 will take up 32,000,000 bits, or 4MB. If you have four of those on a device with 16MB, you will get an out of memory exception and your game will crash (this assumes you have no other objects stored in memory, which is impossible).

In the AndroidGraphics Class, you will find many drawing methods that we will call throughout our development. Don't worry about the try, catch, and finally statements. They will do all the "dirty" work behind the scenes for you.

Let's move on to the other seven implementations.

3. The "AndroidFileIO" Implementation

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidFileIO.
 
Here, you will implement the many abstract methods that you have created in the FileIO interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

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.jamescho.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;
       
 
   
    }

    @Override
    public InputStream readAsset(String file) throws IOException {
        return assets.open(file);
    }

    @Override
    public InputStream readFile(String file) throws IOException {
        return new FileInputStream(externalStoragePath + file);
    }

    @Override
    public OutputStream writeFile(String file) throws IOException {
        return new FileOutputStream(externalStoragePath + file);
    }
   
    public SharedPreferences getSharedPref() {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }
}
 
Once you fix the package, you will have no other errors in this class. I will not describe this class in detail, as most of it is straight forward. If there are specific classes/methods you are unsure about, refer to:http://developer.android.com/reference

4. The "AndroidAudio" Implementation

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidAudio.
 
Here, you will implement the many abstract methods that you have created in the Audio interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

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.jamescho.framework.Audio;
import com.jamescho.framework.Music;
import com.jamescho.framework.Sound;

public class AndroidAudio implements Audio {
    AssetManager assets;
    SoundPool soundPool;

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

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

    @Override
    public Sound createSound(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 + "'");
        }
    }
}
 
This is also a pretty straight forward implementation. SoundPool is worth mentioning here. This class is used to manage audio resources from memory or from the file system.

Typically, short sounds (which are repeated over and over again) can be stored to memory. This reduces load from the CPU; however, longer sounds (which are lengthy and take up a lot of memory) can be played directly from the file system.

This is why we have two types of Audio in our game: Sound and Music. Let's implement those next.

5. The "AndroidSound" Interface

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidSound.
 
Here, you will implement the many abstract methods that you have created in the Sound interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

import android.media.SoundPool;

import com.jamescho.framework.Sound;

public class AndroidSound implements Sound {
    int soundId;
    SoundPool soundPool;

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

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

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

}
 
The AndroidSound interface uses the SoundPool and an integer ID to keep track of various sounds, play them, and dispose them from memory. Let's see how music is handled.

6. The "AndroidMusic" Implementation

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidMusic.
 
Here, you will implement the many abstract methods that you have created in the Music interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

import java.io.IOException;

import android.content.res.AssetFileDescriptor;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaPlayer.OnSeekCompleteListener;
import android.media.MediaPlayer.OnVideoSizeChangedListener;

import com.jamescho.framework.Music;

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

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

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

    @Override
    public boolean isLooping() {
        return mediaPlayer.isLooping();
    }

    @Override
    public boolean isPlaying() {
        return this.mediaPlayer.isPlaying();
    }

    @Override
    public boolean isStopped() {
        return !isPrepared;
    }

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

   
   
    @Override
    public void play() {
        if (this.mediaPlayer.isPlaying())
            return;

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

    @Override
    public void setLooping(boolean isLooping) {
        mediaPlayer.setLooping(isLooping);
    }

    @Override
    public void setVolume(float volume) {
        mediaPlayer.setVolume(volume, volume);
    }

    @Override
    public void stop() {
         if (this.mediaPlayer.isPlaying() == true){
        this.mediaPlayer.stop();
       
       synchronized (this) {
           isPrepared = false;
        }}
    }

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

    @Override
    public void seekBegin() {
        mediaPlayer.seekTo(0);
       
    }


    @Override
    public void onPrepared(MediaPlayer player) {
        // TODO Auto-generated method stub
         synchronized (this) {
               isPrepared = true;
            }
       
    }

    @Override
    public void onSeekComplete(MediaPlayer player) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void onVideoSizeChanged(MediaPlayer player, int width, int height) {
        // TODO Auto-generated method stub
       
    }
}
 
This class defines various methods used for playback.

Although much lengthier than the AndroidSound implementation, this class is also very straightforward. The most important part of this class is the MediaPlayer object, which handles audio/video playback on Android. There are various listeners that check for  changes in playback (these listeners are not unlike the KeyListeners from our Unit 2/3 game. They check for an event and carry out an action).

Let's now move on to Rendering.

7. The "AndroidFastRenderView" class

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidFastRenderView.

Above, I mentioned that an Activity (a window) has a component called a View. This class creates a SurfaceView (which you can use to create graphics-based UI and update very quickly). 
package com.jamescho.framework.implementation;

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

public class AndroidFastRenderView extends SurfaceView implements Runnable {
    AndroidGame game;
    Bitmap framebuffer;
    Thread renderThread = null;
    SurfaceHolder holder;
    volatile boolean running = false;
   
    public AndroidFastRenderView(AndroidGame game, Bitmap framebuffer) {
        super(game);
        this.game = game;
        this.framebuffer = framebuffer;
        this.holder = getHolder();

    }

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

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

            float deltaTime = (System.nanoTime() - startTime) / 10000000.000f;
            startTime = System.nanoTime();
           
            if (deltaTime > 3.15){
                deltaTime = (float) 3.15;
           }
     

            game.getCurrentScreen().update(deltaTime);
            game.getCurrentScreen().paint(deltaTime);
         
           
           
            Canvas canvas = holder.lockCanvas();
            canvas.getClipBounds(dstRect);
            canvas.drawBitmap(framebuffer, null, dstRect, null);                          
            holder.unlockCanvasAndPost(canvas);
           
           
        }
    }

    public void pause() {                        
        running = false;                        
        while(true) {
            try {
                renderThread.join();
                break;
            } catch (InterruptedException e) {
                // retry
            }
           
        }
    }    
   
 
}
It is this class that gives us a direct window into our game. Also notice this is where update() and paint() are called "internally" (much like how update() and paint() were called automatically in our applet). An important variable here is the deltaTime variable, which checks how much time has elapsed since the last time the update/paint methods were called. 

To understand why this is important, we should talk about frame rate independent movement.

Frame rate Independent Movement

In our game from Units 2 and 3, we had a frame rate dependent movement. Although we fixed the frame rate at 60, if the game slowed down due to heavier load on our CPU and dropped the frame rate to 30, our character's movement speed would have been halved. This was fine for our computers, which could easily handle the simple applet without slowing down. However, Android devices are rarely powerful enough to maintain 60fps indefinitely. The CPU will be burdened with incoming messages, internal changes, and much more. That means that the frame rate will fluctuate.

To prevent movement speed from depending on frame rate, we use this deltaTime variable to check how much time elapsed since the last update. If the update took twice as long (i.e. frame rate was halved), then deltatime would be doubled. We multiply this deltaTime throughout our game's update methods to ensure that no matter what the frame rate is, our character will move by the same amount given the same time period.

Of course, this means that our speed could go from 1 pixel per second to 10 pixels per second. If we have a thin wall, this sudden increase in deltaTime could mean that our collision detection system will break. That is why we cap the deltaTime (it is capped at 3.15 in the above example) so that if the game slows down too much, then we will let movement depend on frame rate so that we do not break our entire game trying to maintain consistent movement. This is prioritization at work.

Now that we have discussed our "window" let's discuss input.

8. The "AndroidInput" Implementation

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidInput.
 
Here, you will implement the many abstract methods that you have created in the Input interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

import java.util.List;

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

import com.jamescho.framework.Input;

public class AndroidInput implements Input {    
    TouchHandler touchHandler;

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


    @Override
    public boolean isTouchDown(int pointer) {
        return touchHandler.isTouchDown(pointer);
    }

    @Override
    public int getTouchX(int pointer) {
        return touchHandler.getTouchX(pointer);
    }

    @Override
    public int getTouchY(int pointer) {
        return touchHandler.getTouchY(pointer);
    }



    @Override
    public List<TouchEvent> getTouchEvents() {
        return touchHandler.getTouchEvents();
    }
   
}
 
This implementation relies heavily on three other classes (that we will now create). One interesting thing to note here:

In Android 2.0, multi-touch was introduced. So in this implementation, we check the SDK build version (5 equates to Android 2.0) and implement the appropriate touch handler (which we develop below!).

9. the "TouchHandler" Class

Right click on com.yourname.framework.implementation and create a new Java Class called TouchHandler.
 
REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

import java.util.List;

import android.view.View.OnTouchListener;

import com.jamescho.framework.Input.TouchEvent;

public interface TouchHandler extends OnTouchListener {
    public boolean isTouchDown(int pointer);
   
    public int getTouchX(int pointer);
   
    public int getTouchY(int pointer);
   
    public List<TouchEvent> getTouchEvents();
}
 
This simple class handles touch events by checking for touchDown and the X and Y coordinates.

10. The "SingleTouchHandler" class

Right click on com.yourname.framework.implementation and create a new Java Class called SingleTouchHandler.
 
REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

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

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

import com.jamescho.framework.Pool;
import com.jamescho.framework.Input.TouchEvent;
import com.jamescho.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;
   
    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;
    }
   
    @Override
    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;
        }
    }

    @Override
    public boolean isTouchDown(int pointer) {
        synchronized(this) {
            if(pointer == 0)
                return isTouched;
            else
                return false;
        }
    }

    @Override
    public int getTouchX(int pointer) {
        synchronized(this) {
            return touchX;
        }
    }

    @Override
    public int getTouchY(int pointer) {
        synchronized(this) {
            return touchY;
        }
    }

    @Override
    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;
        }
    }
}
 

11. The "MultiTouchHandler" Class

Right click on com.yourname.framework.implementation and create a new Java Class called MultiTouchHandler.
 
REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

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

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

import com.jamescho.framework.Pool;
import com.jamescho.framework.Input.TouchEvent;
import com.jamescho.framework.Pool.PoolObjectFactory;

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>() {
            @Override
            public TouchEvent createObject() {
                return new TouchEvent();
            }
        };
        touchEventPool = new Pool<TouchEvent>(factory, 100);
        view.setOnTouchListener(this);

        this.scaleX = scaleX;
        this.scaleY = scaleY;
    }

    @Override
    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;
        }
    }

    @Override
    public boolean isTouchDown(int pointer) {
        synchronized (this) {
            int index = getIndex(pointer);
            if (index < 0 || index >= MAX_TOUCHPOINTS)
                return false;
            else
                return isTouched[index];
        }
    }

    @Override
    public int getTouchX(int pointer) {
        synchronized (this) {
            int index = getIndex(pointer);
            if (index < 0 || index >= MAX_TOUCHPOINTS)
                return 0;
            else
                return touchX[index];
        }
    }

    @Override
    public int getTouchY(int pointer) {
        synchronized (this) {
            int index = getIndex(pointer);
            if (index < 0 || index >= MAX_TOUCHPOINTS)
                return 0;
            else
                return touchY[index];
        }
    }

    @Override
    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;
    }
}
 
The two previous classes make a heavy use of the Pool class. As I doubt many people will be interested in knowing how this class works (you cannot really do much with it to change your game), I will hold off on describing it for the sake of time. Perhaps I will update this lesson once Unit 4 is completed so I can discuss how it functions.

12. the "AndroidImage" Implementation

The final class we will create is the AndroidImage class. 

Right click on com.yourname.framework.implementation and create a new Java Class called AndroidImage.
 
Here, you will implement the many abstract methods that you have created in the Image interface.

REMEMBER TO CHANGE THE PACKAGE IN THE PACKAGE AND IMPORT DECLARATIONS.
package com.jamescho.framework.implementation;

import android.graphics.Bitmap;

import com.jamescho.framework.Image;
import com.jamescho.framework.Graphics.ImageFormat;

public class AndroidImage implements Image {
    Bitmap bitmap;
    ImageFormat format;
   
    public AndroidImage(Bitmap bitmap, ImageFormat format) {
        this.bitmap = bitmap;
        this.format = format;
    }

    @Override
    public int getWidth() {
        return bitmap.getWidth();
    }

    @Override
    public int getHeight() {
        return bitmap.getHeight();
    }

    @Override
    public ImageFormat getFormat() {
        return format;
    }

    @Override
    public void dispose() {
        bitmap.recycle();
    }      
}
 
Our last class is very straightforward. All the methods are self-explanatory!

The framework is finished. We are ready to build our first Android game. 

The next lesson will discuss how we will go about creating our game using this framework, so people porting their own game can learn how to do so, and people who are following the tutorial to completion will know which way we are headed.
As always, feel free to email me questions at jamescho7@kilobolt.com

Picture
The source code is available in the next lesson!
Picture
Go to Unit 4: Day 5
Go to "Create an Android Game..."
comments powered by Disqus
© 2014 Kilobolt, LLC. All rights reserved.