In this post, I (Sean Hogan/seagaia) discuss how I ended up porting my in-development game, Anodyne, from its Desktop Adobe AIR version into an Android app by using FlashDevelop, and then how I implemented touch input using Adobe AIR’s APIs.
Two GIGANTIC MEGA-HUGE caveats
1. Anodyne has only a few controls that are at least a little natural for mobile interfaces – essentially the Game Boy with no select button. Without a doubt, that made this port very easy, the mappings for the controls were obvious.
2. Anodyne is also letterboxed (meaning there are empty margins to the left and right when I center the game on a landscape screen) because it has a 160×180 resolution (scaled accordingly). That made it easy to pick where to put the UI elements for controls.
YOUR MILEAGE WILL DEFINITELY VARY depending on what your game is, the resolution, etc. I’m not claiming you’re going to get some awesome 60 FPS app running Flixel which doesn’t use the GPU. In any case, hopefully this will be helpful in getting started in the right direction. I fucked up in quite a few ways doing this (more on that at the end 🙂 )
So, you’re making a game in FlashDevelop, and have made it work as an Adobe AIR app and it works great on the desktop. Awesome! (If you don’t, stop reading here and go see the earlier tutorial on how to do that).
The discussion has a few parts:
1. Stuff you need to even do this
2. An explanation of the build scripts
3. How to get game onto phone
4. How I managed touch input.
5. How I fucked up before figuring this out
1. STUFF YOU NEED
- Buy an Android phone/tablet. (Sorry. Emulators tend to be a miserable experience)
- Windows (well…)
- Start a new “mobile app” project in FlashDevelop.
- Make sure you have at least the AIR SDK 2.6+ (By default, FlashDevelop should install with this)
2. BUILD SCRIPTS
There are a number of setup files packaged with FlashDevelop. If you started a fresh project, all of the IDs and paths should be okay. What I did was copy these scripts to my current project and rename them and edit their paths/etc. to make everything work. If you go that route, things will break at least 100 times, but in small, easily manageable ways (provided you don’t panic!). Namely, a lot of scripts call other scripts, so if you change a script’s name, you will need to change all references to that script in the other scripts.
Here are the scripts:
- Run.bat – Depending on what goto is uncommented (lines 7-11), this will run the game in a certain way. You may as well do Desktop for the fastest compile/test – tantamount to the normal build process -, or else you’ll have to transfer to your phone every time using some awful sockets library and sometimes you get mysterious errors and it takes 10 minutes to transfer a 45 MB file to your phone…but I digress
- PackageApp.bat – This takes a SWF that FlashDevelop puts out, and then puts magic sauce on it, turning it into a .apk that your phone will run. If you want to test, pick  normal (apk) , or else on your phone the AIR Debugger will try to start, and then everything goes straight to hell.
- bat/InstallAirRuntime.bat – If your phone is connected, then this script will detect it and install AIR on it. Or you could just download AIR from the Google Play Store.
- bat/CreateCertificate.bat – This will create a certificate, which is needed to package your SWF as a .apk .
- bat/SetupApplication.bat – This just sets up a lot of environment variables – name of your certificate, path to your game’s icons, application descriptor name, etc. Most of this is filled in by default.
- bat/SetupSDK.bat – Sets up the paths to your FLex SDK and Android SDK. Most of this is filled in by default, though if you use different SDKs you will need to edit the paths (not that scary, I swear!)
- bat/Packager.bat – Called last in the packaging process, does some magic and turns your game into a .apk , provided you did everything else right…
They’re actually relatively straightforward if you start from one of them and trace through what it does.
Anyways, once that’s all in place…you can try putting the game on your phone.
3. Getting that game on the phone
- 0. Build your game in RELEASE mode.
- 1. Run PackageApp.bat . If all the scripts are set up fine, you should get <name of app>.apk in the dist folder.
- 2. Open up your phone. Download ES File Explorer and Adobe AIR Runtime.
- 3. Connect your phone to your computer, and drag the .apk file to your SD card or whatever
- 4. Open up ES File Explorer on your phone, and click on the .apk file to install it, open it, etc. Game should run!
Note that your users will still need to download AIR on their own. There is a way to package AIR with your app, but you must use AIR 3.0+ and pick  captive in the PackageApp.bat build script. Note that Google Play Store apps have a size limit of 50 MB (stupid, right?) so keep that in mind. Additionally, Android will tell the user they need to download AIR if they don’t have it, so…
Also I don’t know shit (yet) about getting it into the store but I assume that’s relatively straightforward.
4. How I ported the game after dealing with all that bullshit
Things you will have to deal with in porting
- Resolution issues. Game might not fit. Etc.
- Space and Time issues
- How to handle your UI, for different screen sizes, keep the game feeling okay.
- Mapping touch input into whatever control handler you’ve programmed.
- Phone events
Well, the market is full of phones of different sizes. Luckily you can scale the game relatively easily – check the stage object of your game, stage.width and stage.height will give you the full-screen sizes of the phone. Then from there just scale your main display object accordingly…you will have to choose how this works. Perhaps you show more of the game (if appropriate) or you just scale the game in cases where that would be totally inappropriate (as in Anodyne).
What I did: Anodyne fits nicely at 3x on most phones, though I’m planning to have options to center the game at an integer scaling (to avoid ugly pixels stretching). I will offer the option on tablets to integer scale to the max at first (this is what I do with the PC version – by default it stretches to the max integer scaling). The letterboxing margins are used for GUI elements.
Space and time issues
The phone is not your PC. If you’re lucky you might have 1 GB of RAM, and a few cores…but in general, if you’re using a CPU-based library like vanilla Flixel you probably won’t have performance as great as on your computer.
AIR/Android also seems to do something weird with space. As a reference point, Anodyne takes up ~40-50 MB of RAM running on PC (most of that space allocated for music), but 90-100 MB on my phone. Just little things to keep in mind – hacks you might get away with on the PC might not help on mobile devices. There are plenty of AS3 libraries that take advantage of the GPU, but I have no knowledge of how to use them.
What I did: Anodyne is luckily not very expensive – not too many sprites on the screen at once, logically it’s in a 160×180 px grid…so this wasn’t a problem for me.
Different icon sizes
Be aware of different icon sizes for phones and tablets! There’s a handy screen size chart here: http://help.adobe.com/en_US/air/build/WSfffb011ac560372f-6fa6d7e0128cca93d31-8000.html and of course Google will let you find anything you want.
What I did: For Anodyne, I picked the average screen size and made the icons for the controls GUI fit something a little smaller. For larger (tablet) screen sizes I might make bigger icons – you can just dynamically choose what you want based on the screen size of the phone.
Mapping touch input…
Okay,well, to be honest, the difficulty here will lie entirely in how you manage your input. Anodyne gives an object to handle input, and then that object changes state based on the inputs – it has state variables for a button being down, just pressed, just released. Since I use flixel, I normally have the FlxG.keys… API calls to check the state of various things, and then I set the appropriate property on my input handler. This lets me use the input handler state throughout the game without worrying about what actually happens in the logic.
Thus, it was straightforward to map touch input to movement – just define what touch events should map to what. Anodyne has the following, done with only the following events. I added a listener to the stage object for these three events.
TouchEvent.TOUCH_MOVE – Fired once per tick when a touch is on the screen. Useful for managing movement.
TouchEvent.TOUCH_END – Fired once, whenever a touch input is “lifted off” of the screen. Useful for stopping movement.
TouchEvent.TOUCH_BEGIN – Fired once whenever a touch input begins. Useful for attacking, pausing, moving around menus.
Note, that for the most part, Mouse move, mouse down, mouse up events – work the same, which makes them useful for debugging without sending the app to your phone every time. Keep that in mind!
- In short, I define “directional hitboxes” for an “up”, “down”, “left”, “right” region. Some of the hitboxes overlap (so Up, left, etc) because we want diagonal movement. I position these on the screen, and then when a touch_move event comes in, I check the event’s “stageX” and “stageY” properties. If the event’s coordinates are in a directional hitbox, I then tell my input manager to have the “is this key pressed” state set to true for that direction.
- If the touch is moved to a a deadzone, then all directional input stops.
- There is an edge case, when a player lifts their finger off of a hitbox, for this, you need to add a listener for a TOUCH_END event, if there is a TOUCH_END event inside of one of the directional hitboxes, then stop all input.
In this early screenshot of the gui, you can see the directional hitboxes on the left, action hitboxes on the right. Notably, some of the directional hitboxes overlap, so I can have diagonal movement. I later moved the directional hitboxes closer together to have less of a “dead zone” where no input is registered.
Menu movement: Just add a TOUCH_BEGIN listener. If the position of this event is in one of your hitboxes, then set the “just pressed” property of that key to true in your input manager.
MOVEMENT AND ACTIONS WORKING TOGETHER
Because I need movement to stop when there is a touch-move event not in a directional hitbox, we need the move events on the right side of the screen to not affect the left side. Just keep a state variable for what side of the screen the directional hitboxes are on, and then in the event handler for the TOUCH_MOVE event, ignore anything on the wrong side based on your state variable.
People will want to move with their Left or right hands, or flip controls…I just keep some state variables and when needed flip the positions of everything. There are a million ways to do this so I won’t bother explaining.
Handling phone events
When you get a phone call or accidentally hit back/home, you don’t want to crash the game or keep the volume on.
Just listen for Event.DEACTIVATE and Event.ACTIVATE events – happens when game enters or exits due to whatever reasons. Add them to the stage object. For me, in DEACTIVATE , I just call SoundMixer.stopAll() to stop sounds, and then try to pause the game. Exiting your game should be through some other method through your menu, where you can just call NativeApplication.nativeApplication.exit();
Note that you will want to add actual icons and hide the hitboxes. That’s what I did, at least – so I can have gigantic hitboxes for the input but smaller icons to look nicer, and they can exist independently of eachother.
5. HOW I FUCKED UP
1. You don’t need to install some Android SDK or fake-phone manager if you have a phone already. Nor do you need to use the adb thing to transfer files…just drag and drop with a USB cable. Much faster. I did so much of this and it was a huge waste of time.
2. For the love of god, don’t add separate event handlers for every single hitbox. I had 16 at one point, one for each cardinal direction, because of all this stuff going wrong with touches lifting and…just don’t. What I have above is much simpler.
Those mistakes cost a lot of hours but NOW I KNOW and SO DO YOU.
…And that’s it!
Or you know, follow me on Twitter,