I just started with game development in android, and I'm working on a super simple game.
The game is basically like flappy bird.
I managed to get everything to work, but I get a lot of stutters and lags.
The phone I'm using for testing is LG G2, so it should and does run games much heavier and complex than this.
Basically there are 4 'obstacles' that are a full screen width apart from each other.
When the game starts, the obstacles start to move (toward the character) at a constant speed. The player character's x value is consistent throughout the whole game, while its y value changes.
The lag occurs mainly when the character passes through an obstacle (and sometimes a little after that obstacle too). What happens is that there are uneven delays in each drawing of the game state causing stutters in the movements.
- GC does not run according to the log.
- The stutters ARE NOT caused by the speed being too high (I know that because at the beginning of the game when the obstacles are out of view the character moves smoothly)
- I don't think the problem is FPS related too, because even when the MAX_FPS field is set to 100 there are still stutters.
My thought is that there is a line or multiple lines of code that cause some kinda of delay to happen (and thus frames skipped). I also think that these lines should be around the update()
and draw()
methods of PlayerCharacter
, Obstacle
, and MainGameBoard
.
The problem is, I'm still new to android development and android game development specifically, so I have no idea what could cause such delay.
I tried looking online for answers... Unfortunately, all of which I found pointed over to the GC being to blame. However, such I don't believe it it the case (correct me if I'm being wrong) those answers do no apply to me. I also read the android developer's Performance Tips
page, but couldn't find anything that helped.
So, please, help me find the answer to solving these annoying lags!
Some code
MainThread.java:
public class MainThread extends Thread {
public static final String TAG = MainThread.class.getSimpleName();
private final static int MAX_FPS = 60; // desired fps
private final static int MAX_FRAME_SKIPS = 5; // maximum number of frames to be skipped
private final static int FRAME_PERIOD = 1000 / MAX_FPS; // the frame period
private boolean running;
public void setRunning(boolean running) {
this.running = running;
}
private SurfaceHolder mSurfaceHolder;
private MainGameBoard mMainGameBoard;
public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
super();
mSurfaceHolder = surfaceHolder;
mMainGameBoard = gameBoard;
}
@Override
public void run() {
Canvas mCanvas;
Log.d(TAG, "Starting game loop");
long beginTime; // the time when the cycle begun
long timeDiff; // the time it took for the cycle to execute
int sleepTime; // ms to sleep (<0 if we're behind)
int framesSkipped; // number of frames being skipped
sleepTime = 0;
while(running) {
mCanvas = null;
try {
mCanvas = this.mSurfaceHolder.lockCanvas();
synchronized (mSurfaceHolder) {
beginTime = System.currentTimeMillis();
framesSkipped = 0;
this.mMainGameBoard.update();
this.mMainGameBoard.render(mCanvas);
timeDiff = System.currentTimeMillis() - beginTime;
sleepTime = (int) (FRAME_PERIOD - timeDiff);
if(sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {}
}
while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
// catch up - update w/o render
this.mMainGameBoard.update();
sleepTime += FRAME_PERIOD;
framesSkipped++;
}
}
} finally {
if(mCanvas != null)
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
MainGameBoard.java:
public class MainGameBoard extends SurfaceView implements
SurfaceHolder.Callback {
private MainThread mThread;
private PlayerCharacter mPlayer;
private Obstacle[] mObstacleArray = new Obstacle[4];
public static final String TAG = MainGameBoard.class.getSimpleName();
private long width, height;
private boolean gameStartedFlag = false, gameOver = false, update = true;
private Paint textPaint = new Paint();
private int scoreCount = 0;
private Obstacle collidedObs;
public MainGameBoard(Context context) {
super(context);
getHolder().addCallback(this);
DisplayMetrics displaymetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
height = displaymetrics.heightPixels;
width = displaymetrics.widthPixels;
mPlayer = new PlayerCharacter(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher), width/2, height/2);
for (int i = 1; i <= 4; i++) {
mObstacleArray[i-1] = new Obstacle(width*(i+1) - 200, height, i);
}
mThread = new MainThread(getHolder(), this);
setFocusable(true);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mThread.setRunning(true);
mThread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Surface is being destroyed");
// tell the thread to shut down and wait for it to finish
// this is a clean shutdown
boolean retry = true;
while (retry) {
try {
mThread.join();
retry = false;
} catch (InterruptedException e) {
// try again shutting down the thread
}
}
Log.d(TAG, "Thread was shut down cleanly");
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN) {
if(update && !gameOver) {
if(gameStartedFlag) {
mPlayer.cancelJump();
mPlayer.setJumping(true);
}
if(!gameStartedFlag)
gameStartedFlag = true;
}
}
return true;
}
@SuppressLint("WrongCall")
public void render(Canvas canvas) {
onDraw(canvas);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GRAY);
mPlayer.draw(canvas);
for (Obstacle obs : mObstacleArray) {
obs.draw(canvas);
}
if(gameStartedFlag) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(100);
canvas.drawText(String.valueOf(scoreCount), width/2, 400, textPaint);
}
if(!gameStartedFlag && !gameOver) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(72);
canvas.drawText("Tap to start", width/2, 200, textPaint);
}
if(gameOver) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(86);
canvas.drawText("GAME OVER", width/2, 200, textPaint);
}
}
public void update() {
if(gameStartedFlag && !gameOver) {
for (Obstacle obs : mObstacleArray) {
if(update) {
if(obs.isColidingWith(mPlayer)) {
collidedObs = obs;
update = false;
gameOver = true;
return;
} else {
obs.update(width);
if(obs.isScore(mPlayer))
scoreCount++;
}
}
}
if(!mPlayer.update() || !update)
gameOver = true;
}
}
}
PlayerCharacter.java:
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap, (float) x - (mBitmap.getWidth() / 2), (float) y - (mBitmap.getHeight() / 2), null);
}
public boolean update() {
if(jumping) {
y -= jumpSpeed;
jumpSpeed -= startJumpSpd/20f;
jumpTick--;
} else if(!jumping) {
if(getBottomY() >= startY*2)
return false;
y += speed;
speed += startSpd/25f;
}
if(jumpTick == 0) {
jumping = false;
cancelJump(); //rename
}
return true;
}
public void cancelJump() { //also called when the user touches the screen in order to stop a jump and start a new jump
jumpTick = 20;
speed = Math.abs(jumpSpeed);
jumpSpeed = 20f;
}
Obstacle.java:
public void draw(Canvas canvas) {
Paint pnt = new Paint();
pnt.setColor(Color.CYAN);
canvas.drawRect(x, 0, x+200, ySpaceStart, pnt);
canvas.drawRect(x, ySpaceStart+500, x+200, y, pnt);
pnt.setColor(Color.RED);
canvas.drawCircle(x, y, 20f, pnt);
}
public void update(long width) {
x -= speed;
if(x+200 <= 0) {
x = ((startX+200)/(index+1))*4 - 200;
ySpaceStart = r.nextInt((int) (y-750-250+1)) + 250;
scoreGiven = false;
}
}
public boolean isColidingWith(PlayerCharacter mPlayer) {
if(mPlayer.getRightX() >= x && mPlayer.getLeftX() <= x+20)
if(mPlayer.getTopY() <= ySpaceStart || mPlayer.getBottomY() >= ySpaceStart+500)
return true;
return false;
}
public boolean isScore(PlayerCharacter mPlayer) {
if(mPlayer.getRightX() >= x+100 && !scoreGiven) {
scoreGiven = true;
return true;
}
return false;
}
synchronize
and keeping the canvas locked while the main thread is sleeping. This is asking for all kinds of trouble. Your animation loop doesn't make much sense. – Dielectric