Android Game Development – Part 1 GameLoop & Sprites

Game Loop

Game Loop is the main part of a game engine, it cycles threw at given intervals updating the game. The Game loop in this section will be fairly simple, it will be a thread that fires off every millisecond.

/**
*
*/
package com.warriormill.warriorengine;
import java.util.concurrent.TimeUnit;
import com.warriormill.warriorengine.drawable.SpriteTile;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
//import android.view.SurfaceHolder;
//import android.view.SurfaceView;
import android.view.View;

/**
* @author maximo guerrero
*
*/
public class GameEngineView extends View {
SpriteTile st;

GameLoop gameloop;
private class GameLoop extends Thread
{
private volatile boolean running=true;
public void run()
{
while(running)
{
try{
TimeUnit.MILLISECONDS.sleep(1);
postInvalidate();
pause();

}
catch(InterruptedException ex)
{
running=false;
}

}

}
public void pause()
{
running=false;
}
public void start()
{
running=true;
run();
}
public void safeStop()
{
running=false;
interrupt();
}

}
public void unload()
{
gameloop.safeStop();

}

public GameEngineView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
init(context);

}
public GameEngineView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}
public GameEngineView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init(context);
}

private void init(Context context)
{
st = new SpriteTile(R.drawable.buster, R.xml.buster, context);
gameloop = new GameLoop();
gameloop.run();

}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
System.out.println("Width " + widthMeasureSpec);
setMeasuredDimension(100, 100);
}

@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
//super.onDraw(canvas);
st.setXpos(15);
st.setYpos(15);

st.draw(canvas);
gameloop.start();

}

}

A couple things to notice. We pause the thread until the screen has finished drawing, the reason this is done is that the thread will continue to fire off and the onDraw() function will never be called. Also Since we are using a generic thread and not a UI-Thread we call postInvalidate() function to let android know that our view has to be re drawn.

The Current Game loop is really simple it will call draw on the items that implement the Drawable Interface.

Sprite Tile Class

The Sprite Tile class will be an object that extends the Drawable interface in the android api. Our Sprite will be composed of two files,  a bitmap file (image of format type jpg, png, gif or bmp) and an xml file that describes its behavior.

