Android 2D Game Tutorial for Beginners

1- Introduction

This document is based on:
  • Android Studio 1.5

Target of the document is help you to become acquainted with a few simple techniques in  programming Android Game 2D. Include:
  • Use SuffaceView
  • Drawing on a Canvas
  • The motion of the game character.
  • Interactions with the player's gestures
In this document, I will guide you step by step, therefore, you need to read and practice up to down. We will write each version of the game from 1 to the final version.

2- Create a Game Project

Note that you're creating a 2D game on Android, so the interface of the game must be drawn by you, so you do not need a activity_main.xml file.
OK, your Project was created.

3- Preparing Images and sounds

You need a few images
  • chibi1.png

  • chibi2.png


 
  • explosion.png





 
The Audio file of the explosion.
Background sound:
Copy these images to the drawable folder of project. Create raw folder, and copy explosion.wav & background.mp3 to this folder.

4- Setting fullscreen (Version:1)

With games, you need to set the background image and an important thing is that you need to set FullScreen mode.
Your MainActivity class must extends from the Activity class .
MainActivity.java (Version: 1)
package org.o7planning.androidgame2d;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Set fullscreen
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // Set No Title
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);

    }

}
Running apps:
Next, set screen is Landscape. You need to set in  AndroidManifest.xml.
android:screenOrientation="landscape"
Rerun apps:
Note: On Windows you can also change the direction of the simulater by Ctrl + F11.

5- Show character in the game (Version:2)

Next you need to write code to show the game character on the screen and move it from left to right in a certain speed.
With a character in the game you only have one image file, but the image are divided into several regions described the different actions of the character.
Using the code you can draw a picture to the Canvas of the game, at the x, y coordinates. Use a  loop to continuously draw on the Canvas, you can create the movement of the character.
When programming a game, you also need to  pay attention to the movement direction of the characters in the game, the speed of the character.
Create a  Gameobject class, objects of the game is extended from this class.
GameObject.java (Version: Release)
package org.o7planning.androidgame2d;


import android.graphics.Bitmap;

public abstract class GameObject {

    protected Bitmap image;

    protected final int rowCount;
    protected final int colCount;

    protected final int WIDTH;
    protected final int HEIGHT;

    protected final int width;


    protected final int height;
    protected int x;
    protected int y;

    public GameObject(Bitmap image, int rowCount, int colCount, int x, int y)  {

        this.image = image;
        this.rowCount= rowCount;
        this.colCount= colCount;

        this.x= x;
        this.y= y;

        this.WIDTH = image.getWidth();
        this.HEIGHT = image.getHeight();

        this.width = this.WIDTH/ colCount;
        this.height= this.HEIGHT/ rowCount;
    }


    protected Bitmap createSubImageAt(int row, int col)  {
        // createBitmap(bitmap, x, y, width, height).
        Bitmap subImage = Bitmap.createBitmap(image, col* width, row* height ,width,height);
        return subImage;
    }

    public int getX()  {
        return this.x;
    }

    public int getY()  {
        return this.y;
    }


    public int getHeight() {
        return height;
    }

    public int getWidth() {
        return width;
    }

}
ChibiCharacter Class simulates a character in the game.
ChibiCharacter.java (Version: 2)
package org.o7planning.androidgame2d;


import android.graphics.Bitmap;
import android.graphics.Canvas;

public class ChibiCharacter extends GameObject {

    private static final int ROW_TOP_TO_BOTTOM = 0;
    private static final int ROW_RIGHT_TO_LEFT = 1;
    private static final int ROW_LEFT_TO_RIGHT = 2;
    private static final int ROW_BOTTOM_TO_TOP = 3;

    // Row index of Image are being used.
    private int rowUsing = ROW_LEFT_TO_RIGHT;

    private int colUsing;

    private Bitmap[] leftToRights;
    private Bitmap[] rightToLefts;
    private Bitmap[] topToBottoms;
    private Bitmap[] bottomToTops;

    // Velocity of game character (pixel/millisecond)
    public static final float VELOCITY = 0.1f;

