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 topost()
that would trigger the update and the rendering to be executed again (I also tried this option usingpostOnAnimation()
andpostInvalidateOnAnimation()
instead ofpost()
). - 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 for some reason, I didn’t even think about it until it was too late.
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!
Hi Julio. I found your article interesting and well written. Kudos for publishing it along with making your source code available on GitHub. I downloaded your game. It is fun to play and has very smooth animation. I am dipping my feet into Android game programming and am trying to find the best game loop and surface to use.
I did have a question about your loop logic. I understand you are using View which is hardware accelerated as it runs on the GUI thread.
So your GUI thread does the render, taking advantage of hardware acceleration, while your update thread does the game updates. In your run() method you perform an update() and postInvalidateOnAnimation(), taking a timestamp before and after to determine an update duration.
While this would give you the duration of the update, it does not seem to include the duration of the render since it is processed asynchronously on the GUI thread. I could be wrong, but it seems like you are trying to provide a consistent frame rate via the sleep time. However, if this is the case, I was wondering why if the render time should be included. This could be done by sleeping the update thread for a short interval, waking up and checking a volatile flag set in onDraw() that the render is completed. Then the update thread could still employ the sleep time logic you already included. Of course I may be missing something but just wanted to throw it out there to see if you had any feedback.
Hi, and sorry for the delay in my response, I just now realised this comment was here.
You are right on your assumptions: the main loop tries to keep the framerate consistent by using
sleep()
, and it focuses on updating the game values more than on the updating the game UI (the UI update is triggered but not awaited).As you suggest, this loop could try to wait for the view to finish its rendering, but that would defeat the purpose of having different threads for game updates and UI updates, as we would end up serialising everything: first, the update; then, the rendering; then, another update; etc.
Thing is, this implementation is the one that seemed to make the game more performant in most devices for my game, but it is far from being ideal or even from being an approach I would recommend. In fact, trying to keep the framerate consistent is now discouraged on Android, as new devices are being released with framerates of 90, 120, and more. Thus, although you can use my dumb code as a starting point, please try to look for alternative approaches.
In any case, thank you very much for your comment!
No worries! And I appreciate the feedback! It seems like serialization could still be beneficial if you were targeting a specific FPS. The GUI thread would still be doing some work (capturing input) while the game thread updates the game world. Then the game thread would sleep waiting for the GUI thread to render the screen, and eventually it would update the game world again, and repeat (especially if the update process was significantly faster than the render process). But it seems like you used more of a UPS (updates per second) approach. And perhaps due to the complex physics of your game this needs to be a priority over FPS. And as you said with new devices supporting higher frame rates it may not be wise to code for a static target FPS anyway.
I wound up switching my game to use SurfaceView. I found out that if the version of android is Oreo or later I can use lockHardwareCanvas() instead of lockCanvas(), which then provides hardware acceleration for rendering on the SurfaceView. With this approach the GUI thread can focus on input while my game thread does the updating and rendering. I have a target frame rate of 50 fps and adjust my sleep time as needed. And if things bog down enough I drop frames while continuing the do updates. As you know using SurfaceView provides more control over the render process. If my game becomes too complex I could even split the update logic and render logic out between two thread. Right now I am working on a simple space invaders clone, so probably not necessary. And my experience in game programming is very limited so take anything I say with a large grain of salt – LOL! But I am having fun learning!
Did you try using SurfaceView with hardware acceleration turned on via lockHardwareCanvas() and if so did it make much difference?
Yeah, I guess that if the added time of updating and rendering is smaller or equal than the time you expect each frame to last, it is fine to serialise everything.
Regarding the
SurfaceView
with hardware acceleration, I did try it, but I remember having some issues with it, although I cannot recall the details. What I remember is that the layouts drawn in front of it wouldn’t be displayed in some devices, which was a problem in my case (I show a popup with a normal Android layout in front of the game sometimes).In any case, I am glad that you are having fun developing your game, that’s the most important part. Putting it all together for the first time can be challenging and even frustrating, but when things start to work out, it is very fun indeed!
By the way, you have probably read about it already, but an interesting fact about Space Invaders is that the increase in speed that you get when you kill more enemies wasn’t implemented on purpose. Instead, it is (was) just a consequence of the computer being able to render the game faster as the number of enemies to display gets smaller.
Thanks for the info Julio! Actually I did not know the speed increase for Space Invaders was a simple byproduct of the hardware limitations. LOL! I have made more progress on my Space Invaders clone and it seems to run smooth with SurfaceView. Of course the movement in Space Invaders is very basic. For my next game I may try using LibGDX as a game framework.
I did have a quick question if you have a moment. On your game I noticed you included some advertising. Did you find any really good source of information on how to add Google advertising to an Android app?
Hi Julio. Hope all is well. Please disregard my question up above. I got my simple space invaders game using Android and Java working a while back. But rather than spending time adding more features, advertising, and polish I think I will eventually try to rewrite it using Unity. It will be interesting to see how much easier it is to do it with a game engine and also seems like it will make adding more features easier. Have a great 2021!