Apps

The ABCs of Android game development: Load and display sprites

In the second installment of his five-part app developer series, William J. Francis instructs you on how to load and display sprites when developing an Android game.

The goal of the game we're developing in this Android app development series is to adjust the flying saucer's momentum in order to avoid getting smashed to bits by the asteroid. This project incorporates all the essential aspects of a video game: a canvas, sprites, animation, collision detection, and user input.

In part one, we created a drawing surface and a framework for controlling and updating the contents of that canvas. We painted our space scape, smattered it with random stars, and even made those stars pulse. The next logical step in building our game is the loading and displaying of sprites. In this context, a sprite is "a computer graphic which may be moved on-screen and otherwise manipulated as a single entity."

In the documentary Indie Gamer, one of the developers describes the process of making a video game much like that of making a movie on a mini-scale. If you think about the process from this perspective, in part one we built our stage or set. Sprites are the actors we put into a scene and direct them to do something interesting. For our purposes, we will be using just two sprites: a UFO (Figure A) and an asteroid (Figure B). Figure A

Figure B

Note: The images above appear to be part of the public domain. If you are the artist and know differently, please contact me, and I will be happy to give you an attribution.

Loading and displaying sprites

This tutorial builds on what we created in part one. However, to really understand what is going on, it helps to see the code in the context of the whole. Therefore, the code listing in the tutorial will be our complete working base, with the new code commented inline. You can follow along with the step-by-step instructions or download and import the entire project into Eclipse.

1. Create a new Android project in Eclipse. Be sure to target Android 2.1 or higher. Rename the startup activity to Main.java and the corresponding layout definition to main.xml.

2. Modify the AndroidManifest.xml file to override the default screen orientation handling.

AndroidMainfest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.authorwjf.gamedevtut02"

android:versionCode="1"

android:versionName="1.0" >

<uses-sdk

android:minSdkVersion="8"

android:targetSdkVersion="15" />

<application

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:screenOrientation="portrait"

android:configChanges="orientation|keyboardHidden"

android:name=".Main"

android:label="@string/title_activity_main" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

3. Although no changes have been made to our /res/layout/main.xml definition since part one, there it is again for the sake of completeness.

main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:orientation="vertical" >

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="top|center"

android:text="ABC's of Android Game Dev" />

<Button

android:id="@+id/the_button"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:gravity="center"

android:enabled="false"

android:text="Reset" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:text="Sprite Speed (?,?)"

android:id="@+id/the_label" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:text="Last Collision XY (?,?)"

android:id="@+id/the_other_label" />

<com.authorwjf.drawing.GameBoard

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:layout_margin="20dip"

android:id="@+id/the_canvas"/>

</LinearLayout>

4. Now we need to create a /res/drawable folder. Here is where we will place the images: ufo.png and asteroid.png. While I use a single drawable folder in this demo code, if you are releasing a game to the market, you will want to create multiple versions of these images at different resolutions and put them in the ldpi, mdpi, hdpi, and xhdpi folders as appropriate. If this is your first time dealing with Android resources, I highly recommend reading Google's documentation on supporting multiple screens.

5. Let's look at the changes we are making to our /src/GameBoard.Java class. Remember from part one, this class exists in its own package: com.authorwjf.drawing.

First, we need to add private variables to keep track of our sprite positions. Next, we will expose those properties to the controller (Main.java) using a series of getters and setters. Then, in our constructor we will load the bitmap images that represent our sprite. Finally, in the on draw loop we will paint the sprites onto the canvas. The sprites need to be drawn last so the background doesn't hide them.

Below is the entire source listing for the GameBoard class, with the code specific to this tutorial highlighted.

GameBoard.java