Sprite sheet ( check out http://www.retrogamezone.co.uk for sample sprites):

Sprite of Buster Bunny

The sprite sheet is one image with all possible animaitions for our sprite

Xml Description:

<?xml version="1.0" encoding="utf-8"?>
<animations>
<animation name="idle" canLoop="true" >
<framerect top="4" left="85" bottom="58" right="115" delayNextFrame="10" />
<framerect top="4" left="122" bottom="58" right="153" delayNextFrame="10" />
<framerect top="4" left="161" bottom="58" right="190" delayNextFrame="5" />
<framerect top="4" left="199" bottom="58" right="228" delayNextFrame="5" />
<framerect top="4" left="237" bottom="58" right="268" delayNextFrame="5" />
<framerect top="4" left="276" bottom="58" right="307" delayNextFrame="5" />
<framerect top="4" left="85" bottom="58" right="115" delayNextFrame="60" />

<collisionrect top="10" left="5" bottom="40" right="20" />
</animation>
</animations>

The xml description file contains the information needed to animation the sprite sheet. All of the Elements and there Attributes will have respective fields in our sprite class. Note that the frame rectangle is not equally uniform, this allows to compose an animation where the tile thats being drawn doesn’t have to be the same size. It also tells us how much time each frame will last on the screen, along with a rectangle for collision for that specific animation.

Sprite Class:
The following class will load, draw and animate the tilesheet.

package com.warriormill.warriorengine.drawable;

import java.util.ArrayList;
import java.util.Hashtable;

import org.xmlpull.v1.XmlPullParser;

import android.content.Context;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;

/**
* @author maximo guerrero
*/
public class SpriteTile extends Drawable {
private Bitmap tileSheet; //sprite tile sheet for all animations. rectangles are used to slip and only show parts of one bitmap
private Hashtable animations; //all animation sequences for this sprite
private String currentAnimation="idle"; //current animation sequence
private int currentFrame=0; //current frame being played
private int xpos=100; // x position
private int ypos=100; // y position
private int waitDelay=0; // delay before the next frame

private ColorFilter cf;

// Class contains Information about one frame
private class FrameInfo
{
public Rect rect = new Rect();
public int nextFrameDelay =0;
}
//Class encapsulates all the data for an animations sequence. List for frames, animcation name, if the sequence will loop and collission info
private class AnimationSequece
{
public ArrayList sequence;
public Rect collisionRect;
public boolean canLoop =false;
@SuppressWarnings("unused")
public String name="idle";
}

//takes resource ids for bitmaps and xmlfiles
public SpriteTile(int BitmapResourceId, int XmlAnimationResourceId, Context context)
{
loadSprite(BitmapResourceId,XmlAnimationResourceId,context);
}

//load bitmap and xml data
public void loadSprite(int spriteid, int xmlid, Context context) {
tileSheet = BitmapFactory.decodeResource(context.getResources(), spriteid);
//load the xml will all the frame animations into a hashtable
XmlResourceParser xpp= context.getResources().getXml(xmlid);

animations= new Hashtable();

try
{
int eventType = xpp.getEventType();
String animationname="";
AnimationSequece animationsequence = new AnimationSequece();
while (eventType != XmlPullParser.END_DOCUMENT){

if(eventType == XmlPullParser.START_DOCUMENT) {
System.out.println("Start document");

} else if(eventType == XmlPullParser.END_DOCUMENT) {
System.out.println("End document");

} else if(eventType == XmlPullParser.START_TAG) {
System.out.println("Start tag "+xpp.getName());
if(xpp.getName().toLowerCase().equals("animation"))
{
animationname=xpp.getAttributeValue(null, "name");
animationsequence = new AnimationSequece();
animationsequence.name=animationname;
animationsequence.sequence=new ArrayList();
animationsequence.canLoop = xpp.getAttributeBooleanValue(null,"canLoop", false);
}
else if(xpp.getName().toLowerCase().equals("framerect"))
{
FrameInfo frameinfo = new FrameInfo();
Rect frame = new Rect();
frame.top = xpp.getAttributeIntValue(null, "top", 0);
frame.bottom = xpp.getAttributeIntValue(null, "bottom", 0);
frame.left = xpp.getAttributeIntValue(null, "left", 0);
frame.right = xpp.getAttributeIntValue(null, "right", 0);
frameinfo.rect = frame;
frameinfo.nextFrameDelay = xpp.getAttributeIntValue(null,"delayNextFrame", 0);
animationsequence.sequence.add(frameinfo);
}
else if(xpp.getName().toLowerCase().equals("collisionrect"))
{
Rect colrect = new Rect();
colrect.top = xpp.getAttributeIntValue(null, "top", 0);
colrect.bottom = xpp.getAttributeIntValue(null, "bottom", 0);
colrect.left = xpp.getAttributeIntValue(null, "left", 0);
colrect.right = xpp.getAttributeIntValue(null, "right", 0);
animationsequence.collisionRect=colrect;
}
}else if(eventType == XmlPullParser.END_TAG) {
if(xpp.getName().toLowerCase().equals("animation"))
{
animations.put(animationname, animationsequence);
}
} else if(eventType == XmlPullParser.TEXT) {
System.out.println("Text "+xpp.getText());

}
eventType = xpp.next();
}
}
catch (Exception e) {
Log.e("ERROR", "ERROR IN SPRITE TILE CODE:"+e.toString());
}
System.out.println("Sprite Loaded ");
}
//Draw sprite onto screen
@Override
public void draw(Canvas canvas) {
try
{
FrameInfo frameinfo= animations.get(currentAnimation).sequence.get(currentFrame);
Rect rclip = frameinfo.rect;
Rect dest = new Rect(this.getXpos(), getYpos(), getXpos() + (rclip.right - rclip.left),
getYpos() + (rclip.bottom - rclip.top));
if(cf!=null)
{
//color filter code here

}
canvas.drawBitmap(tileSheet, rclip, dest, null);
update(); //after drawing update the frame counter
}
catch (Exception e)
{
Log.e("ERROR", "ERROR IN SPRITE TILE CODE:"+e.toString()+e.getStackTrace().toString());
}

}

@Override
public int getOpacity() {
// TODO Auto-generated method stub
return 100;
}

@Override
public void setAlpha(int alpha) {
// TODO Auto-generated method stub

}

@Override
public void setColorFilter(ColorFilter cf) {
// TODO Auto-generated method stub
this.cf = cf;
}

//updates the frame counter to the next frame
public void update()
{
if(waitDelay==0)//if done waiting
{
//set current frame back to the first because looping is possible
if(animations.get(currentAnimation).canLoop &amp;&amp; currentFrame == animations.get(currentAnimation).sequence.size()-1)
currentFrame=0;
else
{
currentFrame++; //go to next frame

FrameInfo frameinfo= animations.get(currentAnimation).sequence.get(currentFrame);
waitDelay = frameinfo.nextFrameDelay; //set delaytime for the next frame
}
}
else
{
waitDelay--; //wait for delay to expire
}

}
//has this sprite collided with a rect
public boolean hasCollided(Rect rect)
{
AnimationSequece as = animations.get(currentAnimation);
if( rect.right &lt; as.collisionRect.left )
return false;
if( rect.left &gt; as.collisionRect.right )
return false;

if( rect.top &gt; as.collisionRect.bottom )
return false;
if( rect.bottom &lt; as.collisionRect.top )
return false;

return true;
}
//has animation finished playing, returns true on a animaiton that can loop for ever
public boolean hasAnimationFinished()
{
AnimationSequece as = animations.get(currentAnimation);
if(currentFrame == as.sequence.size() -1 &amp;&amp; !as.canLoop )
return true;

return false;
}

public void setXpos(int xpos) {
this.xpos = xpos;
}

public int getXpos() {
return xpos;
}

public void setYpos(int ypos) {
this.ypos = ypos;
}

public int getYpos() {
return ypos;
}
}

The class also implements a couple useful functions for checking the animations current state. Also note that the constructor take Resource-ID’s and not file paths. This is so that the developer has the choice of passing in images that have been compressed by the android api or raw images.

Final product:
Android Game Development part 1 - Final Product

That’s it for part 1.

Source Code

29 comments

  1. I’m having issues with the xml description file – when running this code in the emulator, I get buster being cut in half and moved around etc.

  2. WIthout altering the description file – I was getting the rabbit cut in half. How do you determine what the coord’s should be?

    Also, in update – after currentFrame =0; it would be nice to set the waitDelay, so the 1st frame is on screen for more than a flash.

  3. Thanks for a very usefull tutorial. But I have a question how can you create file xml from the sprite image file?

  4. Sorry, I didn’t read last reply of you :d.
    Now I knew how you created the xml file. But can you show me a wait to generate xml file automatically from the sprite file? Thank you very much

  5. The only way to have a file generated form the image is if all the frames where same size and equally spaced…It would be nice to have a photoshop plugin that exports slices in the proper markup but i don’t have the time to write it

  6. Hi I know this is a old post but i was wondering if i could get some help.
    For some reason Eclipse isnt noticing the sequence or get(currentAnimation)

    e.g.
    FrameInfo frameinfo = animations.get(currentAnimation).sequence.get(currentFrame);
    and
    AnimationSequece as = animations.get(currentAnimation);
    are both coming up with errors, any idea why this could be?

  7. I’m having that same problem.

    For the line:
    FrameInfo frameinfo = animations.get(currentAnimation).sequence.get(currentFrame);
    It says that sequence cannot be resolved or is not a field.

    It also says the same about canLoop in the line:
    if(animations.get(currentAnimation).canLoop && currentFrame == animations.get(currentAnimation).sequence.size()-1)
    currentFrame=0;

    And for AnimationSequece as = animations.get(currentAnimation);
    it says that it can’t convert from Object to SpriteTile.AnimationSequece.

  8. Alright. I’ll try it next time I get the chance and let you know whether or not I get the same problem. Thanks!

  9. hi..
    nice tutorial for android development
    I’m still trying to understand it XD
    i have a question
    in this code

    if(cf!=null)
    {
    //color filter code here
    }

    how we insert color filter ??
    I’m already search at google but I can’t find it –a
    thx b4 ^^v

  10. Hello, your tutorial are so great, i use your tutorial on my personal proyect and i found were is the problem for android sdkś 1.5++ :
    Here i put the code that i change and use:

    –Your code:
    public void loadSprite(int spriteid, int xmlid, Context context) {
    tileSheet = BitmapFactory.decodeResource(context.getResources(), spriteid);
    //load the xml will all the frame animations into a hashtable
    XmlResourceParser xpp= context.getResources().getXml(xmlid);// this is the last line, you dont modify nothing after this

    –My code:
    public void loadSprite(int spriteid, int xmlid, Context context) {
    sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
    InputStream is = context.getResources().openRawResource(spriteid);
    try {
    tileSheet = BitmapFactory.decodeStream(is, null, sBitmapOptions);
    }finally {try {
    is.close();
    } catch (IOException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
    }}
    //load the xml will all the frame animations into a hashtable
    XmlResourceParser xpp= context.getResources().getXml(xmlid);// after this all are =

    Sorry for my english, im are spanish.
    I hope this can help you, at you or some other = that help me.
    So thanks for your tutorial.
    You are so good.

  11. Hi, The problem with the cut-in-half has to do with the picture i think

    if you replace the code
    tileSheet = BitmapFactory.decodeResource(context.getResources(), spriteid );

    with:
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inDensity = 1;
    options.inTargetDensity = 1;
    tileSheet = BitmapFactory.decodeResource(context.getResources(), spriteid, options);

    you instruct that it should be interpreted as 1 pixel density. and the idle animation appears!

    good luck

    Arjan

  12. hi,
    thanks for this post.
    I am using this for my app and have put onClick listner to start animation. but when It takes 2 to 3 sec to start animation.
    how can I solve this prob.?
    I hve made my own sprite image which contains more thn 100 images…

  13. some detailed explanation..

    When I click on image to start animation…it takes 2 to 3 sec to start..
    it takes this time for first time at canvas.drawBitmap..
    how can I solve this??

  14. Thanks to arjan franzen……
    I ws getting problem before I replace my code as per your instruction..

    Hello frnds, If you use this code then it will work proper for mdpi devices, but for getting same result in ldpi and hdpi, you should just replce 1 line with 4 lines as per arjan franzen told.
    then it will work fine…
    Thanks to maximo also to share it….

  15. hi, we have created an app for android devices, its called: SpriteTester.
    we have developed the tool, and it´s free, you can find it in google play.
    You can visualize your sprites and improve them by loading, selecting parameters as columns, rows, speed, frame to frame and color background.
    Hope it is useful for indie developers 😀
    Sry about my english and any comments plz write me here. thanks

  16. I tried Arjan’s method and unfortunately it didn’t work.

    However if you go into the Manifest and change your min sdk to this:

    it should work

Leave a Reply

Your email address will not be published. Required fields are marked *