Create a Running Man Game Animation on Android
Making a Running Man Animation on Android is a great way to learn how to work with Bitmaps, Thread and SurfaceView. First thing to make a Running Man Game Animation is to have a character to animate. For that, we will use the following sprite sheet :
Like you can see, our character sprite sheet has 8 frames. Each frame show the character in a different position when he runs. Note that you can also discover this tutorial in video on Youtube :
For our animation, we are going to create a custom GameView extending the SurfaceView class and implementing the Runnable interface. First, we need to define some properties like the game thread, the surface holder, the canvas where running man will be drawn, the bitmap used to load the sprite sheet and some parameters to customize the running man animation like the speed, the size or the position of the man on the screen :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | class GameView extends SurfaceView implements Runnable { private Thread gameThread; private SurfaceHolder ourHolder; private volatile boolean playing; private Canvas canvas; private Bitmap bitmapRunningMan; private boolean isMoving; private float runSpeedPerSecond = 500 ; private float manXPos = 10 , manYPos = 10 ; private int frameWidth = 230 , frameHeight = 274 ; private int frameCount = 8 ; private int currentFrame = 0 ; private long fps; private long timeThisFrame; private long lastFrameChangeTime = 0 ; private int frameLengthInMillisecond = 50 ; // ... } |
To draw correctly the good frame for the running man, we need two Rectangle instances. One used to define the current frame in the sprite sheet and an other to define where to draw the current frame on the screen :
1 2 3 | private Rect frameToDraw = new Rect( 0 , 0 , frameWidth, frameHeight); private RectF whereToDraw = new RectF(manXPos, manYPos, manXPos + frameWidth, frameHeight); |
On the GameView constructor, we get the surface holder and then, we load the sprite sheet into the bitmapRunningMan variable. We apply a scale transformation according values defined in frameWidth and frameHeight parameters :
1 2 3 4 5 6 7 8 | public GameView(Context context) { super (context); ourHolder = getHolder(); bitmapRunningMan = BitmapFactory.decodeResource(getResources(), R.drawable.running_man); bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan, frameWidth * frameCount, frameHeight, false ); } |
Now, it’s time to make the event loop for animation inside the run method overrided from Runnable interface :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @Override public void run() { while (playing) { long startFrameTime = System.currentTimeMillis(); update(); draw(); timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame >= 1 ) { fps = 1000 / timeThisFrame; } } } |
Note that we animate the character while the playing variable is set to true. Like usual in a game, we update the elements and then we draw before to calculate frame per seconds. The update method is just used here to move the man positions in X and Y. Note that when the man reach the end of the screen horizontally or vertically, we set its position to the left or top of the screen :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | public void update() { if (isMoving) { manXPos = manXPos + runSpeedPerSecond / fps; if (manXPos > getWidth()) { manYPos += ( int ) frameHeight; manXPos = 10 ; } if (manYPos + frameHeight > getHeight()) { manYPos = 10 ; } } } |
Before to write the draw method, we need to define a method to manage the current frame to display for the character. We change the current frame only when he have ended the frame duration :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public void manageCurrentFrame() { long time = System.currentTimeMillis(); if (isMoving) { if (time > lastFrameChangeTime + frameLengthInMillisecond) { lastFrameChangeTime = time; currentFrame++; if (currentFrame >= frameCount) { currentFrame = 0 ; } } } frameToDraw.left = currentFrame * frameWidth; frameToDraw.right = frameToDraw.left + frameWidth; } |
And now, we define the draw method :
01 02 03 04 05 06 07 08 09 10 11 | public void draw() { if (ourHolder.getSurface().isValid()) { canvas = ourHolder.lockCanvas(); canvas.drawColor(Color.WHITE); whereToDraw.set(( int ) manXPos, ( int ) manYPos, ( int ) manXPos + frameWidth, ( int ) manYPos + frameHeight); manageCurrentFrame(); canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null ); ourHolder.unlockCanvasAndPost(canvas); } } |
First, we check if the surface is valid. Then we lock the canvas and we draw the character current frame. Last, we unlock the canvas and post it on the Surface View. Finally, we define two methods to pause or resume the running man animation :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | public void pause() { playing = false ; try { gameThread.join(); } catch (InterruptedException e) { Log.e( "ERR" , "Joining Thread" ); } } public void resume() { playing = true ; gameThread = new Thread( this ); gameThread.start(); } |
To start the running man animation, we’re going to wait the user click on the surface view. So, we need to override the onTouchEvent method and wait for an ACTION_DOWN event. When the event is made, we have just to change the isMoving boolean value. If man is running, we stop it. If man doesn’t run, we start to move it :
01 02 03 04 05 06 07 08 09 10 | @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN : isMoving = !isMoving; break ; } return true ; } |
Last thing to make is to assemble all the pieces of the puzzle, create the game view on the main activity, set it as the content view and then resume or pause the game animation when the activity is resumed or paused :
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | package com.ssaurel.runningman; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; public class RunningManAnimation extends AppCompatActivity { private GameView gameView; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); gameView = new GameView( this ); setContentView(gameView); } @Override protected void onResume() { super .onResume(); gameView.resume(); } @Override protected void onPause() { super .onPause(); gameView.pause(); } class GameView extends SurfaceView implements Runnable { private Thread gameThread; private SurfaceHolder ourHolder; private volatile boolean playing; private Canvas canvas; private Bitmap bitmapRunningMan; private boolean isMoving; private float runSpeedPerSecond = 500 ; private float manXPos = 10 , manYPos = 10 ; private int frameWidth = 230 , frameHeight = 274 ; private int frameCount = 8 ; private int currentFrame = 0 ; private long fps; private long timeThisFrame; private long lastFrameChangeTime = 0 ; private int frameLengthInMillisecond = 50 ; private Rect frameToDraw = new Rect( 0 , 0 , frameWidth, frameHeight); private RectF whereToDraw = new RectF(manXPos, manYPos, manXPos + frameWidth, frameHeight); public GameView(Context context) { super (context); ourHolder = getHolder(); bitmapRunningMan = BitmapFactory.decodeResource(getResources(), R.drawable.running_man); bitmapRunningMan = Bitmap.createScaledBitmap(bitmapRunningMan, frameWidth * frameCount, frameHeight, false ); } @Override public void run() { while (playing) { long startFrameTime = System.currentTimeMillis(); update(); draw(); timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame >= 1 ) { fps = 1000 / timeThisFrame; } } } public void update() { if (isMoving) { manXPos = manXPos + runSpeedPerSecond / fps; if (manXPos > getWidth()) { manYPos += ( int ) frameHeight; manXPos = 10 ; } if (manYPos + frameHeight > getHeight()) { manYPos = 10 ; } } } public void manageCurrentFrame() { long time = System.currentTimeMillis(); if (isMoving) { if (time > lastFrameChangeTime + frameLengthInMillisecond) { lastFrameChangeTime = time; currentFrame++; if (currentFrame >= frameCount) { currentFrame = 0 ; } } } frameToDraw.left = currentFrame * frameWidth; frameToDraw.right = frameToDraw.left + frameWidth; } public void draw() { if (ourHolder.getSurface().isValid()) { canvas = ourHolder.lockCanvas(); canvas.drawColor(Color.WHITE); whereToDraw.set(( int ) manXPos, ( int ) manYPos, ( int ) manXPos + frameWidth, ( int ) manYPos + frameHeight); manageCurrentFrame(); canvas.drawBitmap(bitmapRunningMan, frameToDraw, whereToDraw, null ); ourHolder.unlockCanvasAndPost(canvas); } } public void pause() { playing = false ; try { gameThread.join(); } catch (InterruptedException e) { Log.e( "ERR" , "Joining Thread" ); } } public void resume() { playing = true ; gameThread = new Thread( this ); gameThread.start(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN : isMoving = !isMoving; break ; } return true ; } } } |
Now, you have just to run the application on your Android emulator or on your real device and to enjoy your Running Man Game Animation.
2 Comments Already
Leave a Reply
You must be logged in to post a comment.
Nice tutorial! Would be nice to have a link at github.