package com.authorwjf.drawing;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import com.authorwjf.gamedevtut02.R;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class GameBoard extends View{

private Paint p;
       private List<Point> starField = null;
       private int starAlpha = 80;
       private int starFade = 2;

//Add private variables to keep up with sprite position and size

private Rect sprite1Bounds = new Rect(0,0,0,0); private Rect sprite2Bounds = new Rect(0,0,0,0); private Point sprite1; private Point sprite2;

//Bitmaps that hold the actual sprite images

private Bitmap bm1 = null; private Bitmap bm2 = null; private static final int NUM_OF_STARS = 25;

//Allow our controller to get and set the sprite positions

//sprite 1 setter

synchronized public void setSprite1(Point p) {

sprite1=p;

}

//sprite 1 getter

synchronized public Point getSprite1() { return sprite1;

}

//sprite 2 setter

synchronized public void setSprite2(Point p) {

sprite2=p;

}

//sprite 2 getter

synchronized public Point getSprite2() { return sprite2;

}

synchronized public void resetStarField() { starField = null;

}

//expose sprite bounds to controller

synchronized public int getSprite1Width() { return sprite1Bounds.width();

}

synchronized public int getSprite1Height() { return sprite1Bounds.height();

}

synchronized public int getSprite2Width() { return sprite2Bounds.width();

}

synchronized public int getSprite2Height() { return sprite2Bounds.height();

}

public GameBoard(Context context, AttributeSet aSet) { super(context, aSet); p = new Paint();

//load our bitmaps and set the bounds for the controller

sprite1 = new Point(-1,-1); sprite2 = new Point(-1,-1); p = new Paint();

bm1 = BitmapFactory.decodeResource(getResources(), R.drawable.asteroid);

bm2 = BitmapFactory.decodeResource(getResources(), R.drawable.ufo);

sprite1Bounds = new Rect(0,0, bm1.getWidth(), bm1.getHeight()); sprite2Bounds = new Rect(0,0, bm2.getWidth(), bm2.getHeight());

}

private void initializeStars(int maxX, int maxY) { starField = new ArrayList<Point>(); for (int i=0; i<NUM_OF_STARS; i++) { Random r = new Random(); int x = r.nextInt(maxX-5+1)+5; int y = r.nextInt(maxY-5+1)+5; starField.add(new Point (x,y));

}

}

@Override

synchronized public void onDraw(Canvas canvas) {

p.setColor(Color.BLACK);

p.setAlpha(255);

p.setStrokeWidth(1);

canvas.drawRect(0, 0, getWidth(), getHeight(), p);

if (starField==null) {

initializeStars(canvas.getWidth(), canvas.getHeight());

}

p.setColor(Color.CYAN);

p.setAlpha(starAlpha+=starFade);

if (starAlpha>=252 || starAlpha <=80) starFade=starFade*-1;

p.setStrokeWidth(5);

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

canvas.drawPoint(starField.get(i).x, starField.get(i).y, p);

}

//Now we draw our sprites.  Items drawn in this function are stacked.

//The items drawn at the top of the loop are on the bottom of the z-order.

//Therefore we draw our set, then our actors, and finally any fx. if (sprite1.x>=0) { canvas.drawBitmap(bm1, sprite1.x, sprite1.y, null);

}

if (sprite2.x>=0) { canvas.drawBitmap(bm2, sprite2.x, sprite2.y, null);

}

}

}

6. Modify Main.java to assign starting x and y coordinates to our sprites. The method getRandomPoint() returns a random x and y that falls within the bounds of the canvas. We then use a while loop to make sure that the initial placement of our sprites doesn't overlap.

Main.java

package com.authorwjf.gamedevtut02;
import java.util.Random;
import com.authorwjf.drawing.GameBoard;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.app.Activity;
import android.graphics.Point;

public class Main extends Activity implements OnClickListener{

private Handler frame = new Handler();

//Divide the frame by 1000 to calculate how many times per second the screen will update.

private static final int FRAME_RATE = 20; //50 frames per second

@Override

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

setContentView(R.layout.main);

Handler h = new Handler(); ((Button)findViewById(R.id.the_button)).setOnClickListener(this);

//We can't initialize the graphics immediately because the layout manager

//needs to run first, thus call back in a sec. h.postDelayed(new Runnable() {

@Override

public void run() {

initGfx();

}

}, 1000);

}

