0

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();

}
1337ingDisorder
  • 821
  • 1
  • 6
  • 17
  • No experience with android here, but I see that the run method contains a loop that doesn't block (wait) on anything (at least not visibly to me). Is this as it should be? Also, is tenths of a second a good unit of time (dividing nanosecs by 10.000.000)? – laune Jan 17 '15 at 07:36
  • Android doesn't use wait() calls, thanks for the suggestion though! – 1337ingDisorder Jan 17 '15 at 09:30
  • Forgot to mention, this is being done on a Samsung Galaxy II S (not an emulator) and I can run fairly graphics-intensive 2D and 3D games on this device with no lag. (Which makes something like lag during a simple scrolling menu background that much more frustrating hehe) – 1337ingDisorder Jan 17 '15 at 09:31
  • I wasn't suggesting wait or sleep calls. Threads should ideally have a "natural" rescheduling point - receive from an occasionally empty queue, i/o read,... A thread that executes continually is always bad for performance. – laune Jan 17 '15 at 10:06
  • Hmm I recall in the intro-to-java tutorial we used Thread.sleep(int) as a means of regulating framerate but nothing like that is used in any of the Android tutorials in the same series. I also seem to recall reading somewhere that using that kind of thing can have unpredictable results with concurrent threads running, hence the use of deltaTime instead. Perhaps I should research that more thoroughly. – 1337ingDisorder Jan 17 '15 at 11:56
  • But I don't see where that deltaTime is actually used after being passed on to update and paint. – laune Jan 17 '15 at 12:26
  • It gets used in the main game screen class or any other class with an update() method, mainly just in conditionals measuring how much time has passed since the last update() – 1337ingDisorder Jan 17 '15 at 19:03
  • `float deltaTime = (System.nanoTime() - startTime) / 10000000.000f; startTime = System.nanoTime(); if (deltaTime > 3.15) { deltaTime = (float) 3.15; }` you should calculate the elapsed time like this: http://stackoverflow.com/a/23996041/2413303 (that answer was written for LibGDX but you can see the general concept) – EpicPandaForce Jan 23 '15 at 09:31
  • Thanks for the suggestion EpicPandaForce (and btw, awesome username! hehe) although I'm not sure I do see the general concept.. I tried adding a render(float delta) method to my ScreenGame.java class (which extends Screen) and it gave me this error: The method render(float) of type ScreenGame must override or implement a supertype method. – 1337ingDisorder Jan 23 '15 at 19:40
  • I've added the Screen.java class which is being extended to the original post, in case that helps. It doesn't seem to have a render(float) method in it at all, so I'm not sure where the delta figure would come from unless I calculate it on my own. – 1337ingDisorder Jan 23 '15 at 19:52

1 Answers1

0

I'm not sure if this really addresses the root of the problem but I managed to implement what seems like a functional workaround by pre-allocating the entire heap via an array that fills up every spare byte of memory and then immediately making that array unreachable:

ArrayList<Character> mReserve = new ArrayList<Character>();
long maxMem = Runtime.getRuntime().maxMemory();

for (int i = 0; i < maxMem; i++) {
    try {
        mReserve.add('s');
    } catch (OutOfMemoryError e) {
        break;
    }
}
mReserve.clear();
mReserve = null;

It seems to me that this must be a hacky solution -- I can't imagine the recommended course of action is to start throwing elbows and clear off the entire dance floor just so an app can bust a move... But it seems to be working, all the animation on the main menu screen (and in the rest of the game which I've subsequently finished coding) is working great now.

1337ingDisorder
  • 821
  • 1
  • 6
  • 17