Bundling With AIR Captive Runtime

Overview

Sometimes, the hardest part of making a game is distributing the game after it's been made. If your game is designed to be played as a standalone experience, playing a game in-browser can be a large disappointment. This tutorial is designed to show you how to turn your finished game into a standalone, executable file (either a .exe or .app, depending on your target operating system) that doesn't require installing Adobe AIR — just double-click and play.

This guide is written mainly from a Windows perspective, but the adt tools are the same on OS X.

Ingredients

  • A game that's ready to distribute
  • Adobe AIR (anything after AIR 3.0, but I'll be using AIR 14.0)
  • A self-signed certificate (FlashDevelop can generate this for you)
  • Optional Some basic knowledge of scripting on your OS

Instructions

Setting Everything Up

Before you start powering out some command-line hacker crap, you'll need to be sure you have your AIR binaries added to your system's path.

To make sure the AIR tools are properly added to your path, open a command line window and attempt to call the AIR Developer Tool, adt. You should get something like this:

$ adt
Adobe (R) AIR (R) Developer Tool (ADT)
Version 14.0.0.178
Copyright (c) 2008-2014 Adobe Systems Incorporated. All rights reserved.

No arguments were found
usage:
  adt -checkstore SIGNING_OPTIONS
  adt -certificate -cn <name> ( -ou <org-unit> )? ( -o <org-name> )? ( -c <country> )? ( -validityPeriod <years> )? ( 1024-RSA | 2048-RSA ) <pfx-file> <password>
  adt -help
  adt -migrate SIGNING_OPTIONS ( <air-file-in> | <airn-file-in> ) <output-file>
  adt -package SIGNING_OPTIONS ( -target air )? <output-package> ( <app-desc> FILE_OPTIONS | <input-package> )
  adt -package SIGNING_OPTIONS -target airn <output-package> ( <app-desc> FILE-AND-PATH-OPTIONS | <input-package> )
  adt -package -target ( apk | apk-debug | apk-emulator | apk-captive-runtime ) ( CONNECT_OPTIONS? | LISTEN_OPTIONS? ) ( -airDownloadURL <url> )? ( ARCH_OPTIONS )? SIGNING_OPTIONS <output-package> ( <app-desc> PLATFORM-SDK-OPTION? FILE-AND-PATH-OPTIONS | <input-package> PLATFORM-SDK-OPTION? )
  adt -package -target ( ipa-test | ipa-debug | ipa-app-store | ipa-ad-hoc | ipa-test-interpreter | ipa-debug-interpreter | ipa-test-interpreter-simulator | ipa-debug-interpreter-simulator ) ( CONNECT_OPTIONS? | LISTEN_OPTIONS? ) ( -sampler )? ANE_LINK_OPTIONS? AOT_MODE_OPTIONS? SIGNING_OPTIONS <output-package> ( <app-desc> PLATFORM-SDK-OPTION? FILE-AND-PATH-OPTIONS | <input-package> PLATFORM-SDK-OPTION? )
  adt -package SIGNING_OPTIONS? -target native SIGNING_OPTIONS? <output-package> ( <app-desc> FILE-AND-PATH-OPTIONS | <input-package> )
  adt -package SIGNING_OPTIONS? -migrate SIGNING_OPTIONS -target native SIGNING_OPTIONS? <output-package> <app-desc> FILE_OPTIONS PATH-OPTION
  adt -package SIGNING_OPTIONS? -target bundle SIGNING_OPTIONS? <output-package> ( <app-desc> FILE-AND-PATH-OPTIONS | <input-package> )
  adt -package SIGNING_OPTIONS? -target ane <output-package> <ext-desc> ANE_OPTIONS
  adt -prepare <airi-file> <app-desc> FILE_AND_PATH_OPTIONS
  adt -sign SIGNING_OPTIONS ( -target ( air | airn | ane ) )? ( <airi-file> | <unsigned-ane-file> ) <output-file>
  adt -devices          PLATFORM-OPTION PLATFORM-SDK-OPTION?
  adt -installRuntime   PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION? ( -package <apk-file> )?
  adt -installApp       PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION? -package <apk-file | ipa-file>
  adt -uninstallRuntime PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION?
  adt -uninstallApp     PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION? -appid <app-id>
  adt -launchApp        { PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION? ( -debuggerPort port )? -appid <app-id> }
  adt -runtimeVersion   PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION?
  adt -appVersion       PLATFORM-OPTION PLATFORM-SDK-OPTION? DEVICE-OPTION? -appid <app-id>
  adt -version

SIGNING_OPTIONS      : -storetype <type> ( -keystore <store> )? ( -storepass <pass> )? ( -alias <aliasName> )? ( -keypass <pass> )? ( -providerName <name> )? ( -tsa <url> )? ( -provisioning-profile <profile> )?
FILE_OPTIONS         : <fileOrDir>* ( ( -C <dir> <fileOrDir>+ ) | ( -e <file> <path> ) )*
ARCH_OPTIONS              : -arch (armv7 | x86)
CONNECT_OPTIONS      : -connect <host>
LISTEN_OPTIONS       : -listen <port>
ANE_LINK_OPTIONS     : -hideAneLibSymbols ( yes | no )
ANE_OPTIONS          : -swc <swc> ( -platform <name> (-platformoptions <file>)? <fileOrDir>* ( -C <dir> <fileOrDir>+ )* )*
FILE-AND-PATH-OPTIONS: ( PATH-OPTION | FILE-OPTIONS ) FILE-AND-PATH-OPTIONS?
PATH-OPTION          : -extdir <dir>
PLATFORM-OPTION      : -platform (android | ios)
PLATFORM-SDK-OPTION  : -platformsdk <platform-sdk-home-dir>
DEVICE-OPTION        : -device ( deviceID | ios-simulator )
AOT_MODE_OPTIONS     : -useLegacyAOT ( yes | no )

Those are the tools you'll be working with, so get ready. It's time to cook.

Basic Bundling

To create a bundle with the AIR Captive Runtime (remember, that's the bit that allows it to run without requiring AIR to be installed), you'll be using the bundle target. That means, you'll want to use the item from the list that adt graciously provided you with the -target bundle argument. In case you can't find it, here it is:

adt -package SIGNING_OPTIONS? -target bundle SIGNING_OPTIONS? <output-package> ( <app-desc> FILE-AND-PATH-OPTIONS | <input-package> )

Got that? Let's break it down.

  • SIGNING_OPTIONS This is where you slap that self-signed certificate in.
  • output-package This is the name of the file to output, like dist\windows.
  • app-desc The AIR descriptor XML (should be generated by FlashDevelop)
  • FILE-AND-PATH-OPTIONS This one can be tricky, but it's a list of all the assets required by your application XML, including mygame.swf.

Bundling In Action

Now that you know the tools, let's look at a sample game, Super Sample. You would probably have a bundle call that looks very similar to this:

$ adt -package -tsa none -storetype pkcs12 -keystore "cert\supersample.p12" -storepass sspassword123 -target bundle dist\windows application.xml -e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" . -extdir lib

Let's dissect this call.


$ adt -package

Call the AIR Developer Tool, instructing it to package an application for distribution.


-tsa none -storetype pkcs12 -keystore "cert\supersample.p12" -storepass sspassword123

Here are the signing options.

-tsa none -storetype pkcs12 -keystore "cert\supersample.p12" -storepass sspassword123

This section instructs the tool to not check the certificate timestamp. This can be useful if your computer isn't connected to the internet or if your certificate file is out of date (shame on you).

-tsa none -storetype pkcs12 -keystore "cert\supersample.p12" -storepass sspassword123

This section tells the tool what certificate to use for signing. The conversation might go something like this:

The certificate is a PKCS 12 certificate. The certificate is found at cert\supersample.p12. The password for the certificate is "sspassword123."


-target bundle dist\windows application.xml

This tells the tool to create an executable bundle with the AIR Captive Runtime in the dist\windows directory based on the information in application.xml. The XML file will describe the name of the application as well as any other files needed by the application. Since the application's name is stored in application.xml, it will use this to name the resulting executable (in this case, it'll probably be supersample.exe).


-e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" . -extdir lib

Here are the file and path options. As I mentioned before, these can be tricky, but I'm sure you'll get the hang of them. The main thing you need to know are the two flags, -e and -C. -e describes an explicit file, while -C describes a directory change. Here we go.

-e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" . -extdir lib

The bundle should include the file located at bin/supersample.swf, and this file should be in the root directory and named supersample.swf.

If for some reason you needed to change the location or name of the file, you can do that as well. Supposing you goofed real bad and built your file to output-files/goofed.swf and set your application.xml to look for swfs/main.swf, you could change this to:

-e output-files/goofed.swf swfs/main.swf
-e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" . -extdir lib

This describes an entire folder (in this case, this folder contains the icons to use for the executable file).

Switch to the directory icons/desktop/icons. Now, put all the icons there into the root of the bundle.

Again, you can change the output, ., to point to the proper location in your bundle.

-e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" . -extdir lib

This specifies any external libraries you may need in your application. This is often used when working with Native Extensions, (such as FRESteamWorks, which provides your AIR application access to the steamworks libraries).

Look for Native Extensions in the lib directory and bundle those on up as well.

Advanced Bundling Maneuvers

Once you've gotten the hang of bundling your AIR applications, make life easier on yourself with simple tools.

Power Bundling

It can be a pain to keep a command window open, and typing all your stuff in sucks. Make life easier by writing a little batch script like this:

bundle.bat

@echo off

set SIGNING_OPTIONS=-storetype pkcs12 -keystore "cert\supersample.p12" -storepass sspassword123
set APP_XML=application.xml
set DIST_PATH=dist
set DIST_NAME=windows
set FILE_OR_DIR=-e "bin/supersample.swf" supersample.swf -C "icons/desktop/icons" .

set OUTPUT=%DIST_PATH%\%DIST_NAME%

if not exist "%DIST_PATH%" md "%DIST_PATH%"

set AIR_PACKAGE=adt -package -tsa none %SIGNING_OPTIONS% -target bundle %OUTPUT% %APP_XML% %FILE_OR_DIR% -extdir lib

call where adt
call adt -version
echo $ %AIR_PACKAGE%
call %AIR_PACKAGE%

This little bad-boy will not only bundle Super Sample by just double-clicking on it, but will also check the location and version of adt, create your output folder (if it doesn't exist) and show you the exact command it is calling. Additionally, editing it is easy since all the variables are broken out.

Automatic Build & Bundle

Once you've created a bundling script like bundle.bat, you can instruct FlashDevelop to bundle that puppy upon successful build of your game — now that's a next-level bundling maneuver!

In FlashDevelop, open your project's properties and cruise over to the Build tab. Add your bundle batch file to the "Post-Build Command Line" area, making sure that "Always execute" is off (this prevents bundle.bat from being run if the build fails).

Now, whenever you build, FlashDevelop will also bundle for you!

If you want to get really fancy and always test your bundled application, hit up that Output tab, select "Run Custom Command..." under "Test Project" and click "Edit...." From there, enter the location of your bundle and save that beast. Now, when you run your application from FlashDevelop, it'll run your standalone executable. Wow!

Originally posted on the FlashPunk Developers forum.

Awaken the Force in you

After reading the excelent Saw It For You: Star Wars: The Force Awakens Teaser Trailer (2014) by Kris Straub, I was faced with severe disappointment when the web address in one of his throwaway gags didn't resolve. After a few minutes of frowning at my browser, I decided to do something about it.

The something that I did do was buy the domain and build the site so everyone can experience the joy of discovering the saberbody color unique to them. If, for some reason, you don't know your saberbody color, you don't have an excuse anymore.

http://whatsmysaberbodycolor.com

http://whatsmysaberbodycolor.com was built on GitHub pages using Bootstrap in an afternoon. The longest part of the process was writing the descriptions of the different saberbodies. The shortest part was deciding to do it.

New Tools Can't Fix Old Habits

I'm currently facing a problem. Every day I sit down to the same system — piles of shaky dialogs and buttons stacked like cordwood trying to hold back event stacks deeper than the Mariana Trench which were obviously written while learning the platform. Oh the joys of UI development in the world of the "new hotness."

Faced with looming deadlines and hordes of anxious customers, proclamations are uttered from on high, "We don't have time to (re)design! Just make it work." So we do, don't we?

We build interfaces out of the recycled design patterns of yesteryear while shaking our heads. "This sucks, but it meets the requirements. I just hope they don't need more functionality."

But, they always do.

So, we dive back into the sludge we've poured for ourselves, barely able to see our hands in front of our faces, thrash around a bit, and pray that a solution might come and fix all of this garbage.

False Prophets

New technology emerges, as it is wont to do, that claims to solve all the problems we currently have while tearing down all the blockers we currently face. Of course we adopt — it would be foolish not to.

Again, commandments boom from the mount. "With this new technology (which we've graciously purchased), it should be trivial to replicate existing functionality. Also, the customer needs more functionality than what we currently have to justify the cost of the new technology (which they've graciously purchased). Make it work." So we do, don't we?

Sooner or later, but always sooner than we'd imagined, we're back in the muck, blaming lame sprints on the poor foundation we've created because we were only given time to "make it work" for the customer.

Confusion abounds. "How are we facing the same problems we were before? We bought new technology!" The kicker is, the technology is rarely the problem.

Breaking Bad

In rapidly evolving ecosystems, this cycle can be very difficult to mitigate, primarily because it requires designers and developers to stop writing code and start reading code. Of course, if everyone sits around not writing code all day, how will we ship products? The painful truth is, we won't.

Taking time to study new technology will slow things down. Taking time to design based on what you've learned from study will slow things down. The end result, surprisingly enough, is a more stable, more maintainable product whose development time is shorter than a product made to just work.

"But," you ask, "if I'm going to have to take all this time to actually learn about this new technology before I can harness its incredible potential, how is it better than the old technology that I've been learning?" That's the kicker — it isn't necessarily better at all.

Without fully understanding what your technology can do and creating a design based around your technology's strengths, it is impossible to develop a product without little issues that, given time, create huge problems. So the next time a new technology promises to fix all the problems you have, it might be a good idea to sit down and read the instructions before putting it to use.

PROJECT: CODENAME

AJ-43K7-99X

ATTN: [REDACTED]

PROJECT: CODENAME

TO PREVENT ANY POSSIBLE BREACH OF SECURITY OCCURING IN [REDACTED],
ANY DISCUSSION OF [REDACTED] OUTSIDE OF DESIGNATED SAFE ROOMS IS
STRICTLY FORBIDDEN. IN THE EVENT OF EMERGENCY, PROJECT: CODENAME HAS
BEEN CREATED TO ALLOW EXPEDITED CREATION OF SECURE, APPROVED CODENAMES
FOR ANY CLASSIFIED PROJECT OR OPERATION TAKING PLACE IN [REDACTED].

[REDACTED]
[REDACTED], UNITED STATES ARMY

http://projectcodename.com

An Open Reply To Young Game Developers

Young Game Developers,

I love hearing from you, because it gives me a chance that I never had. I knew that I wanted to make video games when I was six years old. Most kids put on all sorts of "future costumes," (fireman one day, policeman the next) but I never really thought about what I wanted to do when I grew up. On the Christmas I received an NES, my father set it up, I played it for a bit, and it struck me like a bolt: "I want to make this when I grow up."

This is why you're incredibly lucky. Back then, "when I grow up" was the only option. The internet was in its infancy, and computer classes were a total joke. As a young adult, I had virtually no way of learning anything about software development. Jump twenty years into the future and here you are, neck-deep in high school (or whatever you call it down there) and already making video games.

Let that sink in. That shit is awesome.

Make sure to learn what you can from school, but also realize that you'll learn way more on your own that you ever will listening to a teacher ramble. Dive into things. If you don't know something, try to figure it out yourself first. Most importantly, learn how to learn. Knowing how to figure out what you need to know is critical, and takes serious practice.

Additionally, if you're looking to start your own business, I'd make sure to take business classes alongside your programming classes. They'll come in handy.

As to "how I remember everything…." Man, that statement makes me grin. The only thing you really need to concern yourself with learning right now is how to think logically. Focus on your program flow and figuring out what needs to happen when and why things happen how. This skill translates across all programming languages — even ones you haven't learned yet! If you can think critically, figuring out the proper functions to call is a cakewalk. Just do a quick Google search on what you're trying to do and about a billion answers will pop up.

If you work with the same set of things often enough, you retain more and more information about them. Think about it like this: You probably know the directions to hundreds of places around where you live, because you've lived there for awhile and are familiar with the area. If I asked you how to get to a couple different places, you could probably tell me how to get there the majority of the time. I'm absolutely amazed.

"Young Game Developers! How do you remember directions so well? I have no idea where I'm going around here half the time!" How would you respond? I'll leave that answer as an exercise for the student.

Best regards,

Zachary Lewis