Well, I have been busy. My awesome map-based game Caravan has been spinning around a bit, but for a good reason.
In august, I went to XDADevCon in Miami. It was amazing. A big thanks to EVERYONE I met there. One of the things I picked up there was an Oppo Find 5 phone. At the time pretty much the best phone on the market, bar none. The best screen, the best speakers, the best camera. And at developer prices 🙂 Plus, the Oppo guys are really onet he right track; custom ROM support out of thebox, their own recovery; it truly is a developer’s company with an unbeatable product at an insanely low price. You can tell their marketing worked on me 🙂
But there’s the thing. This was my first high res phone. It’s awfully strange having a phone in your hand with a 5 inch screen with the same resolution as a full HD tv. So of course I HAD to try my own programs and games on it. Which worked perfectly … except that my game, Ringi, was a bit choppy.
Now, you”d think that the Qualcom 800 and Adreno 320 would be enough to push the pixels of a not-too-intensive game, right? Wrong.
After some checking, it turns out that the game was fill-rate bound: I could not compute all the pixels necessary to run my game smoothly on 1080p. Now it worked perfectly on my old HTC Desire HD at 800×480. But now it was chugging a bit, and not smooth at all.
So, I spent a lot of time optimising it. I also worked on my great map-based game Caravan, ensuring it worked well at these high resolutions, but my main aim was to get Ringi working butter smoothly, so I could integrate all I would learn into Caravan. And people are playing Ringi, so I owed them a great experience on their new phones, too.
So, what did I do?
Basically, three things. Keep in mind, I didn’t immediately know what was causing the choppiness, so I attacked the most likely culprits first and went on from there.
First thing I went after was my input. I had a hunch that sleeping the input thread for a bit was a cause for the input not being so smooth (a common practice to prevent input flooding; especially older android versions have this problem). I’d come across a post by Robert Green at http://www.rbgrn.net/content/342-using-input-pipelines-your-android-game .This looked like something promising and didn”t involve sleeping/blocking my game so much. So, after implementing this and modifying it and my game heavily for multitouch and all Input events (this involved adding more field to the InputObject), my game did run better.But not enough: I was still getting some hickups.
My thinking then was that I was seeing too much garbage collection. Object creation and release does cause immediate GC on Android, and creates micro stuttering when it triggers. An d one of the leading causes of this object creation/cleanup? Strings. And I used a lot. I created a lot of strings in my draw calls, too, converting integers to strings for my score, time, the floating point scores which occured every time you hit a flowerblock and then floated towards the score. A lot of these strings were created and cleaned up every draw call.
So I got rid of all string creation during runtime. Well, kinda: I loaded it all up at the front for a one-time creation hit. Seeing as I was keeping track of all numbers (score, time, points etc) as int’s anyway, I came up with the perfect solution. At the very start of the game, before any movement, I create a large string array of 10.000 strings which I fill with the Strings for the numbers zero to 9.999. BAM, one-time GC hit, but I never have to create a string anymore. Any time I need to draw a String, I just call the String Array using the tracking int as an index, and I get the corresponding String! No creation or destruction penalty, and all it took was a call to myStringArray[scoreInt] 🙂
This got rid of pretty much ALL my GC calls during gameplay and made the game a lot smoother. And I don’t have to screw around with StringBuilder either, which causes less GC but still wouldn”t eliminate it. The only cost? The String Array uses up a bit of memory … but if you can’t trade 100 to 200kb for NO garbage collection calls, you”re doing something wrong.
However, my game was running smoother, but not yet silky smooth. And this is when I realised I was maxing out my pixel fillrate. One of the reasons was that I had to re-draw my game’s background every drawcall. I had experimented with a workaround to this, by making my SurfaceView transparent and on top of a Layout which had my game background as it’s background. But that meant I HAD to set my SurfaceView’s ZOrder to the top. This worked great, and made my game run very well … bu had the drawback that I could not have ANY other drawing over my SurfaceView. A textbox on top? Not visible. Any widget, anything which got drawn on top would not be visible.
My big regret is that this approach does solve all my problems. Not only that, but I could draw everything pixel perfect at 1080p with no slowdown.
Now, were I to re-do Ringo from scratch, I would design around that problem by doing ALL drawing in the SurfaceView. But that would be a LOT of work to retro-fit Ringi in that fashion. So I did the only thing possible: I restricted the size of my SurfaceView and scaled it.
Basically, if the resolution gets to big, I now intercept the SurfaceView’s “onSurfaceChanged” call, check if the resolution is too high and if it is, I tell the SurfaceView that it is smaller than it actually is by intercepting and scaling it’s reported width and height. It is that simple; that one basic change didn’t even need any other code changes, except for scaling the position of where I draw my touch input (I draw a circle around each finger when you touch the area’s which allow you to rotate the rings).
And the scaling isn’t even that drastic. My ondraw call now renders at about 1500×800 instead of 1920×1080 (bigger screens will scale a bit more) and that is auto-scaled to the screen by the surfaceview itself due to it’s height and width being set to “match_parent”.
And now Ringi runs butter smooth, with NO stutter and no GC. If you look hard, you MIGHT MAYBE be able to tell it is upscaled … but in exchange for a game which runs flawlessly on high rez screens, that’s a trade I had to make, because otherwise the game would not be fun anymore.
And the best thing? These three techniques (input pipeline for multitouch, up-front String creation and what I learned about SurfaceViews) are directly transferable to any other project/game.