Beginning Android Game Programming
Introduction
Game development on the Android platform is challenging and rewarding and comes with it's own set of pitfalls and hard learned lessons. In this series of articles I hope to show you some of those pitfalls and maybe teach a lesson or two along the way. While you will be challenged I also hope to help you see the rewards that can come from those challenges.Background
Mobile Java applications are not a new concept and in fact were one of the original focuses of Sun when it because the java project. There have been mobile java games since the first JVM was put on a mobile device. In the case of the Android things are a little bit different. Android uses what is called the Dalvik Virtual Machine, or DVM, which is an opensource implementation of a JVM. There are several differences between Dalvik and a standard JVM, some subtle, some not so subtle. The DVM is also not aligned to either Java SE or Java ME, but to an apache implementation called Apache Harmony Java. All of this makes for a slight learning curve if you happen to be transitioning from Java ME.Basic Game Architecture
This example requires three basic classes in order to implement a basic game. The game logic can be extended or changed and the resources can be replaces easily using the same patterns. The file DrawablePanel.java contains the code for DrawablePanel which extends SurfaceView and provides a full screen canvas. The DrawablePanel contains an AnimationThread. This class extends Thread (you could also provide a runnable, whatever pattern is more familiar). The AnimationThread class holds a reference to the DrawablePanel described above and updates the DrawablePanel for game logic and forces a redraw of the panel.First Steps
I will be using Eclipse as the compilation tool for this tutorial. I am also using the Android Eclipse plugin. Eclipse can be found at www.eclipse.org/downloads and you can find the ADT plugin at developer.android.com/sdk/eclipse-adt.htmlFirst create an Android project:
Since I will be demonstrating an animated sprite implementation we need a sprite strip. Here is the sprite strip we will be using:
You have to add the asset to the project:
Code
Now we come to the actual coding. First we will create the AnimatedSprite class since that has the least amount of dependencies.AnimatedSprite class
01.
package
com.android.tutorial;
02.
03.
import
android.graphics.Bitmap;
04.
import
android.graphics.Canvas;
05.
import
android.graphics.Rect;
06.
07.
public
class
AnimatedSprite {
08.
private
Bitmap animation;
09.
private
int
xPos;
10.
private
int
yPos;
11.
private
Rect sRectangle;
12.
private
int
fps;
13.
private
int
numFrames;
14.
private
int
currentFrame;
15.
private
long
frameTimer;
16.
private
int
spriteHeight;
17.
private
int
spriteWidth;
18.
19.
public
AnimatedSprite() {
20.
sRectangle =
new
Rect(
0
,
0
,
0
,
0
);
21.
frameTimer =
0
;
22.
currentFrame =
0
;
23.
xPos =
80
;
24.
yPos =
200
;
25.
}
26.
27.
public
void
Initialize(Bitmap bitmap,
int
height,
int
width,
int
fps,
int
frameCount) {
28.
this
.animation = bitmap;
29.
this
.spriteHeight = height;
30.
this
.spriteWidth = width;
31.
this
.sRectangle.top =
0
;
32.
this
.sRectangle.bottom = spriteHeight;
33.
this
.sRectangle.left =
0
;
34.
this
.sRectangle.right = spriteWidth;
35.
this
.fps =
1000
/ fps;
36.
this
.numFrames = frameCount;
37.
}
38.
39.
public
int
getXPos() {
40.
return
xPos;
41.
}
42.
43.
public
int
getYPos() {
44.
return
yPos;
45.
}
46.
47.
public
void
setXPos(
int
value) {
48.
xPos = value;
49.
}
50.
51.
public
void
setYPos(
int
value) {
52.
yPos = value;
53.
}
54.
55.
public
void
Update(
long
gameTime) {
56.
if
( gameTime > frameTimer + fps) {
57.
frameTimer = gameTime;
58.
currentFrame +=
1
;
59.
60.
if
( currentFrame >= numFrames ) {
61.
currentFrame =
0
;
62.
}
63.
64.
sRectangle.left = currentFrame * spriteWidth;
65.
sRectangle.right = sRectangle.left + spriteWidth;
66.
}
67.
}
68.
69.
public
void
draw(Canvas canvas) {
70.
Rect dest =
new
Rect(getXPos(), getYPos(), getXPos() + spriteWidth,
71.
getYPos() + spriteHeight);
72.
canvas.drawBitmap(animation, sRectangle, dest,
null
);
73.
}
74.
}
Here we can see that AnimateSprite is a fairly simple class. It basically keeps track of what frame it's currently displaying and increments it during update if the appropriate time has elapsed. If it reaches the end of the list it will wrap around and begin rendering at the start of the list again.
I've abstracted the interaction between the animation logic and display logic with an interface called ISurface. Let's look at that interface now.
ISurface interface
01.
package
com.android.tutorial;
02.
03.
import
android.graphics.Canvas;
04.
05.
public
interface
ISurface {
06.
void
onInitalize();
07.
void
onDraw(Canvas canvas);
08.
void
onUpdate(
long
gameTime);
09.
}
Here we can see that the ISurface interface provides some basic functions. One for initialization which is called at startup or when the sprite is first initialized. The second is the draw function which is pretty self explanatory. The final function is the update function which is what is used by the animation thread to update it's animation(s).
Let's look at the AnimationThread now
AnimationThread class
01.
package
com.android.tutorial;
02.
03.
import
android.graphics.Canvas;
04.
import
android.view.SurfaceHolder;
05.
06.
public
class
AnimationThread
extends
Thread {
07.
private
SurfaceHolder surfaceHolder;
08.
private
ISurface panel;
09.
private
boolean
run =
false
;
10.
11.
public
AnimationThread(SurfaceHolder surfaceHolder, ISurface panel) {
12.
this
.surfaceHolder = surfaceHolder;
13.
this
.panel = panel;
14.
15.
panel.onInitalize();
16.
}
17.
18.
public
void
setRunning(
boolean
value) {
19.
run = value;
20.
}
21.
22.
private
long
timer;
23.
24.
@Override
25.
public
void
run() {
26.
27.
Canvas c;
28.
while
(run) {
29.
c =
null
;
30.
timer = System.currentTimeMillis();
31.
panel.onUpdate(timer);
32.
33.
try
{
34.
c = surfaceHolder.lockCanvas(
null
);
35.
synchronized
(surfaceHolder) {
36.
panel.onDraw(c);
37.
}
38.
}
finally
{
39.
// do this in a finally so that if an exception is thrown
40.
// during the above, we don't leave the Surface in an
41.
// inconsistent state
42.
if
(c !=
null
) {
43.
surfaceHolder.unlockCanvasAndPost(c);
44.
}
45.
}
46.
}
47.
}
48.
}
This class isn't doing much either, but what it is doing is very important. This class extends Thread. You could also create a runnable and implement threading that way. This way is more obvious which is why I chose it. The run function of this class is where all the work is being done. In this case the function retrieves the current system time and calls it's dependent panel's update function. It then obtains an exclusive lock on the containing surface's canvas and passes that canvas to the surface view's draw function. This call is synchronized to ensure stability.
Now we need to provide a concrete implementation of the ISurface interface for our tutorial. This will be the responsibility of the DrawablePanel class. Let's look at that class now:
Now we need to provide a concrete implementation of the ISurface interface for our tutorial. This will be the responsibility of the DrawablePanel class. Let's look at that class now:
DrawablePanel class
01.
package
com.android.tutorial;
02.
03.
import
android.content.Context;
04.
import
android.graphics.Bitmap;
05.
import
android.graphics.BitmapFactory;
06.
import
android.graphics.Canvas;
07.
import
android.graphics.Color;
08.
import
android.view.SurfaceHolder;
09.
import
android.view.SurfaceView;
10.
11.
public
abstract
class
DrawablePanel
12.
extends
SurfaceView
13.
implements
SurfaceHolder.Callback, ISurface {
14.
15.
private
AnimationThread thread;
16.
17.
public
DrawablePanel(Context context) {
18.
super
(context);
19.
getHolder().addCallback(
this
);
20.
21.
this
.thread =
new
AnimationThread(getHolder(),
this
);
22.
}
23.
24.
@Override
25.
public
void
onDraw(Canvas canvas) {
26.
}
27.
28.
@Override
29.
public
void
surfaceChanged(SurfaceHolder holder,
int
format,
int
width,
30.
int
height) {
31.
}
32.
33.
@Override
34.
public
void
surfaceCreated(SurfaceHolder holder) {
35.
thread.setRunning(
true
);
36.
thread.start();
37.
}
38.
39.
@Override
40.
public
void
surfaceDestroyed(SurfaceHolder holder) {
41.
boolean
retry =
true
;
42.
thread.setRunning(
false
);
43.
while
(retry) {
44.
try
{
45.
thread.join();
46.
retry =
false
;
47.
}
catch
(InterruptedException e) {
48.
// we will try it again and again...
49.
}
50.
}
51.
}
52.
}
This class extends SurfaceView and implements the SurfaceHolder.Callback interface. This is one technique to create a custom view. This class also implements our ISurface interface. It also has an AnimationThread. During construction the class does some standard plumbing to hookup the SurfaceHolder.Callback interface and starts the AnimationThread we looked at earlier. One of the parameters of the AnimationThread is an ISurface. This is where we can hook up our update and draw events to the animation thread. In this case we don't have to perform any logic for either event so they are left as empty functions. The final point of interest is surface creation or destruction. This will occur if your app goes out of focus for any reason. This could be because the user hit the home button, the app is being put to sleep, or the operation system is cleaning up memory for a larger foreground app. There are other reasons but those are the most common. In this case we have to make sure the thread no longer pumps animations and tries to draw to the canvas since the canvas will no longer exist. This is done by stopping the thread in the surfaceDestroyed callback. The thread is started in the surfaceCreated callback. This is the main reason for implementing the SurfaceHolder.Callback interface.
Finally edit the main activity class AndroidTutorial.java and replace the code with the following:
AndroidTutorial activity
Finally edit the main activity class AndroidTutorial.java and replace the code with the following:
AndroidTutorial activity
01.
package
com.android.tutorial;
02.
03.
04.
import
android.app.Activity;
05.
import
android.content.Context;
06.
import
android.graphics.BitmapFactory;
07.
import
android.graphics.Canvas;
08.
import
android.os.Bundle;
09.
10.
public
class
AndroidTutorial
extends
Activity {
11.
AnimatedSprite animation =
new
AnimatedSprite();
12.
13.
/** Called when the activity is first created. */
14.
@Override
15.
public
void
onCreate(Bundle savedInstanceState) {
16.
super
.onCreate(savedInstanceState);
17.
setContentView(
new
AndroidTutorialPanel(
this
));
18.
}
19.
20.
class
AndroidTutorialPanel
extends
DrawablePanel {
21.
22.
public
AndroidTutorialPanel(Context context) {
23.
super
(context);
24.
}
25.
26.
@Override
27.
public
void
onDraw(Canvas canvas) {
28.
super
.onDraw(canvas);
29.
AndroidTutorial.
this
.animation.draw(canvas);
30.
}
31.
32.
@Override
33.
public
void
onInitalize() {
34.
AndroidTutorial.
this
.animation.Initialize(
35.
BitmapFactory.decodeResource(
36.
getResources(),
37.
R.drawable.explosion),
38.
32
,
32
,
14
,
7
);
39.
}
40.
41.
@Override
42.
public
void
onUpdate(
long
gameTime) {
43.
AndroidTutorial.
this
.animation.Update(gameTime);
44.
}
45.
}
46.
47.
}
In this class we create an inner class called AndroidTutorialPanel which extends the DrawablePanel class we looked at above. Since DrawablePanel is an ISurace AndroidTutorialPanel overrides the ISurface members and provides custom implementation. In this case the class creates, updates, and draws an instance of an AnimatedSprite which we looked at previously. One point of interest is the creation of the sprite. The constructor expects a resource id, a resource height and width, and finally some timing values. These values control how many times per second the animation is drawn and how many frames to draw. You can use this to create many animations from a single sprite sheet and customize how fast those animations run.
Conclusion
You can easily extend this framework with your own panels or animation threads even. You can even customize your animation sprites how you see fit such as adding velocity, acceleration, or even a complete physics framework. There is no limit accept your own imagination.
Here is a screen shot of the demo in action:
No comments:
Post a Comment