Apps

The ABCs of Android game development: Animate sprites

In the third installment of his five-part app developer series, William J. Francis walks through how to use both kinds of sprite animation when developing this Android game.

If you've been following along with this series on Android game development, you know we now have a game set and our sprites in place. The next step is to animate our sprites around the game board. (The goal of our game is to adjust the saucer's momentum in order to avoid getting smashed to bits by the asteroid. While simplistic, the project incorporates all the essential aspects of a video game: a canvas, sprites, animation, collision detection, and user input.)

Animation is one of the most important aspects of a video game. Generally there are two kinds of sprite animation: The first type moves the image from one location to the next, while the second type results in the next frame in a sequence of animations being played.

Moving a sprite is easy to conceptualize, because the sprite is physically transported from one place on the screen to another. The second type of animation is a little less straightforward. When you were a kid, did you ever draw a series of doodles on the corner of each page of a notebook, then flip through the pages with your thumb to make the doodle look like it was alive? If so, that is what we are trying to do when we animate the frames within a sprite -- add some life to our game objects.

Some sprites in games use only one type of animation, while some may use both. In our game, we will use the first type of animation for our UFO, and the first and second type of animation for our asteroid. The effect should look like the asteroid is moving and spinning.

Since this is a demo and we want to explain the concept in the most basic terms possible, we won't be implementing any physics or gravity in our game. The ship will move at a constant velocity (for now), while the asteroid will move at a random velocity, which will be initialized whenever the reset button gets pressed. If one of our sprites hits the boundaries of our canvas, we will reverse the x and y, essentially making it "bounce" back into game play (Figure A). Figure A

Illustration of two distinct types of sprite animation.

Animating sprites

This tutorial builds on what we created in part two. 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. Target Android 2.1 or higher. Be sure to rename the startup activity Main.java and the corresponding layout to main.java.

2. While there are no changes in our manifest file or layout since part two, I've included both below for completeness.

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.authorwjf.gamedevtut03"
    android:versionCode="1"
    android:versionName="1.0" >
   <uses-sdk
       android:minSdkVersion="7"
       android:targetSdkVersion="15" />
      <application
          android:icon="@drawable/ic_launcher"
          android:label="@string/app_name"
          android:theme="@style/AppTheme" >
          <activity
              android:name=".Main"
              android:label="@string/title_activity_main"
              android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden">
              <intent-filter>
                  <action android:name="android.intent.action.MAIN" />
                  <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
       </activity>
   </application>
</manifest>

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>

3. Use two images in our /res/drawable folder. These are both shown below.

4. Make a few changes to the GameBoard class, which exists in the package com.authorwjf.drawing. The changes are commented inline below and consist of a few key items. First, I have added new private variables; this is so we can set up boundaries for our images in the Main.java class. Besides adding new getter/setters, I also modified some of the existing accessors to work with primitives instead of Java's Point class; this is to insure our sprites aren't inadvertently passed by reference and manipulated directly in our controller. Doing so could circumvent our synchronization. Finally, check out the on draw override. We are now using a matrix to perform a rotation on the asteroid; this accomplishes our spinning animation quite easily.

GameBoard.java

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

import com.authorwjf.gamedevtut03.R;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
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 Matrix m = null;
       private Bitmap bm2 = null;

       private int sprite1Rotation = 0;

       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(int x, int y) {
              sprite1=new Point(x,y);
       }
      //sprite 1 getter
      synchronized public int getSprite1X() {
            return sprite1.x;
      }

      synchronized public int getSprite1Y() {
            return sprite1.y;
      }
     //sprite 2 setter
     synchronized public void setSprite2(int x, int y) {
            sprite2=new Point(x,y);
     }
     //sprite 2 getter
     synchronized public int getSprite2X() {
           return sprite2.x;
     }

     synchronized public int getSprite2Y() {
           return sprite2.y;
     }

     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);
           //Define a matrix so we can rotate the asteroid
           m = new Matrix();
           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) {
                 m.reset();
                 m.postTranslate((float)(sprite1.x), (float)(sprite1.y));
                 m.postRotate(sprite1Rotation,
(float)(sprite1.x+sprite1Bounds.width()/2.0),
(float)(sprite1.y+sprite1Bounds.width()/2.0));
                     canvas.drawBitmap(bm1, m, null);
                     sprite1Rotation+=5;
                     if (sprite1Rotation >= 360) sprite1Rotation=0;
              }
              if (sprite2.x>=0) {
                     canvas.drawBitmap(bm2, sprite2.x, sprite2.y, null);
              }
      }
}

