Kongregate Developers

Dealing with the 64K Method Limit in Your Unity3D Game

dex limit.md

Could you ever imagine a Unity Android game that used more than 64K Java methods? Neither could the architects of the Dalvik bytecode. Perhaps they did (I haven’t read the spec) and the blame falls on other elements in your toolchain. The long and short of it is if your game taps against the 64K method limit per DEX file you’re going to need to get down and dirty with your native plugins and/or build workflow. This post will attempt to walk you through the various ways to deal with it.

First Things First

There’s no shortage of forum posts and blog entries on this subject. The most important takeaway is if you can manage to keep your game comfortably below this number, you will save yourself a lot of trouble.

Know Your Plugins

The most common way to hit this limit in Unity is through the use of native plugins. Android native plugins are a necessity for almost all Unity games. Unfortunately, some plugins are quite large. Google Play Game Services, for example, is close to 25K methods on its own, a significant chunk of the 64K you are allotted.

Super Brief Anatomy of Unity Android Plugins

Unity Android Plugins typically consist of some C# Unity code along with native Android code and resources. The native code and resources will be packaged either as an Android Library Project or Android Archive (AAR) under the Assets/Plugins/Android/ directory. Library Projects are the old way of sharing components in the Android ecosystem and AARs are the newer way. You will find plugins that use both.

