-
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.
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!
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!
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.
Name it com.yourname.framework.implementation.
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)!
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.
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;
}
}
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.
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.
- 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.
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.
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();
}
}
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.
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.
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);
}
}
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.
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 + "'");
}
}
}
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.
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.
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);
}
}
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.
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
}
}
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.
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).
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
}
}
}
}
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.
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.
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.
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();
}
}
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!).
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.
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();
}
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.
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;
}
}
}
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.
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;
}
}
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.
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();
}
}
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.
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
|
|
The source code is available in the next lesson!

comments powered by Disqus