Some lessons I learned from creating a simple Android game without Unity

As I already announced in Twitter, I have recently published a casual Android game called Keepie Uppie (download link here). In this game, the goal is to keep a ball up in the air for as long as possible by tapping on it, just as in a real game of keepie uppie.

While it is true that the game is very simple, developing it on native Android allowed me to learn a little about how to create custom views from scratch by drawing their content directly onto the canvas – as a matter of fact, learning about this was the main reason why I created the game in the first place.

The code of the game is available here. The way it has been implemented is easily deductible from the code, but I will copy some points from the readme file to summarise and clarify the game implementation:


Game loop

The game loop is implemented with an infinite while loop that iterates a maximum of 60 times per second within an always running thread.

This loop takes care of updating all the values and triggering an asynchronous update of the game view by calling postInvalidateOnAnimation().

Game view

The game view is an extension of View in which the onDraw() method has been implemented to care of drawing the whole game onto the canvas over and over again.

Appearance

To ensure the game looks the same in all screens and adapts to certain screen changes, the position, size and speed of all the assets are always relative to the size of the entire game view (many of these values are recalculated on each iteration of the game loop).

State saving/restoration

There are four different states (Tutorial, Start, Play and Game Over), and each one of them has several properties whose values get saved and restored whenever the user leaves the application and then goes back to it.

Pause

Part of the state saving/restoration mechanism is also used to allow the user to pause and resume the game.


Given that the code is available and the key points of the implementation have already been explained, I will not detail each one of the pieces that make up the game. Instead, I would like to focus on talking about a major blocker I encountered during the development process: the performance issues.

But before I get to that, I also want to mention how this game started.

Hello Android game development world!

With no idea about Android game development, or game development in general, but a mental specification about the little game I wanted created, I bought a book on the matter of Android game development (‘The Beginner’s Guide to Android Game Development‘, by James S. Cho).

This book would initially focus on explaining pure Java game development to people with very little experience with the language, and then later it would briefly mention Android development using very outdated information. Thus, I found it to be rather useless for me, which is why I barely read any of it.

However, I did look for the complete source code of the game described in it, as I assumed that checking it out would actually be of some help. Indeed, it was.

I found the code available for download, and I mimicked it in my application to include the game view, the game loop, my game states, my game models, the drawing helper and the assets loader. I would eventually modify and improve most of the original classes, but the general ideas of my implementation, as well as the skeleton of its game framework, come directly from the example included in the book mentioned above.

That was easy, wasn’t it?

I was able to implement most of the game in a very short period of time, not because I did an amazing job at implementing it, but just because it was a very simple game. Unfortunately, as soon as I started testing it, I stumbled upon the performance problems.

For instance, in my OnePlus 5T the game was laggy and would stutter every few seconds, meaning that it wasn’t really playable. But why?

As anyone would do, I immediately assumed that my messy code was a disaster and started planning on running in circles while screaming in panic. Luckily, I started looking for solutions instead.

Correcting general issues

One of the first things I did was to change the bitmap configuration from Bitmap.Config.ARGB_444 to Bitmap.Config.RGB_565, as the latter is more optimised because it uses fewer bits to store graphic information. Besides, Bitmap.Config.ARGB_444 is deprecated since the API 13.

This seemed to improve the performance slightly, but the occasional stutters where still occurring and the frame rate was still lower than it should.

Another thing I noticed was that all image assets were being loaded at their full size and then later resized just before being drawn. To avoid performing so many costly resizing operations over and over, I updated the implementation to make it pre-scale all the assets by re-scaling them to their final size as soon as they were loaded into memory. Just like the previous change did, this modification improved the general performance (i.e., the game was running smoother), but the intermittent stuttering was still persisting.

Then, just when I was starting to get hopeless about finding a solution, I discovered that the original implementation specified in the book was actually keeping two canvas in memory: the current one, created automatically by the operating system; and another one, created manually in code. This immediately rang my alarms.

The original implementation, I realised, was copying the bitmap from one canvas to the other every time a new frame was to be rendered, which was causing the performance of the game to suffer greatly. Luckily, as soon as got rid of the unnecessary, manually-created canvas in order to stop the bitmap from being copied over and over, the stuttering finally went away.

The lesson here is is that, as a general rule, you should avoid doing too much work inside onDraw(), as that will slow down your app and even make it stutter. This applies to all kind of unnecessary operations performed inside this method, but there is one kind in particular that should be avoided at all costs: memory allocation (it could trigger a garbage collection, which in turn could cause your app to stutter!).

Taking care of the game loop

Whilst at this point the stuttering was no longer a problem, the game was still laggy. And since this little game is simple enough to be played smoothly almost in any device, it was clear that something wasn’t quite right in my code.

I drew my attention to the game loop, originally implemented this way:

