Thursday 14 July 2011

Quick and Dirty Android Game Engine

Hi guys! This is my first post of my first blog! :D And it will be about making a quick and dirty game framework from scratch. It took me months to research about android and making games on it (There were very few tutorials for game development on android and there are fewer game engines available for it). You can download the file that we will be making here.

Let's start by creating a new android project. Supply the following information when prompted on creating a new android project:

  • Project Name: Dirty Game
  • Build Target: Android 3.1
  • Application Name: Dirty Game
  • Package Name: ind.crys.dirtyframework (or any package name of you choice)
  • Create Activity: DirtyActivity
  • Min SDK Version: 3

Eclipse will now display a ready to use activity naming DirtyActivity. Lets start by implementing an OnTouchListener and override the onTouch() method.

public class DirtyActivity 
extends Activity 
implements OnTouchListener
{

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

@Override
public boolean onTouch(View view, MotionEvent event) 
{
}
}


Next stop, we will need to make a way to hold some important values every time the players of our game will touch the screen and use those values on our game loop for further processing. We will be making a class inside the dirty activity naming "TouchEvent" with fields naming x, y, and eventType all with a type of int. The x and y fields are the X and Y coordinates of the touch event. The eventType is the type of action made by the user in the screen. We will then add 3 integer constants in this class naming TOUCH_DOWN, TOUCH_MOVE, and TOUCH_UP with values 0, 1, 2 respectively. This constants will be used in assigning the value of the eventType field (we cant use the MotionEvent.ACTION_XX constants because there are many of them, some can be confused with the other. Keeping things simple :D).

class TouchEvent{
int x;
int y;
int eventType;
public static final int TOUCH_DOWN = 0;
public static final int TOUCH_MOVE = 1;
public static final int TOUCH_UP = 2;
/**
* @return the x
*/
public int getX() {
return x;
}
/**
* @param x the x to set
*/
public void setX(int x) {
this.x = x;
}
/**
* @return the y
*/
public int getY() {
return y;
}
/**
* @param y the y to set
*/
public void setY(int y) {
this.y = y;
}
/**
* @return the eventType
*/
public int getEventType() {
return eventType;
}
/**
* @param eventType the eventType to set
*/
public void setEventType(int eventType) {
this.eventType = eventType;
}
public TouchEvent() {
this.x = 0;
this.y = 0;
this.eventType = TOUCH_DOWN;
}

public TouchEvent(int x, int y, int eventType) {
this.x = x;
this.y = y;
this.eventType = eventType;
}
}

We will add a field in our DirtyActivity class naming "lastTouchEvent" with the type TouchEvent. We will use this to get the touch input values and use it in our game loop. There are many other techniques on getting touch events (one of which is called Pooling) but lets keep this game engine simple. Every time the players of our game does something to the screen (touching it, swiping it, drooling on it) we will save the values of that event on the lastTouchEvent variable. Add to fields in our DirtyActivity naming scaleX and scaleY with the type float. We use this later on (particular to drawing things in the screen). Copy the line of code below replacing the old onTouch() method that we had last:

public boolean onTouch(View view, MotionEvent event) {
lastTouchEvent = new TouchEvent();
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
lastTouchEvent.setEventType(TouchEvent.TOUCH_DOWN);
break;
case MotionEvent.ACTION_MOVE:
lastTouchEvent.setEventType(TouchEvent.TOUCH_MOVE);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
lastTouchEvent.setEventType(TouchEvent.TOUCH_UP);
break;
}



int newX = (int)(event.getX() * scaleX);
int newY = (int)(event.getY() * scaleY);
newX = (newX == 0) ? (int)event.getX() : newX;
newY = (newY == 0) ? (int)event.getY() : newY;
lastTouchEvent.setX(newX);
lastTouchEvent.setY(newY);
return true;
}

The code above simply creates a new TouchEvent object and assign it to the lastTouchEvent variable. As you may have noticed, we assigned the X and Y coordinates of our touch event multiplied by scaleX/scaleY variable. It is because we will be drawing on a fixed sized canvas and because not all handsets (or even tablets) have the same screen size. We will then scale our canvas with the size of the handset. The scaleX and scaleY fields are the ratio of the handset's screen size and our canvas' size. By the time users touches the screen, the X and Y coordinates of the touch event will be in the exact position as that of the canvas, not the handsets screen. Later on, we will assign the scaleX/scaleY fields. 

Next thing to do is to create a way to draw things in the screen. We will be extending the SurfaceView class and use it for drawing graphics (By the way, SurfaceView is a special view that we can use to make it draw on another thread). We will name this class as FastRenderView and it will inherit SurfaceView and implements Runnable interface. Copy the code below:

class FastRenderView
extends SurfaceView
implements Runnable{
Thread renderThread;
SurfaceHolder holder;
volatile boolean isRunning = false;
public FastRenderView(Context context) {
super(context);
holder = this.getHolder();
}
//Methods
public void resume(){
isRunning = true;
renderThread = new Thread(this);
renderThread.start();
}
public void pause(){
isRunning = false;
while(true){
try{
renderThread.join();
break;
}
catch(InterruptedException e){
}
}
}
//Event
@Override
public void run() {
Rect dstRect = new Rect();
long startTime = System.nanoTime();
while(isRunning){
if(!holder.getSurface().isValid())
continue;
float deltaTime = (System.nanoTime() - startTime) / 1000000000.0f;
startTime = System.nanoTime();
DirtyActivity.this.update(deltaTime);
DirtyActivity.this.draw(deltaTime);
Canvas canvas = holder.lockCanvas();
canvas.getClipBounds(dstRect);
canvas.drawBitmap(DirtyActivity.this.frameBuffer, null, dstRect, null);
holder.unlockCanvasAndPost(canvas);
}
}
}

