Over the past year I have been working on Clear For Action: a game for iOS and Android. The game was developed using Cocos2D-X, a free and open source game engine under the care of Chukong Technologies, a Chinese outfit located in Beijing. I decided upon Cocos2D-X because it was multi-platform, had a large community to draw knowledge from and I wanted some hands on experience working with C++, which is still the principle language used in game development. My thinking was: if the game flops, which, lets not kid ourselves, is more than likely in the overcrowded mobile market, I would at least have some good experience to draw from.
This game represents a lot of firsts for me; first time profession programming; first time doing 2D art; first 2D game; first time marketing a game; the list goes on. I am a level designer by trade so most of my programming experience thus far has been a lot of scripting and some C++ the learned at university over 10 years. This being my first ever solo developed game I feel like I have hit almost every conceivable obstacle imaginable, so in the blog entry I’m going to cover various lessons learned during development and try to impart some useful tidbits of information to help others.
So to begin lets list the applications I used to build the app:
- Macbook Air
- Cocos2d-x 3.0 rc
- Xcode (iOS)
- Luxology Modo
- Adobe Photoshop
- Adobe Illustrator
- GIMP (Mac)
- Texture Packer
- pvr tool
- QuickTime Player (Capture)
- Adobe Premiere (Editing)
On Integrating Third Party Libraries:
When I first started using Cocos2D-X I was under the assumption that I would just program everything in C++ and not even have to touch the Java or Objective C side of things. That turned out to be an extremely naive assumption. Sure, you can make a game without touching the native interface, but will you be able to monetize your game using any of the hundreds of third party libraries out there?.. No.. not really. There is Plugin-X now which integrates some advertising and social API’s but most likely want to integrate some other library. Furthermore Cocos2D-X does not have an interface for some device specific things like local notifications, checking network reachability… or in app purchases for that matter.
I recommend downloading the Avalon code. They have integrations for the App Store on Android and iOS as well as wrappers for quite a few popular advertising API’s. Most of the code is out of date, but it should provide a good starting point for your own integrations: https://github.com/hovergames/avalon
iOS was my primary dev platform during development. I had a Macbook Air and a fourth generation iPod Touch so it made sense to go with iOS. After starting the Android version many months later that decision turned out to be exactly the right thing to do. Xcode builds faster and the simulator will be up and running within a couple seconds. You can also test things like in app purchases and game center in the simulator, which you can’t on Android. Actually, I think my Facebook integration was the only thing I couldn’t test in the simulator.
The downside is that I found Objective-C a little harder to learn than Java. Ok.. in hindsight it’s dead simple really, but coming from C++ the syntax is bizarre. Whats with all these square brackets and @ symbols all over the place? The good thing is you can mix C++ and Objective-C using Objective-C++ which is a far easier to use and test than Java Native Interface on Android.
Below I have written what a C++ class looks like in Objective-C and vise versa.
Developing on Android using the NDK certainly isn’t as pleasant an experience as developing for iOS. I won’t go into specific details but for various reasons running and debugging in Eclipse is about 3x to 4x times slower than Xcode. I wrote three paragraphs listing exactly why I dislike developing for Android but upon reading I sounded like a whiny little… child, so I deleted them. I haven’t tried Android Studio yet but it’s definitely on my todo list for next time. Here are a few tips:
I recommend downloading NVIDIA’s Tegra Android Development Pack it contains all the tools you’ll need for developing on Android with one exception: at time of writing Cocos2D-X is only compatible with the rd9 version of the NDK which you can get here: https://dl.google.com/android/ndk/android-ndk-r9d-darwin-x86_64.tar.bz2 (Mac)
On Mac to get variables to show in the debugger and breakpoints to work I added the following line to Application.mk
APP_CPPFLAGS += -DCOCOS2D_DEBUG=1 -O0 -g -gdwarf-2
Using wildcards are a lot easier to use than adding all .cpp files to the Android.mk individually.
CLASSES_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) CLASSES_FILES += $(wildcard $(LOCAL_PATH)/../../Classes/*/*.cpp) CLASSES_FILES += $(wildcard $(LOCAL_PATH)/../../Classes/*.cpp) CLASSES_FILES := $(CLASSES_FILES:$(LOCAL_PATH)/%=%) LOCAL_SRC_FILES += $(CLASSES_FILES)
To interface Java and C you need to use Java Native Interface (JNI). I have included a couple of code examples below.
Some useful terminal commands for Mac:
# kill your app adb shell am force-stop com.your.game # restart ddms server (sometimes a device won't show up unless I do this) adb kill-server # uninstall your app adb uninstall com.your.game # install your app adb install ~/dir/to/your/app.apk # show a list of devices adb devices # debug crash call stack adb logcat | $NDK_ROOT/ndk-stack -sym your/cocos2d/project/dir/proj.android/obj/local/armeabi # get sh1a key for keystore keytool -exportcert -alias my.alias -keystore my.keystore -list -v # sign your .apk when ready to upload to google play jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -storepass my.password -keystore my.keystore my.apk my.keypasswd # I change my release keystore alias and password to the debug defaults. This makes debugging in app purchases a lot easier from Eclipse . # change the alias of a keystore keytool -changealias -keystore my.keystore -alias my_name -destalias my_new_name # change the password of a keystore keytool -keypasswd -keystore my.keystore -alias my_alias
My final tip is not to do anything that would result in a GL call in ApplicationDidEnterBackground. If you do you will get a “GL Context Lost” error and bad things will happen (i.e. black and invisible textures followed by a crash). Because of how Activities work on android ApplicationDidEnterBackground and ApplicationWillEnterForeground get called a lot more often than on iOS. Something to keep in mind anyway.
On Save Games:
I was happily using Cocos2D-X UserDefault for my save game for most of the project. You can save quite a few Mb on iOS and not even notice a hitch. Like most things on this project, as soon as I got to the Android version the whole thing went pear shaped. The main problem is that saving to SD card on Android is slow. And I mean really really slow. UserDefault(iOS) and SharedPreference(Android) use XML which is not the most compact form of save game data and when it comes to saving the SD card smaller file = better. To solve this problem I had a brief flirtation with the Boost serialization library but had a lot of trouble compiling a framework for Xcode. I did eventually get it to compile but by that time I had SQLite working. What was almost a 18 Mb XML file got reduced to a < 100 Kb sqlite file. Problem solved. To be honest Boost serialization looks like a better and far more simple solution for a small game, but I will leave that for next time. I have added some code snippets below. More info at: https://www.sqlite.org
Modified build script for Boost Serialization 1.56.0 on iOS (Mac):
On Load Times:
In order to save memory I have an empty intermediate load scene between each active scene. This allows me to dump everything out of memory before I load up the next scene. I fade to black in 0.125 seconds swap in my black load scene and then fade in the next scene. This means I have less than a second to load in the next scene. Anything longer than that feels very sluggish. Fortunately textures load in fairly fast. Unfortunately loading Sprite sheets using SpriteFrameCache is slow. And I mean really really slow. So if you have a big texture with lots of little sprite frames on it I would recommend just keeping it around in memory. Originally, because I have an empty intermediate load scene, I just used removedAllUnusedSpriteFrames and dumped all the sprite frames. After a while it took several seconds to change scene. I thought I was just loading a lot of textures but after a little bit of profiling SpriteFrameCache turned out to be the biggest offender for slow load times.
The only algorithm I implemented of any particular note was dijkstra’s algorithm. I used Dijkstras to find the shortest path between each town, allowing the boats to sail around the map without colliding into islands and the like. I had an obscenely difficult time understanding such a simple algorithm too. For some reason I couldn’t wrap my head around the fact that you had to first load your graph with values and then trace back from the destination node back to the source node. One of the problems was the lack of a simple, easy to understand C++ example. There was one written in C, but the author seemed to love using the most incomprehensible three letter variable names and the other was Boost which so large I would take in a day just to figure out where the algorithm actually is. So rejoice, a simple example:
I did make a adjacency graph for the cargo screen, but that wasn’t particularly difficult. It did seem to require a large amount of code because of all the border highlighting and bonus checking I do.
On Texture Compression:
I wish hardware manufactures would just agree on some ISO standard for image compression. Platitized PNG’s are the only universally accepted format on both iOS and Android. Realistically, the best you will get is 16 bits per pixel. Compare this to hardware specific format which will usually get you 4 bits per pixel. Thats a quarter the size in memory, which means you could double the resolution of your texture. On iOS it’s pretty simple, you have PVRTC. On Android it’s a different story; you’ve got devices that use PVRTC(PowerVR), S3TC(Nvidia) or ATITC(ATI). ETC1 format is supported on most Android devices but does not have an alpha channel. ETC2 fixes that problem but it’s only available on the latest devices. You can make a shader that will combine the alpha from a separate texture, but using two textures starts to make the whole proposition not really worth it, plus the rendering overhead of the shader.
Thankfully Google allows you to ship multiple .apk files for the same app. In combination with Manifest filtering you can ship three different versions of your app to support the three different formats. In the end I threw that notion in the too hard basket because:
- I use a lot of sprite sheets and texture packer doesn’t export to .dxt or .ktx. I’m not even sure that cocos2d-x will accept those formats for sprite sheets.
- I don’t own devices with the right graphics hardware to test the different .apk files.
- The size limit for apps downloaded over a cellular connection is only 50Mb on Android (100Mb on iOS but lets target the low end here). If you abide by that limit most devices have more memory than you could flood with your app (unless you do something silly like using only RGBA8). This means, in my case, file size was of more concern than memory size. Using something like pngquant you can crush your png files down to something pretty small so using platform specific compression had less of an appeal. Furthermore png’s don’t have to be power of two either so you can save a fair bit of space that way.
All in all I would just save yourself a lot of headaches and just use PNG’s. I did use ETC1 and PVRTC for Android and iOS respectively for images that did not need an alpha, which are far and few between on a 2D game. In a 3D game I think you would have more of a case for using more exotic compression formats.
The biggest problem with solo development I can see so far is lack of have testers on hand to iron out all the bugs and provide feedback. There are websites out there to facilitate alpha and beta testing but it can be an arduous process when don’t have the person on hand to reproduce a bug. People thousand of miles away and without any vested interest in the project are just as likely to pickup your game and put it straight back down again. I have caught all the bugs I know about but, being human, I’m just as likely as anyone else to fall into predicable use habits as anyone else. Yesterday I got a crash bug on Android that seems to effect 50% of users which I can not reproduce on my device. Yippee.
On The App Store:
Don’t underestimate how much material you will need to make for the app store.. especially Apple App Store. Problem is that Apple wants separate screenshots and video at the native resolution of all it’s devices. That means you’ll need at least three screenshots for iPhone 4, iPhone 5, iPhone 6, iPhone 6 Plus, iPad and iPad2 if you are doing a universal app. This is not a big problem for screenshots because you can just use the simulator to capture, but for video previews you can only use a device. Which is a problem if you only have one device. I originally used IMovie to make my video preview.. which would have been fine except for the fact that iMovie only lets you cut your video at standard HD resolutions. So I remade it using Adobe Premiere. Ugh.
Android lets you cut your screenshots within a certain range but places no requirement on screenshots and video for every device. And thank god because there are so many different Android devices it would take me a year just to make a screenshots to fit every screen resolution.
Note on naming in app purchase id’s: Google only allows lowercase letters, so if you want to use the same id across both platforms keep that in mind.
I messed around with various platform specific audio formats but in the end I just used signed 16-bit pcm WAV files for sound effects and MP3 for music. Using audacity and setting your project rate to 16000 Hz gives you a pretty small sound file with minimal loss of quality. I collapsed most of the sounds into a mono channel which halves the file size again. Music and sound effects were purchased from Audio Jungle.
While on the topic of audio; I will submit that I had to do some minor surgery in CocosDehension. Once you play 32 sounds the next sound to be played will clobber the first sound, even if it is still playing. So you can forget about playing a looping background sound and music at the same time. On IOS you can have one or the other, but not both. I changed CocosDehension so that one channel is reserved for looping sounds, which fixed my use case. I hear that the audio engine in Cocos2D-x has been updated since version 3.2 so hopefully this is no longer an issue.
I haven’t covered a fraction of the things I did on the project and it’s already a lengthy document. I’m made no mention of the week lost to changing from Cocos2D-X version 2 to version 3. Months spent rendering art and animation in modo. Materials made for advertising vendors like Chartboost. Month or more of testing. Modifications to Scrollview. Writing my own scheduler to allow realtime progress of ships while outside the the app. Writing a UI, dialog and text box system. Typing Font Label. Localization system that isn’t in use. AI system that isn’t in use. Making my own editor for authoring the map graph and setting up ship layouts. And many others that I probably can’t recall right now.
On reflection I was fairly naive about the amount of work required to make a multi-platform game. This is doubtless not surprising being my first attempt at programming any full functional application. My foremost error was underestimating how long it takes to write wrappers for both platforms. This was due to my inexperience with Objective C and, to a far lesser extent, Java. Second was how long it took to test and debug. Third, I was spoiled by Apples tools and got a rude surprise when developing for Android. Although, bitching and moaning aside, it took far less time to make the Android version working than it would have to rewrite the whole thing in Java.
I find it hard to believe that any developer, going in cold, would not, at some point, be tripped up by one of the many gotchas in Cocos2D-X. OR find a bug or some deficiency in the engine that they are not desierious of changing. OR be blindsided by some platform difference that results in redoing a fair bit of work. When it happens, and it will, be prepared to spend many hours wrapping your mind around some code that seems bizarre and incomprehensible because it is not written the way “you” would write it. And that takes a lot longer than I think most programmers would like.
Using a commercial engine and toolset like Unity or Unreal Engine probably would have taken far less time and resulted in a far better game. As a solo developer the amount of money require to lease these engines is minor compared the amount of time and effort you will expel to make up for the deficiencies in a free engine. However, as a learning experience in multi-platorm mobile development using C++, it was excellent. And making a game on your own when you have worked with teams of 50 or more people is a empowering experience indeed.