@Override
public void run() {
    long updateDurationMillis = 0;
    long sleepDurationMillis = 0;
    long beforeUpdateRender = 0; 
    long deltaMillis = 0;
    while (running) {
        beforeUpdateRender = System.nanoTime();
        deltaMillis = sleepDurationMillis + updateDurationMillis;

        updateGame(deltaMillis);
        renderGame();

        updateDurationMillis = (System.nanoTime() - beforeUpdateRender) / 1000000L;
        sleepDurationMillis = Math.max(2, 17 - updateDurationMillis);
        try {
            Thread.sleep(sleepDurationMillis);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Why was the thread going to sleep sometimes in order to limit the game to have a maximum of 60 FPS?

Why is that both the game calculations and the drawing (rendering) were being performed in the same thread?

It was time to change this implementation for the better.

I tried many different options, including several combinations of the following ones:

  • Having different threads for the rendering and the updating.
  • Removing the sleeping time.
  • Replacing the while loop with a call to post() that would trigger the update and the rendering to be executed again (I also tried this option using postOnAnimation() and postInvalidateOnAnimation() instead of post()).
  • Implementing a TimerTask to schedule the game updates.

Some of the implementation changes that I tried out did provide the game with a huge performance boost in certain devices, but none of them worked perfectly in all phones. This is, every time the game was running smoother in a device after a change, there would another device in which it would run slower than before.

Unable to find an ultimate, universal game loop implementation, I settled for one solution that seemed to work, if not perfectly, at least good enough in all my test devices:

@Override
public void run() {

    long timeBeforeUpdate = 0; 
    long updateDuration = 0; 
    long deltaMillis = 0;
    long timeAfterUpdate = System.nanoTime();

    while (isRunning) {
        timeBeforeUpdate = System.nanoTime();
        updateDuration = timeBeforeUpdate - timeAfterUpdate;
        deltaMillis = updateDuration / 1000000L;

        updateGame(deltaMillis);
        postInvalidateOnAnimation();

        timeAfterUpdate = System.nanoTime();

        long sleepTime = Math.max(2, 17 - (timeAfterUpdate - timeBeforeUpdate));
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Override
protected void onDraw(Canvas canvas) {
    renderGame(canvas);
}

Coincidentally, or maybe not, this final implementation is very similar to the original one. However, in this one the rendering and the updates are being performed in different threads (postInvalidateOnAnimation() schedules an execution of the method onDraw() in the next display frame without waiting for it to happen).

Hasta la vista, SurfaceView

While trying to improve the code to make the game run faster, another detail that at some point came to my attention was that the game view was a SurfaceView.

In theory, this view is ideal for fast drawing because it renders independently of the rest of the app. This means that we can draw on it from a secondary thread without being slowed down by any other drawing that the app might be undergoing. Perfect for a game, one would think.

As great as it sounds, though, the advantages of the SurfaceView come at a cost:

  • It doesn’t always work well when it overlaps with or gets overlapped by other views.
  • It doesn’t use hardware acceleration.

The first point was a problem in my case because I wanted to be able to show a dialog in front of the game so the user could share their last score – when using a SurfaceView to draw the game, this dialog would be displayed in front of it in some versions of Android, but behind of it in others.

The second point was even more important, as it was the cause of an interesting paradox: in some cases, the game would run smoothly in old devices and laggy in modern ones. This might sound odd and counterintuitive, but it actually makes sense.

If you think about it, the lower the screen resolution, the less number of pixels involved in the operations that are needed for rendering. Thus, software rendering is easier to do in old devices than it is in modern ones, as the modern ones have a much larger number of pixels to take care of!

I switched from SurfaceView to View, moved the rendering code inside onDraw()… and voilà, the game finally started working smoothly in all devices, including the modern ones.

Debug ≠ Release

When I thought I was almost done with all the necessary testing, I discovered that the game would behave differently when built in release. This might sound obvious to those who are more used to developing games, but it came as a surprise to me.

In release, the game felt smoother because its frame rate was actually higher than in debug, but the overall speed of its physics was lower. As the ball would move at a slower speed when the game was built in release, I had to adjust some values to keep the game experience exactly they way I wanted it.

So, is it worth implementing a game on native Android?

If you want to learn a little bit more about Android, then go for it. But if your goal is to create a mobile game without hassle, then absolutely not.

The performance issues are just the tip of the iceberg. Along with them, there is the fact that you will have to take care of everything manually, which will make you waste both time and energy (for example, this game has very simple physics, but had they been more complicated, I would have struggled a lot to get them implemented). In addition, boilerplate code will probably be all over the place, and your game will exist in one mobile platform only. Unless you just want to learn more Android, what is the point?

I can only think of one exception: if your game is not so simple and you know OpenGL well, then there is a chance that you can take advantage of developing specifically for Android. But the internet seems to be unanimous about how hard it is to master OpenGL, so unless you are an expert on it already, in order to create a normal game you are probably better off just using a solid development framework like Unity.

Final disclaimer

There are probably many things that I did wrong, so please don’t take this article as advice. I just wanted to detail some of the steps I followed to fix my game and some of the lessons I learned after a painful process of blind trial and error, but keep in mind that I still lack a lot of knowledge in this area.

If you have any suggestions or remarks, feel free to leave your feedback in the comments section below!