4. With our game board updating itself, we can move to the /src/Main.java file and set everything in motion. Since part two we've added code to set up our sprite velocity and to calculate the new position with each call back to update the canvas. If it turns out that the new x-y coordinate of a sprite would place it off the screen, we multiply the velocity for that axis by negative one, causing the object to bounce.

Main.java

package com.authorwjf.gamedevtut03;
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();
       //Velocity includes the speed and the direction of our sprite motion
       private Point sprite1Velocity;
       private Point sprite2Velocity;
       private int sprite1MaxX;
       private int sprite1MaxY;
       private int sprite2MaxX;
       private int sprite2MaxY;
       //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 getRandomVelocity() {
              Random r = new Random();
              int min = 1;
              int max = 5;
              int x = r.nextInt(max-min+1)+min;
              int y = r.nextInt(max-min+1)+min;
              return new Point (x,y);
        }

        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.x, p1.y);
        ((GameBoard)findViewById(R.id.the_canvas)).setSprite2(p2.x, p2.y);
        //Give the asteroid a random velocity
        sprite1Velocity = getRandomVelocity();
        //Fix the ship velocity at a constant speed for now
        sprite2Velocity = new Point(1,1);
        //Set our boundaries for the sprites
        sprite1MaxX = findViewById(R.id.the_canvas).getWidth() -
((GameBoard)findViewById(R.id.the_canvas)).getSprite1Width();
        sprite1MaxY = findViewById(R.id.the_canvas).getHeight() -
((GameBoard)findViewById(R.id.the_canvas)).getSprite1Height();
        sprite2MaxX = findViewById(R.id.the_canvas).getWidth() -
((GameBoard)findViewById(R.id.the_canvas)).getSprite2Width();
        sprite2MaxY = findViewById(R.id.the_canvas).getHeight() -
((GameBoard)findViewById(R.id.the_canvas)).getSprite2Height();
        ((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);
            //First get the current positions of both sprites
            Point sprite1 = new Point
(((GameBoard)findViewById(R.id.the_canvas)).getSprite1X(),
                              ((GameBoard)findViewById(R.id.the_canvas)).getSprite1Y()) ;
            Point sprite2 = new Point
(((GameBoard)findViewById(R.id.the_canvas)).getSprite2X(),
                              ((GameBoard)findViewById(R.id.the_canvas)).getSprite2Y());
            //Now calc the new positions.
            //Note if we exceed a boundary the direction of the velocity gets reversed.
            sprite1.x = sprite1.x + sprite1Velocity.x;
            if (sprite1.x > sprite1MaxX || sprite1.x < 5) {
                    sprite1Velocity.x *= -1;
            }
            sprite1.y = sprite1.y + sprite1Velocity.y;
            if (sprite1.y > sprite1MaxY || sprite1.y < 5) {
                   sprite1Velocity.y *= -1;
            }
            sprite2.x = sprite2.x + sprite2Velocity.x;
            if (sprite2.x > sprite2MaxX || sprite2.x < 5) {
                   sprite2Velocity.x *= -1;
            }
            sprite2.y = sprite2.y + sprite2Velocity.y;
            if (sprite2.y > sprite2MaxY || sprite2.y < 5) {
                   sprite2Velocity.y *= -1;
            }
            ((GameBoard)findViewById(R.id.the_canvas)).setSprite1(sprite1.x,
sprite1.y);
           ((GameBoard)findViewById(R.id.the_canvas)).setSprite2(sprite2.x, sprite2.y);
              ((GameBoard)findViewById(R.id.the_canvas)).invalidate();
              frame.postDelayed(frameUpdate, FRAME_RATE);
        }
   };
}

Once again we have our game at a stable point. Better yet, at this juncture, when run on a device or emulator our code is actually starting to look like a video game. Go ahead and see for yourself!

Notice how the asteroid passes right through the ship though? That's a problem. Not to mention it would be nice if you had some control over the saucer and the little alien in the driver's seat. We'll handle both in the final two parts of this game development series, so be sure to check it out.

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...

3 comments
johnbritto473
johnbritto473

This series on android game development is very helpful for learners and for starters in android development. Thanks for starting this discussion!

madcow_uk
madcow_uk

Just found your series and I'm burning my spare time running through them all. Pitched just right for an android beginner. Many thanks, they are very useful. Short question. I can import the eclipse project for the "The ABC’s of Android game development" episode one, just fine. However when I try and import episode 2 and 3, neither appears in the Eclipse "Import projects list". It may well be my lack of experience? (Win 7, Eclipse Indigo) Many thanks

authorwjf
authorwjf

I am glad you are finding the series helpful. Try "File->Import->Android->Existing Android Code Into Workspace". Depending on the version of the ADT and SDK you are using this may solve the issue for you.

Editor's Picks