    private int movingVectorX = 10;
    private int movingVectorY = 5;

    private long lastDrawNanoTime =-1;

    private GameSurface gameSurface;

    public ChibiCharacter(GameSurface gameSurface, Bitmap image, int x, int y) {
        super(image, 4, 3, x, y);

        this.gameSurface= gameSurface;

        this.topToBottoms = new Bitmap[colCount]; // 3
        this.rightToLefts = new Bitmap[colCount]; // 3
        this.leftToRights = new Bitmap[colCount]; // 3
        this.bottomToTops = new Bitmap[colCount]; // 3

        for(int col = 0; col< this.colCount; col++ ) {
            this.topToBottoms[col] = this.createSubImageAt(ROW_TOP_TO_BOTTOM, col);
            this.rightToLefts[col]  = this.createSubImageAt(ROW_RIGHT_TO_LEFT, col);
            this.leftToRights[col] = this.createSubImageAt(ROW_LEFT_TO_RIGHT, col);
            this.bottomToTops[col]  = this.createSubImageAt(ROW_BOTTOM_TO_TOP, col);
        }
    }

    public Bitmap[] getMoveBitmaps()  {
        switch (rowUsing)  {
            case ROW_BOTTOM_TO_TOP:
                return  this.bottomToTops;
            case ROW_LEFT_TO_RIGHT:
                return this.leftToRights;
            case ROW_RIGHT_TO_LEFT:
                return this.rightToLefts;
            case ROW_TOP_TO_BOTTOM:
                return this.topToBottoms;
            default:
                return null;
        }
    }

    public Bitmap getCurrentMoveBitmap()  {
        Bitmap[] bitmaps = this.getMoveBitmaps();
        return bitmaps[this.colUsing];
    }


    public void update()  {
        this.colUsing++;
        if(colUsing >= this.colCount)  {
            this.colUsing =0;
        }
        // Current time in nanoseconds
        long now = System.nanoTime();

        // Never once did draw.
        if(lastDrawNanoTime==-1) {
            lastDrawNanoTime= now;
        }
        // Change nanoseconds to milliseconds (1 nanosecond = 1000000 milliseconds).
        int deltaTime = (int) ((now - lastDrawNanoTime)/ 1000000 );

        // Distance moves
        float distance = VELOCITY * deltaTime;

        double movingVectorLength = Math.sqrt(movingVectorX* movingVectorX + movingVectorY*movingVectorY);

        // Calculate the new position of the game character.
        this.x = x +  (int)(distance* movingVectorX / movingVectorLength);
        this.y = y +  (int)(distance* movingVectorY / movingVectorLength);

        // When the game's character touches the edge of the screen, then change direction

        if(this.x < 0 )  {
            this.x = 0;
            this.movingVectorX = - this.movingVectorX;
        } else if(this.x > this.gameSurface.getWidth() -width)  {
            this.x= this.gameSurface.getWidth()-width;
            this.movingVectorX = - this.movingVectorX;
        }

        if(this.y < 0 )  {
            this.y = 0;
            this.movingVectorY = - this.movingVectorY;
        } else if(this.y > this.gameSurface.getHeight()- height)  {
            this.y= this.gameSurface.getHeight()- height;
            this.movingVectorY = - this.movingVectorY ;
        }

        // rowUsing
        if( movingVectorX > 0 )  {
            if(movingVectorY > 0 && Math.abs(movingVectorX) < Math.abs(movingVectorY)) {
                this.rowUsing = ROW_TOP_TO_BOTTOM;
            }else if(movingVectorY < 0 && Math.abs(movingVectorX) < Math.abs(movingVectorY)) {
                this.rowUsing = ROW_BOTTOM_TO_TOP;
            }else  {
                this.rowUsing = ROW_LEFT_TO_RIGHT;
            }
        } else {
            if(movingVectorY > 0 && Math.abs(movingVectorX) < Math.abs(movingVectorY)) {
                this.rowUsing = ROW_TOP_TO_BOTTOM;
            }else if(movingVectorY < 0 && Math.abs(movingVectorX) < Math.abs(movingVectorY)) {
                this.rowUsing = ROW_BOTTOM_TO_TOP;
            }else  {
                this.rowUsing = ROW_RIGHT_TO_LEFT;
            }
        }
    }

