zeno dhaene zeno dhaene - 5 months ago 43
Android Question

Android game programming - onDraw() stuttering

I am creating an animation, and basically a ball is moving on the screen. However, I am experiencing stuttering, making the game less playable. Please have a look at my code, any pointing in the right direction is very welcome!

GameActivity.java -> starts the game loop, and handles updating.

Thread gameThread = new Thread(this);

public boolean running = true;

private final static int MAX_FPS = 60;
private final static int MAX_FRAME_SKIPS = 5;
private final static int FRAME_PERIOD = 1000000000/MAX_FPS;

private final static int FPS_COUNTER_REPORTTIME = 1000000000;
long lastFPSReport;
int UPS; //updates per second
public static int DPS; //draws per second

public SurfaceView gameView;

public void init() {
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();

Point screenDim = new Point();
display.getRealSize(screenDim);

width = screenDim.x;
height = screenDim.y;

initGame();

gameThread.start();
}

public void initGame() {
fingerX = (width / 2);
fingerY = height * 0.90f;

GamePlatform platform = new GamePlatform(fingerX, fingerY, width * 0.3f, height * 0.025f, 0, 0);
GameBall ball = new GameBall(fingerX - 50, height * 0.8f - 50, 100, 100, 20, 20);
}

@Override
public void run() {
Canvas canvas = null;

Log.d("testlog", "Starting game loop");

lastFPSReport = System.nanoTime();

while(running) {
beginTime = System.nanoTime();
framesSkipped = 0;

try{
canvas = gameView.getHolder().lockCanvas();

update();

synchronized (gameView.getHolder()) {
gameView.postInvalidate();
}

timeDiff = System.nanoTime() - beginTime;
sleeptime = (int)(FRAME_PERIOD - timeDiff);

long sleeptimeMillis = (long) (Math.floor(sleeptime / 1000000));
int sleeptimeNanos = (int)(sleeptime - (sleeptimeMillis * 1000000));

if(sleeptime > 0) {
try{
Thread.sleep(sleeptimeMillis, sleeptimeNanos);
}catch(InterruptedException e) {}
}

while (sleeptime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
//only update

update();

sleeptime += FRAME_PERIOD;
framesSkipped++;
}
}finally {
if(canvas != null) {
gameView.getHolder().unlockCanvasAndPost(canvas);
}
}
}
}

public void update() {
UPS++;

//FPS counter
if(System.nanoTime() - lastFPSReport > FPS_COUNTER_REPORTTIME) {
Log.d("testlog", "UPS: " + UPS + " | DPS: " + DPS);
lastFPSReport = System.nanoTime();
UPS = 0;
DPS = 0;
}

//Entities
GameEntity.updateAllEntities();
}


GameBall.java -> handles updating and drawing the ball, extends GameEntity.java (see below)

public class GameBall extends GameEntity {

public GameBall(float x, float y, float width, float height, float dx, float dy) {
super(x, y, width, height, dx, dy, "ballMain");
}

@Override
public void update() {
ArrayList<GameEntity> entityList = getEntityList();

//Collision

for(int i = 0; i < entityList.size(); i++) {
GameEntity entity = entityList.get(i);

if(entity.getType() != "ballMain") {
if(this.getX() < entity.getX() + entity.getWidth() && this.getX() + this.getWidth() > entity.getX() && this.getY() < entity.getY() + entity.getHeight() && this.getY() + this.getHeight() > entity.getY()) {
if(Math.abs(this.getDx()) < Math.abs(entity.getDx())) this.setDx(entity.getDx());
if(Math.abs(this.getDy()) < Math.abs(entity.getDy())) this.setDy(entity.getDy());

if(this.getDx() > 20) entity.setDx(20);
else if(this.getDx() < -20) entity.setDx(-20);
if(this.getDy() > 20) entity.setDy(20);
else if(this.getDy() < -20) entity.setDy(-20);
}
}
}

this.setX(this.getX() + this.getDx());
this.setY(this.getY() + this.getDy());

//walls

if(this.getX() + this.getWidth() >= GameActivity.width || this.getX() <= 0) {
this.setDx(-this.getDx());
}

if(this.getY() <= 0) {
this.setDy(-this.getDy());
}else if(this.getY() + this.getHeight() >= GameActivity.height) {
Log.d("testlog", "game over");
this.setDy(-this.getDy());
}
}

@Override
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setColor(Color.BLUE);

canvas.drawOval(new RectF(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight()), paint);
}
}


