/*******************************************************************************
* Copyright (c) 2011 Pieter Pareit.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* <p>
* Contributors:
* Pieter Pareit - initial API and implementation
******************************************************************************/
package be.ppareit.android;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import net.vrallev.android.cat.Cat;
import static be.ppareit.android.Utils.sleepIgnoreInterrupt;
/**
* This class contains all logic related to running a game loop.<p>
*
* This class must be extended by your game. All game logic should happen in
* onUpdate(). All drawing should happen in onDraw(). The game loop is started with
* startGameLoop() and stopped with pauseGameLoop(). When the game loop is paused, and
* the screen needs to be refreshed, call invalidate(). <p>
*
* While part of GameOfLife, this class can be reused in other applications that need
* a view containing a game loop and the related logic.
*
*/
public abstract class GameLoopView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = GameLoopView.class.getSimpleName();
class AnimationThread extends Thread {
private volatile boolean mRun;
private long mLastTime = System.currentTimeMillis();
private int mFrameSamplesCollected = 0;
private int mFrameSampleTime = 0;
private int mFps = 0;
private final SurfaceHolder mSurfaceHolder;
private final Paint mFpsTextPaint;
public AnimationThread(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
mFpsTextPaint = new Paint();
mFpsTextPaint.setARGB(255, 255, 0, 0);
mFpsTextPaint.setTextSize(32);
}
@SuppressLint("WrongCall")
@Override
public void run() {
Log.d(TAG, "AnimationThread.run'ing");
// block until the surface is completely created in the main thread
while (!surfaceCreatedCompleted) {
}
while (mRun) {
// give the system some time to run
sleepIgnoreInterrupt(1);
// update logic
onUpdate();
// redraw canvas
Canvas canvas = null;
try {
canvas = mSurfaceHolder.lockCanvas();
synchronized (mSurfaceHolder) {
onDraw(canvas);
drawFps(canvas);
}
} catch (Exception ignore) {
} finally {
try {
mSurfaceHolder.unlockCanvasAndPost(canvas);
} catch (Exception e) {
Cat.e(e.getMessage());
}
}
sleepIfNeeded();
updateFps();
}
Log.d(TAG, "AnimationThread.run'ed");
}
private long mNextGameTick = System.currentTimeMillis();
/**
* Will only sleep if we need to limit the frame rate to a certain number.
*/
private void sleepIfNeeded() {
if (mTargetFps <= 0) return;
mNextGameTick += 1000 / mTargetFps;
long sleepTime = mNextGameTick - System.currentTimeMillis();
if (sleepTime >= 0) {
sleepIgnoreInterrupt(sleepTime);
} else {
Log.i("GameLoopView", "Failed to reach expected FPS!");
}
}
public void setRunning(boolean state) {
mRun = state;
}
private void updateFps() {
long currentTime = System.currentTimeMillis();
int timeDifference = (int) (currentTime - mLastTime);
mFrameSampleTime += timeDifference;
mFrameSamplesCollected++;
if (mFrameSamplesCollected == 10) {
mFps = ((10 * 1000) / mFrameSampleTime);
mFrameSampleTime = 0;
mFrameSamplesCollected = 0;
}
mLastTime = currentTime;
}
private void drawFps(Canvas canvas) {
if (mDrawFps && mFps != 0) {
int x = getWidth() - getWidth() / 8;
int y = getHeight() - (int) mFpsTextPaint.getTextSize() - 5;
canvas.drawText(mFps + " fps", x, y, mFpsTextPaint);
}
}
}
private AnimationThread mThread = null;
private int mTargetFps = 30;
private boolean mDrawFps = true;
public GameLoopView(Context context, AttributeSet attrs) {
super(context, attrs);
if (isInEditMode()) return;
getHolder().addCallback(this);
setFocusable(true);
}
/**
* Will start a seperate thread that runs the game loop. From within this will
* call onUpdate() and onDraw().
*/
public void startGameLoop() {
Log.d(TAG, "startGameLoop'ing");
// if thread exists, the gameloop is running
if (mThread == null) {
mThread = new AnimationThread(getHolder());
mThread.setRunning(true);
mThread.start();
}
Log.d(TAG, "startGameLoop'ed");
}
/**
* Pauses the gameloop, this can be restared with startGameLoop().
*/
public void pauseGameLoop() {
// only pause a gameloop that is running
if (mThread != null) {
boolean retry = true;
mThread.setRunning(false);
while (retry) {
try {
mThread.join();
retry = false;
} catch (InterruptedException e) {
// swallow exception and retry joining thread
}
}
mThread = null;
}
}
/**
* Set's the frame rate at which the game loop should run. Be conservative and
* implement an efficient onUpate()/onDraw() so this frame rate can be maintaned.
*
* @param fps The frame rate at which the game loop should run, set to zero to
* run as fast as possible.
*/
public void setTargetFps(int fps) {
mTargetFps = fps;
}
/**
* If set to true, the gameloop will display the fps in the bottom right corner.
*
* @param show Flag indicating wheter to show the fps or not.
*/
public void setDrawFps(boolean show) {
mDrawFps = show;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
/* keeps track if the surface is completely created, the gameloop can only
* run if we have a surface, so the gameloop thread has to block until so */
private volatile boolean surfaceCreatedCompleted = false;
@SuppressLint("WrongCall")
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (isInEditMode()) return;
Log.d(TAG, "surfaceCreated'ing");
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
synchronized (holder) {
onDraw(canvas);
}
} finally {
holder.unlockCanvasAndPost(canvas);
}
surfaceCreatedCompleted = true;
Log.d(TAG, "surfaceCreated'ed");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@SuppressLint("WrongCall")
@Override
public void invalidate() {
SurfaceHolder holder = getHolder();
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
synchronized (holder) {
onDraw(canvas);
}
} finally {
holder.unlockCanvasAndPost(canvas);
}
}
/**
* Override this to implement the game logic.
*/
abstract protected void onUpdate();
/**
* Override this to de the drawing.
*/
@Override
abstract protected void onDraw(Canvas canvas);
}