    public void draw(Canvas canvas)  {
        Bitmap bitmap = this.getCurrentMoveBitmap();
        canvas.drawBitmap(bitmap,x, y, null);
        // Last draw time.
        this.lastDrawNanoTime= System.nanoTime();
    }

    public void setMovingVector(int movingVectorX, int movingVectorY)  {
        this.movingVectorX= movingVectorX;
        this.movingVectorY = movingVectorY;
    }
}
 
Game Thread is a thread that controls the update of the game interface.
GameThread.java (Version: Release)
package org.o7planning.androidgame2d;


import android.graphics.Canvas;
import android.view.SurfaceHolder;

public class GameThread extends Thread {

    private boolean running;
    private GameSurface gameSurface;
    private SurfaceHolder surfaceHolder;

    public GameThread(GameSurface gameSurface, SurfaceHolder surfaceHolder)  {
         this.gameSurface= gameSurface;
         this.surfaceHolder= surfaceHolder;
    }

    @Override
    public void run()  {
         long startTime = System.nanoTime();

         while(running)  {
             Canvas canvas= null;
             try {
                 // Get Canvas from Holder and lock it.
                 canvas = this.surfaceHolder.lockCanvas();

                 // Synchronized
                 synchronized (canvas)  {
                     this.gameSurface.update();
                     this.gameSurface.draw(canvas);
                 }
             }catch(Exception e)  {
                 // Do nothing.
             } finally {
                 if(canvas!= null)  {
                     // Unlock Canvas.
                     this.surfaceHolder.unlockCanvasAndPost(canvas);
                 }
             }
             long now = System.nanoTime() ;
             // Interval to redraw game
             // (Change nanoseconds to milliseconds)
             long waitTime = (now - startTime)/1000000;
             if(waitTime < 10)  {
                 waitTime= 10; // Millisecond.
             }
             System.out.print(" Wait Time="+ waitTime);

             try {
                 // Sleep.
                 this.sleep(waitTime);
             } catch(InterruptedException e)  {

             }
             startTime = System.nanoTime();
             System.out.print(".");
         }
    }

    public void setRunning(boolean running)  {
        this.running= running;
    }
}
GameSurface Class simulate the entire surface of the game. This class extends from SurfaceView, SurfaceView contains a Canvas object, the objects in the game will be drawn on the Canvas.
GameSurface.java (Version: 2)
package org.o7planning.androidgame2d;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {


   private GameThread gameThread;

   private ChibiCharacter chibi1;

   public GameSurface(Context context)  {
       super(context);

       // Make Game Surface focusable so it can handle events. .
       this.setFocusable(true);

       // Sét callback.
       this.getHolder().addCallback(this);
   }

   public void update()  {
       this.chibi1.update();
   }

 

   @Override
   public void draw(Canvas canvas)  {
       super.draw(canvas);

       this.chibi1.draw(canvas);
   }

   // Implements method of SurfaceHolder.Callback
   @Override
   public void surfaceCreated(SurfaceHolder holder) {
       Bitmap chibiBitmap1 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi1);
       this.chibi1 = new ChibiCharacter(this,chibiBitmap1,100,50);

       this.gameThread = new GameThread(this,holder);
       this.gameThread.setRunning(true);
       this.gameThread.start();
   }

   // Implements method of SurfaceHolder.Callback
   @Override
   public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

   }

   // Implements method of SurfaceHolder.Callback
   @Override
   public void surfaceDestroyed(SurfaceHolder holder) {
       boolean retry= true;
       while(retry) {
           try {
               this.gameThread.setRunning(false);

               // Parent thread must wait until the end of GameThread.
               this.gameThread.join();
           }catch(InterruptedException e)  {
               e.printStackTrace();
           }
           retry= true;
       }
   }

}
MainActivity.java (Version: Release)
package org.o7planning.androidgame2d;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Set fullscreen
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // Set No Title
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);

      
        this.setContentView(new GameSurface(this));
    }

}
OK, 2nd version is completed, you can run the game.