private Point getRandomPoint() { Random r = new Random(); int minX = 0; int maxX = findViewById(R.id.the_canvas).getWidth() - ((GameBoard)findViewById(R.id.the_canvas)).getSprite1Width(); int x = 0; int minY = 0; int maxY = findViewById(R.id.the_canvas).getHeight() - ((GameBoard)findViewById(R.id.the_canvas)).getSprite1Height(); int y = 0;

x = r.nextInt(maxX-minX+1)+minX;

y = r.nextInt(maxY-minY+1)+minY;

return new Point (x,y);

}

synchronized public void initGfx() {

((GameBoard)findViewById(R.id.the_canvas)).resetStarField();

//Select two random points for our initial sprite placement.

//The loop is just to make sure we don't accidentally pick

//two points that overlap.

Point p1, p2;

do {

p1 = getRandomPoint();

p2 = getRandomPoint();

} while (Math.abs(p1.x - p2.x) <

((GameBoard)findViewById(R.id.the_canvas)).getSprite1Width());

((GameBoard)findViewById(R.id.the_canvas)).setSprite1(p1);

((GameBoard)findViewById(R.id.the_canvas)).setSprite2(p2);

((Button)findViewById(R.id.the_button)).setEnabled(true);

frame.removeCallbacks(frameUpdate);

frame.postDelayed(frameUpdate, FRAME_RATE);

}

@Override

synchronized public void onClick(View v) {

initGfx();

}

private Runnable frameUpdate = new Runnable() {

@Override

synchronized public void run() {

frame.removeCallbacks(frameUpdate);

//make any updates to on screen objects here

//then invoke the on draw by invalidating the canvas

((GameBoard)findViewById(R.id.the_canvas)).invalidate();

frame.postDelayed(frameUpdate, FRAME_RATE);

}

};

}

Our game code has reached a good stopping point. You can load it to your device or emulator. This time when you press the reset button, our actors will take their places.

Stay tuned for part three of this tutorial, where the director will shout "action," and we will begin animating the game objects.

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

6 comments
derandru
derandru

is anyone else having trouble opening part one?

nomorewine
nomorewine

Great tutorials. :) Of all the tutorials on android on the internet, I find yours the best. It's is really easy to understand even for a newbie like me, I guess teaching your son has something to do with it ;) In the GameBoard constructor you have initialized p = new Paint(); twice, could you please help me with that part? I was unable to find the answer on google, Thank you :)

jpbaugh
jpbaugh

I don't find the Code formatting that difficult to deal with. I keep MS Wordpad open, and paste it into there, and then do another Select All and copy it from there, THEN into Eclipse, and it has newlines in it. I spruce it up a little with some whitespace, but it's good to go. :)

os2baba
os2baba

It would be nice, if possible and not too much trouble, if the lines of code that were changed or added had a different color. The highlighting is a little difficult to spot. I'm an experienced Java developer and somewhat experienced Android developer but have never written any games. My kids are getting to an age where they want to learn programming and of course they want to write games! So this is a great series for me. Thanks!

authorwjf
authorwjf

Thanks for tuning in. I apologize about the code formatting. I was told by the person who handles the CMS color-coding the new lines isn't really possible at this time. I hope you will still see the series through. I have a fourteen-year-old son and he and I had a great time last year developing a game together and releasing it to the Android market. It was fun to see him and his friends excited about the entire process. Not just the coding but as we progressed he took an interest in the number of downloads, user reviews, and even in-app advertising!

os2baba
os2baba

Ah. Too bad about the color coding. But the code (so far) is small so it's not too difficult to keep up with the changes. I'm checking back daily to see if there is a new blog update on the series :-) My kid is 9 years old and not yet ready for coding in Java. Am going to start teaching him OO and maybe Java this summer. But he has started programming in App Inventor and I think I should be able to translate this into an App Inventor app.

Editor's Picks