Friday, May 16, 2014

Game Loop and Animation - Android Programming

In this tutorial we are going to see the "Game Loop" concept and a little about animations techniques.

The game loop is the repetition of two main activities:
  1. Physics update; this is the game data update as for example the x and y position coordinates for a little character (Sprites positions, Score base in time, ...)
  2. Drawing; this is about drawing the picture you see in the screen. When this method is called repeatedly it gives you the perception of a movie or of an animation.
We are going to execute the game loop in a separated thread. In one thread we call the updates and drawings and in the main thread we handle the events like we use to do in a normal application. The code below shows this implementation:
package com.edu4java.android.killthemall;
import android.graphics.Canvas;
 
public class GameLoopThread extends Thread {
       private GameView view;
       private boolean running = false;
      
       public GameLoopThread(GameView view) {
             this.view = view;
       }
 
       public void setRunning(boolean run) {
             running = run;
       }
 
       @Override
       public void run() {
             while (running) {
                    Canvas c = null;
                    try {
                           c = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(c);
                           }
                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(c);
                           }
                    }
             }
       }
}
The running field is a flag that makes the game loop stop. Inside the loop we call the onDraw method as we learned in the last tutorial. In this case for simplicity we make the update and drawing activities in the onDraw method. We use synchronise to avoid some other thread to make conflict when we are drawing. In the SurfaceView we just add an int x field to maintain the x coordinate to draw the image in the onDraw method. In addition in the onDraw method we increment x position if it hasn't reached the right border.
package com.edu4java.android.killthemall;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
public class GameView extends SurfaceView {
       private Bitmap bmp;
       private SurfaceHolder holder;
       private GameLoopThread gameLoopThread;
       private int x = 0; 
      
       public GameView(Context context) {
             super(context);
             gameLoopThread = new GameLoopThread(this);
             holder = getHolder();
             holder.addCallback(new SurfaceHolder.Callback() {
 
                    @Override
                    public void surfaceDestroyed(SurfaceHolder holder) {
                           boolean retry = true;
                           gameLoopThread.setRunning(false);
                           while (retry) {
                                  try {
                                        gameLoopThread.join();
                                        retry = false;
                                  } catch (InterruptedException e) {
                                  }
                           }
                    }
 
                    @Override
                    public void surfaceCreated(SurfaceHolder holder) {
                           gameLoopThread.setRunning(true);
                           gameLoopThread.start();
                    }
 
                    @Override
                    public void surfaceChanged(SurfaceHolder holder, int format,
                                  int width, int height) {
                    }
             });
             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
       }
 
       @Override
       protected void onDraw(Canvas canvas) {
             canvas.drawColor(Color.BLACK);
             if (x < getWidth() - bmp.getWidth()) {
                    x++;
             }
             canvas.drawBitmap(bmp, x, 10, null);
       }
}
The animated image below shows us the result. If you watch carefully you can notice that the animation speed is not constant. This is because when the game loop gets more CPU time the animation is faster. We can fix this defining how many FPS , "frames per second" we want in our application.
We limited the drawing to 10 FPS that is 100 ms (millisecond). We use the sleep method for the remaining time to get the 100 ms. If the loop takes more than 100 ms we sleep 10 ms anyway to avoid our application to be too much CPU demanding.
package com.edu4java.android.killthemall;
import android.graphics.Canvas;
 
public class GameLoopThread extends Thread {
       static final long FPS = 10;
       private GameView view;
       private boolean running = false;
      
       public GameLoopThread(GameView view) {
             this.view = view;
       }
 
       public void setRunning(boolean run) {
             running = run;
       }
 
       @Override
       public void run() {
             long ticksPS = 1000 / FPS;
             long startTime;
             long sleepTime;
             while (running) {
                    Canvas c = null;
                    startTime = System.currentTimeMillis();
                    try {
                           c = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(c);
                           }
                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(c);
                           }
                    }
                    sleepTime = ticksPS-(System.currentTimeMillis() - startTime);
                    try {
                           if (sleepTime > 0)
                                  sleep(sleepTime);
                           else
                                  sleep(10);
                    } catch (Exception e) {}
             }
       }
}
In the SurfaceView we just add a xSpeed field to maintain the animation direction and in onDraw we change the direction when the borders are reached. Now the icon goes back and forward indefinitely between the left and right borders.
package com.edu4java.android.killthemall;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
public class GameView extends SurfaceView {
       private Bitmap bmp;
       private SurfaceHolder holder;
       private GameLoopThread gameLoopThread;
       private int x = 0; 
       private int xSpeed = 1;
      
       public GameView(Context context) {
             super(context);
             gameLoopThread = new GameLoopThread(this);
             holder = getHolder();
             holder.addCallback(new SurfaceHolder.Callback() {
 
                    @Override
                    public void surfaceDestroyed(SurfaceHolder holder) {
                           boolean retry = true;
                           gameLoopThread.setRunning(false);
                           while (retry) {
                                  try {
                                        gameLoopThread.join();
                                        retry = false;
                                  } catch (InterruptedException e) {
                                  }
                           }
                    }
 
                    @Override
                    public void surfaceCreated(SurfaceHolder holder) {
                           gameLoopThread.setRunning(true);
                           gameLoopThread.start();
                    }
 
                    @Override
                    public void surfaceChanged(SurfaceHolder holder, int format,
                                  int width, int height) {
                    }
             });
             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
       }
 
       @Override
       protected void onDraw(Canvas canvas) {
             if (x == getWidth() - bmp.getWidth()) {
                    xSpeed = -1;
             }
             if (x == 0) {
                    xSpeed = 1;
             }
             x = x + xSpeed;
             canvas.drawColor(Color.BLACK);
             canvas.drawBitmap(bmp, x , 10, null);
       }
}

No comments:

Post a Comment