6- Interact with player (Version: 3)

Next, you can handle events when the user touches the screen, the game character will run towards that you touched. You should handle this event on GameSurface class.
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        int x=  (int)event.getX();
        int y = (int)event.getY();

        int movingVectorX =x-  this.chibi1.getX() ;
        int movingVectorY =y-  this.chibi1.getY() ;

        this.chibi1.setMovingVector(movingVectorX,movingVectorY);
        return true;
    }
    return false;
}
View full code:
GameSurface.java (Version: 3)
package org.o7planning.androidgame2d;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {


    private GameThread gameThread;

    private ChibiCharacter chibi1;

    public GameSurface(Context context)  {
        super(context);

        // Make Game Surface focusable so it can handle events.
        this.setFocusable(true);

        // Set callback.
        this.getHolder().addCallback(this);
    }

    public void update()  {
        this.chibi1.update();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            int x=  (int)event.getX();
            int y = (int)event.getY();

            int movingVectorX =x-  this.chibi1.getX() ;
            int movingVectorY =y-  this.chibi1.getY() ;

            this.chibi1.setMovingVector(movingVectorX,movingVectorY);
            return true;
        }
        return false;
    }

    @Override
    public void draw(Canvas canvas)  {
        super.draw(canvas);

        this.chibi1.draw(canvas);
    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Bitmap chibiBitmap1 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi1);
        this.chibi1 = new ChibiCharacter(this,chibiBitmap1,100,50);

        this.gameThread = new GameThread(this,holder);
        this.gameThread.setRunning(true);
        this.gameThread.start();
    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry= true;
        while(retry) {
            try {
                this.gameThread.setRunning(false);

                // Parent thread must wait until the end of GameThread.
                this.gameThread.join();
            }catch(InterruptedException e)  {
                e.printStackTrace();
            }
            retry= true;
        }
    }

}
Rerun the game:

7- Game with multiple characters (Version: 4)

You can add other characters to the game, here I add a secondary character. You need to edit the code of the   GameSurfac class:
GameSurface.java (Version: 4)
package org.o7planning.androidgame2d;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.ArrayList;
import java.util.List;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {

    private GameThread gameThread;

    private final List<ChibiCharacter> chibiList = new ArrayList<ChibiCharacter>();

    public GameSurface(Context context)  {
        super(context);

        // Make Game Surface focusable so it can handle events.
        this.setFocusable(true);

        // Sét callback.
        this.getHolder().addCallback(this);
    }

    public void update()  {
        for(ChibiCharacter chibi: chibiList) {
            chibi.update();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            int x=  (int)event.getX();
            int y = (int)event.getY();

            for(ChibiCharacter chibi: chibiList) {
                int movingVectorX =x-  chibi.getX() ;
                int movingVectorY =y-  chibi.getY() ;
                chibi.setMovingVector(movingVectorX, movingVectorY);
            }
            return true;
        }
        return false;
    }

    @Override
    public void draw(Canvas canvas)  {
        super.draw(canvas);

        for(ChibiCharacter chibi: chibiList)  {
            chibi.draw(canvas);
        }

    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Bitmap chibiBitmap1 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi1);
        ChibiCharacter chibi1 = new ChibiCharacter(this,chibiBitmap1,100,50);

        Bitmap chibiBitmap2 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi2);
        ChibiCharacter chibi2 = new ChibiCharacter(this,chibiBitmap2,300,150);

        this.chibiList.add(chibi1);
        this.chibiList.add(chibi2);

        this.gameThread = new GameThread(this,holder);
        this.gameThread.setRunning(true);
        this.gameThread.start();
    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry= true;
        while(retry) {
            try {
                this.gameThread.setRunning(false);

                // Parent thread must wait until the end of GameThread.
                this.gameThread.join();
            }catch(InterruptedException e)  {
                e.printStackTrace();
            }
            retry= true;
        }
    }

}
Rerun the game:

