After analyzing different screen resolutions that are used on various Android devices, I decided to create all the game assets in 6 different sizes. However, I worried that this would increase the size of the final .apk file. So, for the first time since I started the project I decided put in all the assets to check.
I created a package containing only the lowest (800x480) and highest (2560x1600) resolution images. It's 106MB!
I needed to add 4 intermediate resolutions, so I was looking at 300+ MB for the final .apk. I searched the docs and found that the limit is 100MB and rest has to be provided as a separate package. Or, you could distribute two different .apk files for different devices. I didn't like any of those. I hate when you download an Android game, and then you have to wait for it to download hundreds of megabytes more. I needed a new approach.
I searched the Internet. Most advices are about packing your code using compressor/obfuscator programs. But the gains are really negligible. Then one thing caught my eye: using JPEG for textures. I used TextureAtlas to pack everything automatically, but I could separate the images with and those without transparent areas and pack them separately as PNG or JPG texture atlases.
The problem is that 90% or the graphics in my game needed transparency. In this particular case the main problem were the card images, which have rounded corners. And then I got this idea: separate the card image into frame and contents. There are only four card types in the game (one for each faction and gold one for the items), and all cards of the same type share the same frame. Inside of the card is different for each one, but it does not require transparency. Instead of packing 126 cards into PNG texture, I would pack 126 cards into JPG texture plus 4 frames into PNG.
When staging the images, make sure the jpg one is drawn first and PNG frame over it.
BTW, by “staging” I mean placing the Images into libGDX Stage. To make the code simpler, I created a CardImage class (descended from libGDX Group) to handle drawing transparently. This Group contains the center of the card from JPG texture and then positions the frame from PNG texture over that.
This saved A LOT, but I got so concerned about the size that I wanted to do something about flipping as well. When card is facing left or right on the board, I would simply flip the image. This would save space, since I wouldn't have to store two full images of the card. I researched if I could only flip a part of the image. It's possible using AtlasRegion.flip() function. But I didn't want to flip the whole JPG image. The textual part should remain the same and only the unit graphics should be mirrored. To do this, after calling flip(true,false) I used setRegionY and setRegionHeight to reduce the flipped region size. Using the flipped region I created another Image and added it to the CardImage group. At runtime, I would simply show or hide this image when I need the unit to face left or right.
Newly created .apk file with the same content was reduced to only 12MB. A 88% save. Once I added all the other resolutions, the final .apk was 42MB:
Compare this with 300MB+ I expected at the beginning.
Beside all the other advice you might find on the web (compressing the code, etc.), add these three:
- use JPG wherever possible
- if you use PNG only for semi-transparent border, cut the image into frame + content. Store the frame in PNG and content in JPG. Or, even better: cut the frame into four pieces (left,right,up,down) and assemble it at runtime. No more empty space in the middle of the frame in PNG. This saves on storage and also uses less textures, so the game will run faster.
- If you have images that are flipped or rotated, store only the original and flip/rotate at runtime.