The classes in both Library projects and AARs exist in JAR files, which are simple zips of compiled Java class files. The AAR file is also simply a zip of various Android resources, some of which will be libs/*.jar (a.k.a. Java class archives). Library projects are simple directory structures, and again the JARs will be in libs/*.jar.

Steps to Minimize Method Counts

The only way to reduce the number of Java methods included in your game's APK using the standard Unity build system is to remove or modify the JAR files included with your Android native plugins. The alternative is to export your Unity Project as an Android Project where you can apply more advanced techniques.

You should try each of the following techniques in order:

  • Remove any plugins your game is not using
  • Since Google has broken Play Services into a set of modules, only include the ones you actually use.
  • Use the Jar Jar Links tool with the zap rule to remove unneeded classes from the plug-ins JARs. You could also simply unzip, delete unused classes, and rezip the JAR.
  • Export your project as an Android Project so you can apply ProGuard or MultiDex. This is where things get dicey.

Most of this blog post will focus on the last bit because, at the time of writing, there aren’t a lot of resources that walk you through this approach. Exporting as an Android Project will be more disruptive to your development and build cycle. Until ProGuard or MultiDex are directly supported by Unity, you're best off going down this path as a last resort.

What to Look for When Testing

Once you have your game under the 64K limit and are able to generate the APK again, the key thing to look for while testing your game is ClassNotFoundException and VerifyError errors in logcat. This indicates your code is trying to use some class or method which is unavailable. Usually the error will be associated with a crash, so it’ll be quite obvious. Sometimes, however, the plugin may attempt to fail gracefully and, though your app does not crash, some feature you hope to be available will not function as expected.

ProGuard and MultiDex

ProGuard is a tool used to obfuscate and trim unused classes and methods. MultiDex is technology that enables multiple DEX files within your APK, thus removing the 64K method limit for your game. Unity does not have direct support for either of these techniques, but you can make use of them if you export your project to an Android Project.

When all else fails, ProGuard will hopefully bring you below the limit. If not, you can turn to MultiDex. MultiDex has the added strike of only working in API Level 14 (4.0) and up. It is natively supported in Android (5.0) and up. Support libraries must be used for 4.X. Finally, MultiDex comes with a set of Known Limitations.

Exporting to an Android Project

If you need to use ProGuard or MultiDex, the first step is to export your Unity Project as an Android Project. Depending on the complexity of your Project, this in itself can be a daunting task. It also likely means no more Unity Cloud Build. When done correctly, however, it can work similarly to exporting to XCode for iOS. The Android Studio or Gradle project will need to be set up after the export, but this should be a one-time task. You will be able to re-export your project without having to set up the Android build configuration again.

I’ve found three ways to successfully work with a project exported to Android. I’ll briefly cover the first two because they are simpler and may be preferable if your project is not too complex. The last approach requires a little more manual setup, but is probably the cleanest way to organize your project. It also may be your only option if you need MultiDex.

A Few Words of Caution

Even after exporting your game to Android Studio, it’s possible that the plugins your game uses depend on Unity Post Process scripts that will not translate to Android Studio or Gradle builds. You may still hit a dead end.

Approach 1: Simple Unity Export/Android Studio Import

This approach will work for games that do not use too many plugins. I imagine Unity and Android Studio will continue to improve this approach.

  1. Under File -> Build Settings -> Android click the Google Android Project checkbox and the Export button. Create/Choose a directory to export. Android would be a good choice.
  2. Open Android Studio and choose Import project (Eclipse ADT, Gradle, etc.). Navigate to your exported Unity Project, which will be a subdirectory of your export directory (e.g. ./Android/Your Unity Project).
  3. You will need to choose a destination directory. You may leave the various checkboxes checked.

At this point, if all goes well, you should be able to run the project within Android Studio.

Pros & Cons

  • Pro: It’s simple.
  • Pro: The imported Android Studio project is also a standard Gradle project, allowing easier integration of Gradle-based tasks.
  • Con: Every time you export from Unity and import into Android Studio, a brand new project is created. Any manipulation you need to make to the studio project – for example, configure ProGuard – will need to be done every time you build. This would pretty severely impact your development cycle.
  • Con: Depending on the complexity of your project, it simply may not work without significant modification to the Android Studio Project.

Approach 2: Import the Exported Unity Project from Source

In this approach you directly import the exported Unity Project into Android Studio from sources and then manually update the various modules and dependencies. The difference from the first approach is rather than importing /Android/Your Unity Project, you import /Android, and Android Studio will attempt to set up modules for your main application and each exported library project.

The nice thing about this approach is that once you have the Android Studio project set up, you can re-export the Unity Project to the same location and in general will not need to update Android Studio Project.

The downside to this approach is that your Android Project will be tied to Android Studio Project files. Configuring and managing the dependencies will be a challenge.

Since I’d like to focus on the third approach, I’ll simply say once you have your project in Android Studio, it isn’t too difficult to enable ProGuard. However, the process of setting up the Android Studio Project involves correctly configuring each of the modules and dependencies using Android Studio’s UI. Depending on your familiarity with Android Studio project modules, this could be a tricky task. Further, I found getting MultiDex configured through the Android Studio UI challenging, which led me to the third approach.

Approach 3: Configuring a Gradle Project for an Exported Unity Project

Gradle is the build tool Android settled into a few years back. Android Studio Projects may be synced with Gradle Projects. Though old Android Studio Project modules are still supported, new projects are based on Gradle files. In this approach we correctly set up the Gradle files for the exported Unity Project, at which point we can either work with them and build from Android Studio or from the command line. We are given access to useful Gradle tasks such as ProGuard and MultiDex.

Set Up the Gradle Wrapper

In the directory where you exported your game, set up the Gradle wrapper with the following command:

gradle wrapper --gradle-version 2.14.1

Gradle comes with Android Studio, so you should have some version of it installed. The above command will create a gradlew script that will lock your build script to a specific version of Gradle. At the moment 2.14.1 is a good choice.

Create Root build.gradle file

In the same directory, create your top level Gradle file build.gradle. You may simply copy and paste the following:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Create Your Application build.gradle File

Place the following file in the main project sub-directory created for your Unity Project under your export directory (e.g. Android/Your Unity Project). This file also must be named build.gradle.

apply plugin: 'com.android.application'

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }

        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }
}

Create Your settings.gradle File

Back in the root directory of your exported Android project, create a settings.gradle file with the following contents. Of course, replace :Your Unity Project with whatever the directory Unity created for your exported project.

include ':Your Unity Project'

At this point, if you had a super-simple Unity Project with no plugins, you should be good to go. Within Android Studio you may choose Open an existing Android Studio project. Navigate to and open the settings.gradle file you created and work with your project within Android Studio. You may also build your project from the command line like so:

./gradlew assembleDebug

You can see the complete list of Gradle build tasks with:

./gradlew tasks

My Project Wasn’t That Simple :(

Chances are if you are reading this, it’s because your project wasn’t that simple. When you export from Unity, in addition to a main app directory (e.g. Android/Your Unity Project), it creates a directory for each library project and AAR used by your native plugins. For the AARs, they were extracted into Library project format.

Add the following file for each library project subdirectory created by the Unity export. Again, name this file(s) build.gradle

apply plugin: 'com.android.library'

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"
    publishNonDefault true

    defaultConfig {
        minSdkVersion 9
        targetSdkVersion 24
    }

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }
}

Next, back in your settings.gradle file, add include rules for each subdirectory.

include ':appcompat'
include ':google-play-services_lib'

Finally, back in the build.gradle file for your main application (e.g. Android/Your Unity Project/build.gradle), update the dependencies section to include the library projects.

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':google-play-services_lib')
}

Resolving Dependencies

In some cases, you may have one library project that depends on another library project. For example, this output is shown because the MainLibProj module depends on Google Play Game Services.

.../MainLibProj/build/intermediates/manifests/aapt/release/AndroidManifest.xml:31:28-65: AAPT: No resource found that matches the given name (at 'value' with value '@integer/google_play_services_version').

There is no hard and fast rule to interpret these dependencies, but in general, the name of the missing resource gives you a pretty good clue. For this case, google_play_services_version pretty clearly points to the Google Play Game Services. We can use grep to figure out which of the Google Play game services modules contain this value.

grep -r  google_play_services_version .
./MainLibProj/AndroidManifest.xml:            android:value="@integer/google_play_services_version" />
...
./play-services-basement-9.4.0/res/values/values.xml:    <integer name="google_play_services_version">9452000</integer>

We see the resource is defined in play-services-basement and referenced by MainLibProj. Open up <export_dir>/MainLibProj/build.gradle and update the dependencies entry like so:

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':play-services-basement-9.4.0')
}

Now Gradle knows the MainLibProj module depends on play-services-basement-9.4.0.

Resolving Duplicate Class Conflicts

When Unity exports the plugins as Library projects, it’s not unusual to see errors along these lines:

Dex: Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/unity/purchasing/googleplay/BuildConfig;

The BuildConfig class is generated by Android build tools. They are often included when a plugin is constructed as an AAR and then a second one is created by your build process when the AAR is converted to a library project and re-compiled. You can fix this by deleting the class from the expanded library project.

zip -d GooglePlay/libs/classes.jar "com/unity/purchasing/googleplay/BuildConfig.class"
deleting: com/unity/purchasing/googleplay/BuildConfig.class

Since you’ll need to do this every time you export, you probably want to write script to clean up all the JARs after export.

An alternative solution is to use the AAR, if one exists for the plugin, rather than the extracted Library Project Unity creates for the AAR when exporting. For this example, we find GooglePlay.aar, which is included with the UnityPurchasing plugin, and copy it over to a new aars directory we create in our exported project tree.

cp /Assets/Plugins/UnityPurchasing/Bin/Android/GooglePlay.aar <exported_proj>/aars/

Next we add a line to our root build.gradle file to add the new aars directory to the repository search path.

allprojects {
    repositories {
        jcenter()
        flatDir { dirs '../aars' }
    }
}

Finally, add the dependency to Your Unity Project/build.gradle. Note that we use a slightly different format to reference the aar instead of the library project.

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile ':GooglePlay@aar'
}

Other Issues

There is a host of other issues you may or may not encounter converting your exported Unity Project to Gradle/Android Studio. In general, the two classes of issues include (1) conflicts between the AndroidManifest.xml included by plugins and (2) the behavior the post-process scripts native plugins depend on may not translate properly to the exported project.

The former will typically occur in regular Unity builds as well, during the manifest merging task. Resolving them requires tweaking the manifest entries. Usually, the errors tell you what the conflict is and provide some clue about how to resolve them. If possible, it’s better to resolve these in the main Unity Project, so you don’t need to re-perform the steps every time you export.

The second issue regarding post process scripts is a lot more tricky, and may end up being a blocker for effectively working with the exported project. There are no general guidelines here.

Resolving 64K DEX Method Limit in Your Gradle Project

Now that we have our Unity Project in Gradle, we can use ProGuard to attempt to bring our method count below 64K, or we can enable MultiDex to support greater than 64K.

Enabling ProGuard

A whole separate blog post could be written on how to configure ProGuard for exported Unity Projects. Here we’ll show how to add ProGuard to your Gradle build script. Add the following to the android section of Your Unity Project/build.gradle to enable ProGuard for release builds.

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
  }
}

We specified two ProGuard configuration files – the standard one that’s included with the Android SDK (proguard-android.txt) and one which is exported with the Unity Project as of Unity 5.4, (proguard-unity.txt). You almost certainly need to maintain another ProGuard configuration file with rules specifying which classes and methods need to be kept for the plugins your game uses.

To disable ProGuard, simply change the value of minifyEnabled to false.

Enabling MultiDex

To enable MultiDex for your exported build, add the following lines to the android section of Your Unity Project/build.gradle.

defaultConfig {
    minSdkVersion 15
    targetSdkVersion 24

    // Enabling multidex support.
    multiDexEnabled true
}

This will enable MultiDex support on Android 5.0 and up devices. To support Android 4.0 and up devices, you must make a few additional modifications. First, add a new dependency for the support library com.android.support:multidex to New Unity Project\build.gradle.

dependencies {
    compile 'com.android.support:multidex:1+'
    compile fileTree(dir: 'libs', include: '*.jar')
    // other dependencies
}

Next, update the <application> tag in your main AndroidManifest.xml to specify the MultiDexApplication support class.

<application android:name="android.support.multidex.MultiDexApplication"
... >

If your Unity Project does not already have a main AndroidManifest.xml file, you probably want to add one to /Assets/Plugins/Android/AndroidManifest.xml and update the application tag there, so it’s included with future exports.

Complete Application build.gradle File

Here’s what the complete build.gradle file looks like for our simple application with a single dependency. A complex project bumping against the 64K method limit will likely have quite a bit more dependencies.

apply plugin: 'com.android.application'

dependencies {
    compile 'com.android.support:multidex:1+'
    compile fileTree(dir: 'libs', include: '*.jar')
    compile ':GooglePlay@aar'
}

android {
    compileSdkVersion 24
    buildToolsVersion "24"

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')

      signingConfigs {
        myConfig {
          storeFile file("<path-to-key>/private_keystore.keystore")
          storePassword System.getenv("KEY_PASSWORD")
          keyAlias "<your_key_alias>"
          keyPassword storePassword
        }
      }
    }

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 24

         // Enabling multidex support.
         multiDexEnabled true
    }

    buildTypes {
      release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
        signingConfig signingConfigs.myConfig
      }
    }

}

This snippet also adds entries required to sign your app with a private key. The key password is drawn from an environment variable. If all is good, you can build your minified/multidexed game like so:

KEY_PASSWORD=XXXXXX ./gradlew assembleRelease

References

Author

Rusty Kinnicutt

Rusty's spent over 15 years working in the gaming industry. From engineering games to frameworks and SDKs, he's witnessed many exciting twists and turns. Rusty just enjoyed his six-year Kongregate anniversary. Most of his energy at Kongregate is directed toward supporting our mobile game developers. He does all this as the lone East Coast engineer, operating out of a home office in an attic of a 200-year-old house in Providence, Rhode Island.

Quest for Progress: The Math of Idle Games

At GDC Europe this year I gave a talk that took a deeper look at some of the numbers, formulas, and patterns underlying idle games. The slides for the talk are on Slideshare and the GDC Vault, if you have access there.

In the slides you’ll find information about growth curves, formulas for bulk purchases, purchase optimization models, a variant on growth based on derivatives, a look at prestige cycles, callouts for some great new idle games, and an aside on stupidly large numbers.

However, what I hope becomes the biggest value from the talk is that all of the charts generated for the slides come from spreadsheets that I’ve opened up for general use. If you’re planning on doing a lot with them I recommend downloading the Excel versions, as Google Sheets starts to cry on some of these simulations.

These sheets allow you to play around with values in a variety of situations to see how the curves change. While it’s unlikely you’ll be able to fully model and balance an idle game with these, they will hopefully allow you to better understand the tools that you have available to build an engaging and compelling progression.

As a specific example, Sheet 1e models optimal player choice for buying generators (i.e. the buildings, investments, or whatever generates the main currency), with the ability to set multipliers for the generators that trigger when the player owns a certain number of them (similar to AdVenture Capitalist bonuses). By setting these carefully, you can create a progression that varies over time in terms of the most valuable generator, keeping the gameplay more varied for the player, like this:

I hope these turn out to be useful! If you have questions, find a bug, decide to make an extension to a sheet, or have an awesome new idle game you want to share, please feel free to reach out and email me at anthony@kongregate.com.

Author

Anthony Pecorella

Anthony has been at Kongregate for 7 years and is the director of virtual goods for browser games. He works to bring great games to the platform and help developers make a living with their games. He was the lead producer for the mobile version of AdVenture Capitalist.

Anthony also designs and develops games on the side with his indie game studio Level Up Labs, including the acclaimed tower defense RPG Defender's Quest: Valley of the Forgotten, and a MacArthur-grant-funded educational biology game, CellCraft.

Kongregate’s Animation Throwdown: The Quest for Cards mobile game launches today on iOS & Android!

San Francisco, Calif., September 15, 2016 – Order up! Animation Throwdown: The Quest for Cards is now available globally on iOS and Android.

Animation Throwdown: The Quest for Cards is the only collectible card game featuring five hit Twentieth Century Fox animated comedies: American Dad!, Bob’s Burgers, Family Guy, Futurama, and King of the Hill. For the first time ever, players get to assemble their favorite characters and experience iconic show moments in the ultimate deck-building game of domination. Cross-dress with Roger, pimp sweet lady propane in the heart of Texas, stop for a Butterface Burger with the Belchers, and pound a Pawtucket Patriot Ale (or two) without having to leave the comfort of your local New New York suicide booth!

Animation Throwdown is loaded with a hilarious mashup of screwball families, witty dialogue, and absurd card combinations. Get in on the family fracas as the Belchers, Hills, Griffins, Smiths, and Planet Express crew duke it out; have Tina unleash her charm bomb (and erotic friend fiction) when she throws down with Bobby Hill; get in on some “alien-on-alien” action as Roger takes on Kif; or end up on the ropes when Leela steps into the ring with Peter.

Animation Throwdown: The Quest for Cards will launch with a multiverse of morally-questionable content and game features that fans will love, including more than 460 battle cards with over 300 hilarious combos, as well as new content, campaigns, and PvP experiences more varied than Quagmire’s collection of “art”.

The highly anticipated free-to-play game has been launched by Fox Digital Entertainment and Kongregate, a leading publisher of mobile and web games and a wholly-owned subsidiary of GameStop Corporation. Kongregate co-developed the title with Chicago-based Synapse Games (developer of Tyrant Unleashed, Global Assault, and Spellstone) and San Francisco-based Chinzilla Games (developer of Little Alchemist).

Animation Throwdown: The Quest for Cards is free and now available on both iOS and Android devices. The game will be coming soon to other platforms, including kongregate.com.

eCPM Decay in Rewarded Video Ads

Some of you have heard the term "eCPM decay." Let's first understand what it means. If you are a publisher, CPM is the revenue you are getting for 1,000 impressions. If an advertiser is paying you a CPM of $5 and you show your users 1,000 impressions of that ad, you will receive $5. The advertising world is slightly more complex, however; most of the advertisers are actually paying CPI -- in other words, they are paying for installs. This means that unless the user clicks on an ad and installs the app, the publisher doesn't receive any revenue. What eCPM decay means is that if you take a certain user and show him more ads, he is not necessarily going to install more apps, so you wouldn't receive more money as a result.

Simple Example of eCPM Decay

Let's think of this scenario:

  • You have a single user in a given day
  • He wants to get 1 life for free, so he watches a video -- you as a publisher don't get paid for that
  • Let's say the chance of him installing the app is 1% -- if he does you get $5
  • The eCPM for the first impression is $50 (1,000 x 1% x $5)

Let's now consider another scenario:

  • The same user already watched 9 videos, and now he sees the 10th one in the same day
  • By the time he reaches the 10th ad, he is already blind to the ads, and the chance of installing is 0.2%
  • The ad network can't show the same ads to this user, so now he sees ads for apps that pay less -- let's assume $1 per install
  • The eCPM for the 10th impression is $2 (1,000 x 0.2% x $1)

What About RTB?

Some of the ad providers you might integrate are utilizing real-time bidding. This means that the price for every impression is determined by the marketplace in real time. If you look at the logs of the SSP or your mediation provider, you will actually see how the CPM bids decline from impression to impression. What happens behind the scenes is that advertisers are doing the same math we did in the example, so they are bidding higher on 1st impressions compared to later impressions. Machine Zone, for example, is known to pay a high CPM but demands that their ads only show up as a first impression to the user.

Why the Decay is Stronger with Rewarded Video

If you measure your eCPM decay with an ad revenue tracing platform like SOOMLA TRACEBACK, you will see that the eCPM decay is stronger with rewarded video ads compared to other formats. The reason for that is the misalignment of incentives. The user gets rewarded for watching the video while the publishers mostly get paid for installs. You might want to check this article about rewarded video payout for more details.

Mediation Helps a Bit with CPM Decay

If you use an ad mediation provider, you can improve the situation to some extent. By aggregating more ad providers, you are creating better fill rates and getting a larger variety of advertisers that can pay higher CPIs. By having more variety, you also reduce the blindness of the user to the ads and improve the chances a user will end up installing and generating revenue for you.

If You Can't Fight Them, Join Them -- Focus on 1st Impressions

CPM decay is here to stay, but instead of fighting it, there are ways to make the situation work to your advantage. If 1st impressions pay 10x more, why not focus on having as many 1st impressions as possible? Using an ad tracing platform like SOOMLA TRACEBACK reveals that most mobile games have between 10% and 40% opt-in ratios. This means that at least 60% of the users don't watch even the 1st impression. Getting more users to opt in means you will get a lot more of these high-paying impressions and can double if not triple your revenue.

DISCLAIMER:
Kongregate allows guest posts for topics we find interesting and relevant to our developer community. Guest posts can be created by developers or third-party companies, and all content is subject to review. A guest post does not imply any business relationship, use, or endorsement of any persons or products between Kongregate and those of the author.

Author

Yaniv Nizan

Yaniv Nizan is the CEO and Co-Founder of SOOMLA, the creators of TRACEBACK, a mobile analytics product that traces ad revenues to individual users. Prior to SOOMLA, Yaniv co-founded EyeView, a leading video advertising technology company. Mr. Nizan is a regular speaker at industry events and a mobile monetization thought leader with articles published in Gamasutra, Gamedev.net, Develop Magazine and others.

Optimizing Games for Low-End Devices

One of the biggest challenges in mobile game development is having your game run great on a low-end device without compromising the experience on a high-end one. We tend to optimize and budget the best experience for our high-end devices. There are many good reasons for this: better monetization, better engagement, etc. Low-end devices seem to run away from us in scope and get poorly prioritized because of the focus on high-end devices. But they’re still important! According to a recent Mixpanel trend report, ~10% of iPhone users have an iPhone 5 or below.

Can we have older devices run the same graphics as new ones?

You simply can’t. Give your players a comparable experience across all devices. Allow them to accomplish the same core activities that your high-end users do. The experience may not be the same, but the core progression should be close.

Tips on Optimizing for Low-End Devices

  • Think about low-end devices very early on in the project. Add checkpoints to test low-end devices at a frequency or feature check-in.
  • Categorize your devices in buckets like Low, Medium, and High Quality. For example, the iPhone 6s is a High Quality device, the Samsung Galaxy 3 is low-end, etc. This list will continually fluctuate as new devices come online and technology advances. Some important things to keep in mind are device specs, chip sets, video memory, video cards, etc.
  • Leverage the built-in Quality Settings in Unity. You can find it under Edit->Projects->Quality. Remember you can add/remove as many levels as you like. Customize it for your needs. I’d recommend going with three total: Low, Medium, and High. Unity has a bunch of controls you can tune per bucket. This is very helpful for a 3D game, but not so much on 2D.
  • Deliver different assets based on the device quality. High-end devices get HD; low-end ones get standard. This will help with memory footprint and performance (better FPS and fewer low-memory crashes). You can leverage iOS’s app-thinning technology or Unity’s asset bundles for this as well.
  • Run your game through different code paths based on the quality settings. For example, your dialogs may animate in/out when they dismiss. On low-end devices, you may want to disable the animations.
  • Disable masks and alpha. They can put a heavy tax on your frames per second.
  • Be prepared to make game-specific calls such as, "Which animations should I disable? What PFX should I enable/disable? Should I disable sound?"
  • Lower concurrent HTTP connections for low-end devices. They usually have fewer CPU threads to work with. Also, make fewer HTTP connections in general, since they hurt your battery life.
  • Create custom shaders for low-end devices. Limit extra passes and functionality. As an example, a low-end user can still function in your game without having an outline around him when selected.
  • Use your game engine's profiler to find hot spots on performance. Make sure to run it on the low-end device.

Feel free to reach out to me by leaving a comment below -- I’d be more than happy to share any data or code snippets we have.

Author

Siavash Ghamaty

Siavash Ghamaty is the VP of Engineering at Ultrabit/Kongregate. He loves big engineering challenges. When he's not racking his brain, he loves eating Mexican food and fantasizing about being an interior designer in his spare time.