8- Effects in the Game (Version: 5)

Sometime, you need to handle some effects of the game, for example, you are steering a plane, when the plane hit the ground it will explode, so here explosion is an effect. In this section, I will simulate when you click Chibi character, it will explode.
Explosion Class simulate an explosion, when you click on the Chibi characte, it is removed from the game and an Explosion object will be added to the game at position where Chibi character has been eliminated.
Explosion.java (Version: 5)
package org.o7planning.androidgame2d;


import android.graphics.Bitmap;
import android.graphics.Canvas;

public class Explosion extends GameObject {

    private int rowIndex = 0 ;
    private int colIndex = -1 ;

    private boolean finish= false;
    private GameSurface gameSurface;

    public Explosion(GameSurface GameSurface, Bitmap image, int x, int y) {
        super(image, 5, 5, x, y);

        this.gameSurface= GameSurface;
    }

    public void update()  {
        this.colIndex++;

        if(this.colIndex >= this.colCount)  {
            this.colIndex =0;
            this.rowIndex++;

            if(this.rowIndex>= this.rowCount)  {
                this.finish= true;
            }
        }
    }

    public void draw(Canvas canvas)  {
        if(!finish)  {
            Bitmap bitmap= this.createSubImageAt(rowIndex,colIndex);
            canvas.drawBitmap(bitmap, this.x, this.y,null);
        }
    }

    public boolean isFinish() {
        return finish;
    }

}
Change code of GameSurface class:
GameSurface.java (Version: 5)
package org.o7planning.androidgame2d;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {

  private GameThread gameThread;

  private final List<ChibiCharacter> chibiList = new ArrayList<ChibiCharacter>();
  private final List<Explosion> explosionList = new ArrayList<Explosion>();

  public GameSurface(Context context)  {
      super(context);

      // Make Game Surface focusable so it can handle events.
      this.setFocusable(true);

      // Sét callback.
      this.getHolder().addCallback(this);
  }


  @Override
  public boolean onTouchEvent(MotionEvent event) {
      if (event.getAction() == MotionEvent.ACTION_DOWN) {

          int x=  (int)event.getX();
          int y = (int)event.getY();

          Iterator<ChibiCharacter> iterator= this.chibiList.iterator();
       
          while(iterator.hasNext()) {
              ChibiCharacter chibi = iterator.next();
              if( chibi.getX() < x && x < chibi.getX() + chibi.getWidth()
                  && chibi.getY() < y && y < chibi.getY()+ chibi.getHeight())  {
                  // Remove the current element from the iterator and the list.
                  iterator.remove();

                  // Create Explosion object.
                  Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(),R.drawable.explosion);
                  Explosion explosion = new Explosion(this, bitmap,chibi.getX(),chibi.getY());

                  this.explosionList.add(explosion);
              }
          }


          for(ChibiCharacter chibi: chibiList) {
              int movingVectorX =x-  chibi.getX() ;
              int movingVectorY =y-  chibi.getY() ;
              chibi.setMovingVector(movingVectorX, movingVectorY);
          }
          return true;
      }
      return false;
  }

  public void update()  {
      for(ChibiCharacter chibi: chibiList) {
          chibi.update();
      }
      for(Explosion explosion: this.explosionList)  {
          explosion.update();
      }

      Iterator<Explosion> iterator= this.explosionList.iterator();
      while(iterator.hasNext())  {
          Explosion explosion = iterator.next();

          if(explosion.isFinish()) {
              // If explosion finish, Remove the current element from the iterator & list.
              iterator.remove();
              continue;
          }
      }
  }

  @Override
  public void draw(Canvas canvas)  {
      super.draw(canvas);

      for(ChibiCharacter chibi: chibiList)  {
          chibi.draw(canvas);
      }

      for(Explosion explosion: this.explosionList)  {
          explosion.draw(canvas);
      }

  }

  // Implements method of SurfaceHolder.Callback
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
      Bitmap chibiBitmap1 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi1);
      ChibiCharacter chibi1 = new ChibiCharacter(this,chibiBitmap1,100,50);

      Bitmap chibiBitmap2 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi2);
      ChibiCharacter chibi2 = new ChibiCharacter(this,chibiBitmap2,300,150);

      this.chibiList.add(chibi1);
      this.chibiList.add(chibi2);

      this.gameThread = new GameThread(this,holder);
      this.gameThread.setRunning(true);
      this.gameThread.start();
  }

  // Implements method of SurfaceHolder.Callback
  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

  }

  // Implements method of SurfaceHolder.Callback
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
      boolean retry= true;
      while(retry) {
          try {
              this.gameThread.setRunning(false);

              // Parent thread must wait until the end of GameThread.
              this.gameThread.join();
          }catch(InterruptedException e)  {
              e.printStackTrace();
          }
          retry= true;
      }
  }

}
  public void update()  {
      this.colIndex++;

      if(this.colIndex >= this.colCount)  {
          this.colIndex =0;
          this.rowIndex++;

          if(this.rowIndex>= this.rowCount)  {
              this.finish= true;
          }
      }
  }

  public void draw(Canvas canvas)  {
      if(!finish)  {
          Bitmap bitmap= this.createSubImageAt(rowIndex,colIndex);
          canvas.drawBitmap(bitmap, this.x, this.y,null);
      }
  }

  public boolean isFinish() {
      return finish;
  }
 
}
Rerun the game:

