I'm quite new to Android development -- I made one game using a tutorial and now I'm working on an original creation, but I didn't get very far without encountering severe lag issues and the only thing that the app does so far is just scrolling a background image.
The game I made with the tutorial doesn't have any lag and I'm using the same framework, but it's lagging quite badly -- to the point of having 3-4 frames per second.
It's worth noting that this lag is intermittent.
It'll be laggy for a few seconds or a couple minutes, then the background scrolling might flow very smoothly as intended for a few seconds or a few minutes, then more lag then more smooth animation etc, and there doesn't seem to be any explanation as to why the framerate is so low sometimes. Looking at the DDMS perspective in Eclipse isn't offering any insight - there's no other apps or syncing happening in the background that are causing CPU or RAM spikes.
Here are the classes which I think are relevant:
ScreenMainMenu.java (displays the main menu with a scrolling background)
package com.kittykazoo.gamecore;
import java.util.List;
import com.kittykazoo.framework.Game;
import com.kittykazoo.framework.Graphics;
import com.kittykazoo.framework.Screen;
import com.kittykazoo.framework.Input.TouchEvent;
public class ScreenMenuMain extends Screen {
boolean firstCreate = true;
int bgPos = 0;
public ScreenMenuMain(Game game) {
super(game);
// Cue music
if (firstCreate) {
// GameAssets.ostMenuMain.play();
firstCreate = false;
}
}
@Override
public void update(float deltaTime) {
if (bgPos >= 1280) {
bgPos = 0;
} else {
bgPos += 1;
}
// Handle Touch Events
List<TouchEvent> touchEvents = game.getInput().getTouchEvents();
int len = touchEvents.size();
for (int i = 0; i < len; i++) {
try {
TouchEvent event = touchEvents.get(i);
if (event.type == TouchEvent.TOUCH_DOWN) {
// Start Game
if (inBounds(event, 168, 441, 463, 145)) {
Assets.click.play(3.0f);
// game.setScreen(new ScreenGame(game));
}
// Options Menu
if (inBounds(event, 168, 624, 463, 145)) {
Assets.click.play(3.0f);
// game.setScreen(new ScreenOptions(game));
}
// Help Screen
if (inBounds(event, 168, 807, 463, 145)) {
Assets.click.play(3.0f);
// game.setScreen(new ScreenHelp(game));
}
// Quit
if (inBounds(event, 100, 1053, 463, 145)) {
Assets.click.play(3.0f);
quitGame();
}
}
} catch (IndexOutOfBoundsException e) {
}
}
}
private boolean inBounds(TouchEvent event, int x, int y, int width,
int height) {
if (event.x > x && event.x < x + width - 1 && event.y > y
&& event.y < y + height - 1)
return true;
else
return false;
}
@Override
public void paint(float deltaTime) {
Graphics g = game.getGraphics();
g.drawImage(Assets.menuMainBg, 0, bgPos);
g.drawImage(Assets.menuMainBg, 0, bgPos - 1280);
g.drawImage(Assets.menuMainInterface, 0, 0);
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
}
@Override
public void backButton() {
// TODO: Display "Exit Game?" Box
quitGame();
}
public static void quitGame() {
android.os.Process.killProcess(android.os.Process.myPid());
}
}
ScreenLoading.java (which calls ScreenMainMenu after loading all assets)
package com.kittykazoo.gamecore;
import com.kittykazoo.framework.Game;
import com.kittykazoo.framework.Graphics;
import com.kittykazoo.framework.Graphics.ImageFormat;
import com.kittykazoo.framework.Screen;
import com.kittykazoo.gamecore.Assets;
public class ScreenLoading extends Screen {
boolean assetsLoaded = false;
public ScreenLoading(Game game) {
super(game);
}
@Override
public void update(float deltaTime) {
loadAssets();
if (assetsLoaded) {
game.setScreen(new ScreenMenuMain(game));
}
}
public void loadAssets() {
Graphics g = game.getGraphics();
// Load all assets from:
// com.kittykazoo.gamecore.Assets.java
Assets.menuMainBg = g.newImage("menu_main_bg.png",
ImageFormat.RGB565);
Assets.menuMainInterface = g.newImage("menu_main_interface.png",
ImageFormat.RGB565);
Assets.click = game.getAudio().createSound("click.ogg");
assetsLoaded = true;
}
@Override
public void paint(float deltaTime) {
Graphics g = game.getGraphics();
g.drawImage(Assets.splash_bg, 0, 0);
}
@Override
public void pause() {
// TODO Auto-generated method stub
}
@Override
public void resume() {
// TODO Auto-generated method stub
}
@Override
public void dispose() {
// TODO Auto-generated method stub
}
@Override
public void backButton() {
// TODO Auto-generated method stub
}
}
And in case it's helpful, here's the framework interfaces and implementations:
Game.java
package com.kittykazoo.framework;
public interface Game {
public Audio getAudio();
public Input getInput();
public FileIO getFileIO();
public Graphics getGraphics();
public void setScreen(Screen screen);
public Screen getCurrentScreen();
public Screen getInitScreen();
}
AndroidGame.java (implementation of Game)
package com.kittykazoo.framework.implementation;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.view.Window;
import android.view.WindowManager;
import com.kittykazoo.framework.Audio;
import com.kittykazoo.framework.FileIO;
import com.kittykazoo.framework.Game;
import com.kittykazoo.framework.Graphics;
import com.kittykazoo.framework.Input;
import com.kittykazoo.framework.Screen;
public abstract class AndroidGame extends Activity implements Game {
AndroidFastRenderView renderView;
Graphics graphics;
Audio audio;
Input input;
FileIO fileIO;
Screen screen;
WakeLock wakeLock;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
boolean isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
int frameBufferWidth = isPortrait ? 800: 1280;
int frameBufferHeight = isPortrait ? 1280: 800;
Bitmap frameBuffer = Bitmap.createBitmap(frameBufferWidth,
frameBufferHeight, Config.RGB_565);
float scaleX = (float) frameBufferWidth
/ getWindowManager().getDefaultDisplay().getWidth();
float scaleY = (float) frameBufferHeight
/ getWindowManager().getDefaultDisplay().getHeight();
renderView = new AndroidFastRenderView(this, frameBuffer);
graphics = new AndroidGraphics(getAssets(), frameBuffer);
fileIO = new AndroidFileIO(this);
audio = new AndroidAudio(this);
input = new AndroidInput(this, renderView, scaleX, scaleY);
screen = getInitScreen();
setContentView(renderView);
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "TEMPLATE");
}
@Override
public void onResume() {
super.onResume();
wakeLock.acquire();
screen.resume();
renderView.resume();
}
@Override
public void onPause() {
super.onPause();
wakeLock.release();
renderView.pause();
screen.pause();
if (isFinishing())
screen.dispose();
}
@Override
public Input getInput() {
return input;
}
@Override
public FileIO getFileIO() {
return fileIO;
}
@Override
public Graphics getGraphics() {
return graphics;
}
@Override
public Audio getAudio() {
return audio;
}
@Override
public void setScreen(Screen screen) {
if (screen == null)
throw new IllegalArgumentException("Screen must not be null");
this.screen.pause();
this.screen.dispose();
screen.resume();
screen.update(0);
this.screen = screen;
}
public Screen getCurrentScreen() {
return screen;
}
}
Graphics.java
package com.kittykazoo.framework;
import android.graphics.Paint;
public interface Graphics {
public static enum ImageFormat {
ARGB8888, ARGB4444, RGB565
}
public Image newImage(String fileName, ImageFormat format);
public void clearScreen(int color);
public void drawLine(int x, int y, int x2, int y2, int color);
public void drawRect(int x, int y, int width, int height, int color);
public void fillRect(int x, int y, int width, int height, int color);
public void drawImage(Image image, int x, int y, int srcX, int srcY,
int srcWidth, int srcHeight);
public void drawImage(Image Image, int x, int y);
public void drawImage(Image Image, int x, int y, Paint paint);
public void drawScaledImage(Image Image, int x, int y, int width,
int height, int srcX, int srcY, int srcWidth, int srcHeight);
public void drawScaledImage(Image Image, int x, int y, int width,
int height, int srcX, int srcY, int srcWidth, int srcHeight, Paint paint);
void drawString(String text, int x, int y, Paint paint);
public int getWidth();
public int getHeight();
public void drawARGB(int i, int j, int k, int l);
}
AndroidGraphics.java (implementation of Graphics)
package com.kittykazoo.framework.implementation;
import java.io.IOException;
import java.io.InputStream;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import com.kittykazoo.framework.Graphics;
import com.kittykazoo.framework.Image;
public class AndroidGraphics implements Graphics {
public static AssetManager assets;
Bitmap frameBuffer;
Canvas canvas;
Paint paint;
Rect srcRect = new Rect();
Rect dstRect = new Rect();
public AndroidGraphics(AssetManager assets, Bitmap frameBuffer) {
this.assets = assets;
this.frameBuffer = frameBuffer;
this.canvas = new Canvas(frameBuffer);
this.paint = new Paint();
}
@Override
public Image newImage(String fileName, ImageFormat format) {
Config config = null;
if (format == ImageFormat.RGB565)
config = Config.RGB_565;
else if (format == ImageFormat.ARGB4444)
config = Config.ARGB_4444;
else
config = Config.ARGB_8888;
Options options = new Options();
options.inPreferredConfig = config;
InputStream in = null;
Bitmap bitmap = null;
try {
in = assets.open(fileName);
bitmap = BitmapFactory.decodeStream(in, null, options);
if (bitmap == null)
throw new RuntimeException("Couldn't load bitmap from asset '"
+ fileName + "'");
} catch (IOException e) {
throw new RuntimeException("Couldn't load bitmap from asset '"
+ fileName + "'");
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
}
}
}
if (bitmap.getConfig() == Config.RGB_565)
format = ImageFormat.RGB565;
else if (bitmap.getConfig() == Config.ARGB_4444)
format = ImageFormat.ARGB4444;
else
format = ImageFormat.ARGB8888;
return new AndroidImage(bitmap, format);
}
@Override
public void clearScreen(int color) {
canvas.drawRGB((color & 0xff0000) >> 16, (color & 0xff00) >> 8,
(color & 0xff));
}
@Override
public void drawLine(int x, int y, int x2, int y2, int color) {
paint.setColor(color);
canvas.drawLine(x, y, x2, y2, paint);
}
@Override
public void drawRect(int x, int y, int width, int height, int color) {
paint.setColor(color);
paint.setStyle(Style.STROKE);
canvas.drawRect(x, y, x + width - 1, y + height - 1, paint);
}
@Override
public void fillRect(int x, int y, int width, int height, int color) {
paint.setColor(color);
paint.setStyle(Style.FILL);
canvas.drawRect(x, y, x + width - 1, y + height - 1, paint);
}
@Override
public void drawARGB(int a, int r, int g, int b) {
paint.setStyle(Style.FILL);
canvas.drawARGB(a, r, g, b);
}
@Override
public void drawString(String text, int x, int y, Paint paint) {
canvas.drawText(text, x, y, paint);
}
public void drawImage(Image Image, int x, int y, int srcX, int srcY,
int srcWidth, int srcHeight) {
srcRect.left = srcX;
srcRect.top = srcY;
srcRect.right = srcX + srcWidth;
srcRect.bottom = srcY + srcHeight;
dstRect.left = x;
dstRect.top = y;
dstRect.right = x + srcWidth;
dstRect.bottom = y + srcHeight;
canvas.drawBitmap(((AndroidImage) Image).bitmap, srcRect, dstRect, null);
}
@Override
public void drawImage(Image Image, int x, int y) {
canvas.drawBitmap(((AndroidImage) Image).bitmap, x, y, null);
}
public void drawImage(Image Image, int x, int y, Paint paint) {
canvas.drawBitmap(((AndroidImage) Image).bitmap, x, y, paint);
}
public void drawScaledImage(Image Image, int x, int y, int width,
int height, int srcX, int srcY, int srcWidth, int srcHeight) {
srcRect.left = srcX;
srcRect.top = srcY;
srcRect.right = srcX + srcWidth;
srcRect.bottom = srcY + srcHeight;
dstRect.left = x;
dstRect.top = y;
dstRect.right = x + width;
dstRect.bottom = y + height;
canvas.drawBitmap(((AndroidImage) Image).bitmap, srcRect, dstRect, null);
}
public void drawScaledImage(Image Image, int x, int y, int width,
int height, int srcX, int srcY, int srcWidth, int srcHeight, Paint paint) {
srcRect.left = srcX;
srcRect.top = srcY;
srcRect.right = srcX + srcWidth;
srcRect.bottom = srcY + srcHeight;
dstRect.left = x;
dstRect.top = y;
dstRect.right = x + width;
dstRect.bottom = y + height;
canvas.drawBitmap(((AndroidImage) Image).bitmap, srcRect, dstRect, paint);
}
@Override
public int getWidth() {
return frameBuffer.getWidth();
}
@Override
public int getHeight() {
return frameBuffer.getHeight();
}
}
Screen.java
package com.kittykazoo.framework;
public abstract class Screen {
protected final Game game;
public Screen(Game game) {
this.game = game;
}
public abstract void update(float deltaTime);
public abstract void paint(float deltaTime);
public abstract void pause();
public abstract void resume();
public abstract void dispose();
public abstract void backButton();
}
And the last class I think may be relevant is this one in the framework:
AndroidFastRenderView.java
package com.kittykazoo.framework.implementation;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class AndroidFastRenderView extends SurfaceView implements Runnable {
AndroidGame game;
Bitmap framebuffer;
Thread renderThread = null;
SurfaceHolder holder;
volatile boolean running = false;
public AndroidFastRenderView(AndroidGame game, Bitmap framebuffer) {
super(game);
this.game = game;
this.framebuffer = framebuffer;
this.holder = getHolder();
}
public void resume() {
running = true;
renderThread = new Thread(this);
renderThread.start();
}
public void run() {
Rect dstRect = new Rect();
long startTime = System.nanoTime();
while (running) {
if (!holder.getSurface().isValid())
continue;
float deltaTime = (System.nanoTime() - startTime) / 10000000.000f;
startTime = System.nanoTime();
if (deltaTime > 3.15) {
deltaTime = (float) 3.15;
}
game.getCurrentScreen().update(deltaTime);
game.getCurrentScreen().paint(deltaTime);
Canvas canvas = holder.lockCanvas();
canvas.getClipBounds(dstRect);
canvas.drawBitmap(framebuffer, null, dstRect, null);
holder.unlockCanvasAndPost(canvas);
}
}
public void pause() {
running = false;
while (true) {
try {
renderThread.join();
break;
} catch (InterruptedException e) {
// retry
}
}
}
}
Any insight as to what's causing the slowdowns would be greatly appreciated!
EDIT: Still hoping someone out there can explain why this is happening. I have numerous other games on the same device which are much more resource intensive and don't lag at all, so I can only assume there's some fundamental thing that should be in all games to make them run smoothly which I've missed or was not included in any of the tutorials I read.
Am I supposed to reserve additional memory at runtime? Force some flag that I'm not forcing? This is really driving me nuts.
EDIT 2 (re EpicPandaForce's suggestion): Here's the Screen.java used in the framework I'm using (from the tutorials). The framework is based on the open source framework from the book "Beginning Android Games" by Mario Zechner of LibGDX.
public abstract class Screen {
protected final Game game;
public Screen(Game game) {
this.game = game;
}
public abstract void update(float deltaTime);
public abstract void paint(float deltaTime);
public abstract void pause();
public abstract void resume();
public abstract void dispose();
public abstract void backButton();
}