We will use FastRenderView class as our game loop. On the run() method of this class, you will find that it calls 2 methods of our DirtyActivity naming update() and draw() both of which accepts a float value representing the delta time. We will be using these methods to update game logic and to draw objects in our game. The FastRenderView class also uses a Bitmap that is found in our DirtyActivity naming frameBuffer. The frameBuffer field will be our fixed sized canvas. The 2 methods of our FastRenderView class, naming pause and resume, are used when our activity is paused or resumed (pretty self explanatory isn't it?). 

Were almost there! Copy the code before and place it in our DirtyActivity class:

// Fields
FastRenderView view;
TouchEvent lastTouchEvent;
Bitmap frameBuffer;
Canvas graphics;
Paint centralPainter;
float scaleX;
float scaleY;
WakeLock wakeLock;

public static int PREFFERED_WIDTH = 320;
public static int PREFFERED_HEIGHT = 480;

//Methods

public void initialize(){
}

public void update(float deltaTime){
lastTouchEvent = null;
}

public void draw(float deltaTime){
}

/**
 * @return the lastTouchEvent
 */
public TouchEvent getLastTouchEvent() {
return lastTouchEvent;
}

/**
 * @return the graphics
 */
public Canvas getGraphics() {
return graphics;
}

/**
 * @return the centralPainter
 */
public Paint getCentralPainter() {
return centralPainter;
}

The view field is the instance of our FastRenderView that we will use. The frameBuffer field is our fixed sized canvas that we used to draw in our FastRenderView. The graphics field is the instance of our Canvas class that we will use to draw things in our game loop. The centralPainter field is an instance of Paint class that we will use on our graphics field (I guess 1 Paint class is enough, but you can make more instances of Paint class if you want to :D). We then declared a field named wakeLock with a type of WakeLock. We will use this for the heck of not turning off the lights of our screens while our users play our games. The methods below are getters that will be used to on our game engine (We will use getLastTouchEvent() to get the touch event values and use it in our update() method to further process. getGraphics() and getCentralPainter() methods will be used in our draw() method). PREFFERED_WIDTH and PREFFERED_HEIGHT is the width and height of our frameBuffer. Our game will be drawn out of these dimensions, scaled to the screen of our player's handsets. So if your game idea needs it to be in portrait orientation, change their values. Or if your game needs to be with 1024X768 screen dimensions (Wow!), change their values. We can change this the way you want your canvas will be drawn.

Copy the code below to finalize everything.

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "wakelock");
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
frameBuffer = Bitmap.createBitmap(PREFFERED_WIDTH, PREFFERED_HEIGHT, Config.RGB_565);
graphics = new Canvas(frameBuffer);
centralPainter = new Paint();
view = new FastRenderView(this);
view.setOnTouchListener(this);
scaleX = getWindowManager().getDefaultDisplay().getWidth() / PREFFERED_WIDTH;
scaleY = getWindowManager().getDefaultDisplay().getHeight() / PREFFERED_HEIGHT;
setContentView(view);
initialize();
}

@Override
protected void onPause() {
wakeLock.release();
view.pause();
super.onPause();
}

@Override
protected void onResume() {
wakeLock.acquire();
view.resume();
super.onResume();
}

Most things here are android specific. PowerManager provides us the instance of our wakeLock field. Then we made our game full screen by calling requestWindowFeature() method and getWindow().setFlags() method. We then initialize our frameBuffer, graphics, and centralPainter fields. We then initialize our view field set our activity's view with this FastRenderView instance. 

Thats it! To finalize everything, create an activity that extends our DirtyActivity class and override initialize(), update(), and draw() methods. Set this activity as your main activity in your Manifest file. Also set the screen orientation as portrait (or landscape, as long as it corresponds to your PREFFERED_WIDTH/PREFFERED_HEIGHT. You should use fixed orientation to not to confuse the players of our game)

Lets try drawing something:

@Override
public void draw(float deltaTime) {
getGraphics().drawRGB(100, 149, 237);

super.draw(deltaTime);
}

This will color the screen to my favorite color "Cornflower blue" (this color is the default background color of games made in XNA, a game framework for PC and XBOX using the language C#). You can do more in drawing with canvas like drawing lines, shapes, images (perfect for games) and many more. Your imagination is the limit. You can get some info in the official android Canvas documentation.

Lets try processing input in our update() method:

@Override
public void update(float deltaTime) {
TouchEvent touchEvent = getLastTouchEvent();
if(touchEvent != null){
Log.i("Touch Event", touchEvent.toString());
}
super.update(deltaTime);
}

Everytime a touch event is made, LogCat will display the values of our TouchEvent. There's not much we can do in processing input because were limited with one TouchEvent and its not even supporting multi-touch functionality. Nevertheless, many games are still great with such limitations (Imagine all the games with point and click or point and drag input. Already imagined something?).

This game engine does not do much, unlike other free/commercial android game engines out there. But I believe that making a great game or app is not about what engine we use. Its all in our ideas as a developer. 

Happy coding! :D


2 comments:

  1. The code that I downloaded worked well when I draw using the getGraphics() method.

    But the touch input doesn't work!!!

    ReplyDelete
  2. I just updated the post about a few days ago. try looking at the code above. I think the code in the link is wrong. I'll change that.

    ReplyDelete

Chitika