9- Sound effects in game (Version: Release)

Next you need to add sound effects to the game, such as background sounds of the game, the sound of explosions when Chibi character is destroyed.
Explosion.java (Version: Release)
package org.o7planning.androidgame2d;


import android.graphics.Bitmap;
import android.graphics.Canvas;

public class Explosion extends GameObject {

    private int rowIndex = 0 ;
    private int colIndex = -1 ;

    private boolean finish= false;
    private GameSurface gameSurface;

    public Explosion(GameSurface GameSurface, Bitmap image, int x, int y) {
        super(image, 5, 5, x, y);

        this.gameSurface= GameSurface;
    }

    public void update()  {
        this.colIndex++;

        // Play sound explosion.wav.
        if(this.colIndex==0 && this.rowIndex==0) {
            this.gameSurface.playSoundExplosion();
        }

        if(this.colIndex >= this.colCount)  {
            this.colIndex =0;
            this.rowIndex++;

            if(this.rowIndex>= this.rowCount)  {
                this.finish= true;
            }
        }
    }

    public void draw(Canvas canvas)  {
        if(!finish)  {
            Bitmap bitmap= this.createSubImageAt(rowIndex,colIndex);
            canvas.drawBitmap(bitmap, this.x, this.y,null);
        }
    }

    public boolean isFinish() {
        return finish;
    }

}
GameSurface.java (Version: Release)
package org.o7planning.androidgame2d;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {

    private GameThread gameThread;

    private final List<ChibiCharacter> chibiList = new ArrayList<ChibiCharacter>();
    private final List<Explosion> explosionList = new ArrayList<Explosion>();

    private static final int MAX_STREAMS=100;
    private int soundIdExplosion;
    private int soundIdBackground;

    private boolean soundPoolLoaded;
    private SoundPool soundPool;



    public GameSurface(Context context)  {
        super(context);

        // Make Game Surface focusable so it can handle events.
        this.setFocusable(true);

        // Sét callback.
        this.getHolder().addCallback(this);

        this.initSoundPool();
    }

    private void initSoundPool()  {
        // With Android API >= 21.
        if (Build.VERSION.SDK_INT >= 21 ) {

            AudioAttributes audioAttrib = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_GAME)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build();

            SoundPool.Builder builder= new SoundPool.Builder();
            builder.setAudioAttributes(audioAttrib).setMaxStreams(MAX_STREAMS);

            this.soundPool = builder.build();
        }
        // With Android API < 21
        else {
            // SoundPool(int maxStreams, int streamType, int srcQuality)
            this.soundPool = new SoundPool(MAX_STREAMS, AudioManager.STREAM_MUSIC, 0);
        }

        // When SoundPool load complete.
        this.soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
            @Override
            public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
                soundPoolLoaded = true;

                // Playing background sound.
                playSoundBackground();
            }
        });

        // Load the sound background.mp3 into SoundPool
        this.soundIdBackground= this.soundPool.load(this.getContext(), R.raw.background,1);

        // Load the sound explosion.wav into SoundPool
        this.soundIdExplosion = this.soundPool.load(this.getContext(), R.raw.explosion,1);


    }

    public void playSoundExplosion()  {
        if(this.soundPoolLoaded) {
            float leftVolumn = 0.8f;
            float rightVolumn =  0.8f;
            // Play sound explosion.wav
            int streamId = this.soundPool.play(this.soundIdExplosion,leftVolumn, rightVolumn, 1, 0, 1f);
        }
    }

    public void playSoundBackground()  {
        if(this.soundPoolLoaded) {
            float leftVolumn = 0.8f;
            float rightVolumn =  0.8f;
            // Play sound background.mp3
            int streamId = this.soundPool.play(this.soundIdBackground,leftVolumn, rightVolumn, 1, -1, 1f);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            int x=  (int)event.getX();
            int y = (int)event.getY();

            Iterator<ChibiCharacter> iterator= this.chibiList.iterator();

      
            while(iterator.hasNext()) {
                ChibiCharacter chibi = iterator.next();
                if( chibi.getX() < x && x < chibi.getX() + chibi.getWidth()
                    && chibi.getY() < y && y < chibi.getY()+ chibi.getHeight())  {
                    // Remove the current element from the iterator and the list.
                    iterator.remove();

                    // Create Explosion object.
                    Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(),R.drawable.explosion);
                    Explosion explosion = new Explosion(this, bitmap,chibi.getX(),chibi.getY());

                    this.explosionList.add(explosion);
                }
            }


            for(ChibiCharacter chibi: chibiList) {
                int movingVectorX =x-  chibi.getX() ;
                int movingVectorY =y-  chibi.getY() ;
                chibi.setMovingVector(movingVectorX, movingVectorY);
            }
            return true;
        }
        return false;
    }

    public void update()  {
        for(ChibiCharacter chibi: chibiList) {
            chibi.update();
        }
        for(Explosion explosion: this.explosionList)  {
            explosion.update();
        }

        Iterator<Explosion> iterator= this.explosionList.iterator();
        while(iterator.hasNext())  {
            Explosion explosion = iterator.next();

            if(explosion.isFinish()) {
                // If explosion finish, Remove the current element from the iterator & list.
                iterator.remove();
                continue;
            }
        }
    }

    @Override
    public void draw(Canvas canvas)  {
        super.draw(canvas);

        for(ChibiCharacter chibi: chibiList)  {
            chibi.draw(canvas);
        }

        for(Explosion explosion: this.explosionList)  {
            explosion.draw(canvas);
        }

    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Bitmap chibiBitmap1 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi1);
        ChibiCharacter chibi1 = new ChibiCharacter(this,chibiBitmap1,100,50);

        Bitmap chibiBitmap2 = BitmapFactory.decodeResource(this.getResources(),R.drawable.chibi2);
        ChibiCharacter chibi2 = new ChibiCharacter(this,chibiBitmap2,300,150);

        this.chibiList.add(chibi1);
        this.chibiList.add(chibi2);

        this.gameThread = new GameThread(this,holder);
        this.gameThread.setRunning(true);
        this.gameThread.start();
    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    // Implements method of SurfaceHolder.Callback
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry= true;
        while(retry) {
            try {
                this.gameThread.setRunning(false);

                // Parent thread must wait until the end of GameThread.
                this.gameThread.join();
            }catch(InterruptedException e)  {
                e.printStackTrace();
            }
            retry= true;
        }
    }

}
Note: You can refer to the documents on sound effects at:
OK, now you can rerun the game and listen to the sound effects of the game.