GameEntity.java:

public abstract class GameEntity {

float x = 0, y = 0, width = 0, height = 0;
float dx = 0, dy = 0;
String type;

static ArrayList<GameEntity> entityList = new ArrayList();

public GameEntity(float x, float y, float width, float height, float dx, float dy, String type) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.dx = dx;
this.dy = dy;
this.type = type;

entityList.add(this);
}

public static void updateAllEntities() {
for(int i = 0; i < entityList.size(); i++) {
entityList.get(i).update();
}
}

public void update() {
this.x += this.dx;
this.y += this.dy;
}

public static void drawAllEntities(Canvas canvas) {
for(int i = 0; i < entityList.size(); i++) {
entityList.get(i).draw(canvas);
}
}

public abstract void draw(Canvas canvas);

//Getters and Setters

public ArrayList<GameEntity> getEntityList() {
return entityList;
}

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

public void setX(float x) {
this.x = x;
}

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

public void setY(float y) {
this.y = y;
}

public float getWidth() {
return this.width;
}

public void setWidth(float width) {
this.width = width;
}

public float getHeight() {
return this.height;
}

public void setHeight(float height) {
this.height = height;
}

public Point getAbsCenter() {
return new Point((int)(this.x + (this.width / 2)), (int)(this.y + (height / 2)));
}

public Point getSurfaceCenter() {
return new Point((int)(this.x + (this.width / 2)), (int)(this.y));
}

public float getDx() {
return this.dx;
}

public void setDx(float speed) {
this.dx = speed;
}

public float getDy() {
return this.dy;
}

public void setDy(float speed) {
this.dy = speed;
}

public String getType() {
return this.type;
}

public void setType(String type) {
this.type = type;
}
}


Example of the FPS counter log:

06-24 16:40:43.304 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 51 | DPS: 46
06-24 16:40:44.308 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 55 | DPS: 55
06-24 16:40:44.786 30689-30793/com.zenodhaene.keepitup D/testlog: game over
06-24 16:40:45.324 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 49 | DPS: 49
06-24 16:40:46.330 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 53 | DPS: 53
06-24 16:40:46.566 30689-30793/com.zenodhaene.keepitup D/testlog: game over
06-24 16:40:47.345 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 54 | DPS: 54
06-24 16:40:48.344 30689-30793/com.zenodhaene.keepitup D/testlog: game over
06-24 16:40:48.363 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 56 | DPS: 56
06-24 16:40:49.490 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 50 | DPS: 19
06-24 16:40:50.213 30689-30793/com.zenodhaene.keepitup D/testlog: game over
06-24 16:40:50.514 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 60 | DPS: 0
06-24 16:40:51.516 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 63 | DPS: 1
06-24 16:40:51.855 30689-30793/com.zenodhaene.keepitup D/testlog: game over
06-24 16:40:52.522 30689-30793/com.zenodhaene.keepitup D/testlog: UPS: 62 | DPS: 44

Answer

As a rule of thumb, you should absolutely avoid allocating / deallocating memory in your onDraw().

You are using it to create a Paint object. Since onDraw() is called up to 60 times per second, the performance hit of method creation there is huge. Just move your Paint object to a field of the class and initialise it once. This will give you a nice save for starters.

Yan is absolutely spot on regarding the updates. Using some kind of interpolation will give you a smoother animation. There is a great article you can take a look at to optimise your game thread.

Comments