Air Hockey Arcade is my current project I’m working on. I want to be able to add and update content without making the end user download the entire game again. Unity gives developers the ability to load content in at runtime using AssetBundles. Android’s Play Store allows developers to include two additional files with their apk. These files are called expansion files and can include anything but must remain within 2 GB in size each.
Another option is to store AssetBundles on another server and have the application call out for updates. Unfortunately I don’t have a server for this.
My solution is to zip all my AssetBundles into an expansion file and extract each bundle into cache (Application.temporaryCachePath) as I need them.
If you would like a better understanding of what expansion files are this link covers them: https://developer.android.com/google/play/expansion-files.html
Also before I start explaining my implementation Unity does provide an easy way to use the main expansion files to reduce the APK size but I don’t believe it provides all the functionality that mine does. More on Unity’s expansion file feature can be found here: https://docs.unity3d.com/Manual/android-OBBsupport.html
My goal for this implementation is to allow myself to easily manage:
- The versioning of the APK and expansion files.
- Packaging AssetBundles into expansion files.
- Loading AssetBundles from expansion files at runtime.
- The ability to run from the unity editor on any scene or from an android mobile device.
Here’s a quick sketch of my build pipeline. I’ll be going into more detail on each step below.
Persistent singleton monobehaviour that is used at application runtime to manage starting the program and loading and unloading AssetBundles. When a scene is started in the unity editor I have a small script that checks to see if the scene contains the apploader. If the scene doesn’t the apploader is added to the scene and initialized before anything else starts. This only happens when testing scenes in unity because the default scene will have the apploader already.
The AppLoader also has methods to handle loading assets when requested by any other component in the application.
Initialization at runtime consists of:
- Deserializing the appdata.xml
- Check if the expansion files exist and if they don’t download them from the play store.
- If we’re not running on Android then use the override values to check the windows locations.
- Load the manifest from the main expansion file.
- If we have an initial scene load it.
A model that contains essential values for the apploader to initialize. This class is serialized using DataContract into an xml file called appdata.xml. This file resides in the resources folder and is available to the AppLoader at runtime.
|Bundle Version||Same as the Version on Player settings in the identification section.|
|Android Version||Same as the Bundle Version Code on Player settings in the identification section.|
|Main Expansion File Version||The version of the main expansion file. This is the same version as the APK file when the expansion file was deployed. A new APK can be deployed and use the previous expansion file.|
|Patch Expansion File Version||Same as the main expansion file.|
|Expansion Local File Path Override||This allows me to run the application in windows. When running in windows this directory path is used instead. This is where the main and patch expansion files for windows are placed.|
|Expansion Cloud File Path Override||The location of where the AssetBundle files are to package for windows.|
|Android Bundle Identifier||Same as the Package Name on Player settings in the identification section.|
|Initial Scene Name||If populated, the apploader will load this scene from the AssetBundles after initialization.|
|Public Key||The Android public licensing key.|
|Enable Main Expansion File||When set to true the apploader will attempt initialize the main expansion file.|
|Enable Patch Expansion File||When set to true the apploader will attempt initialize the patch expansion file.|
<?xml version="1.0" encoding="utf-8"?> <AppDataModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://spacecastlegames.com/appdatamodel"> <AndroidBundleIdentifier>4</AndroidBundleIdentifier> <BundleIdentifier>1.0</BundleIdentifier> <EnableMainExpansionFile>true</EnableMainExpansionFile> <EnablePatchExpansionFile>false</EnablePatchExpansionFile> <ExpansionCloudFilePathOverride>C:\dev\workspace\Unity5Projects\AirHockey2 - Copy\AssetBundles\StandaloneWindows</ExpansionCloudFilePathOverride> <ExpansionLocalFilePathOverride>C:\dev\workspace\Unity5Projects\AirHockey2 - Copy\AssetBundles\obb</ExpansionLocalFilePathOverride> <InitialSceneName>MenuScene.unity</InitialSceneName> <MainExpansionFileVersion>4</MainExpansionFileVersion> <PackageName>com.spacecastlegames.airhockeyarcade</PackageName> <PatchExpansionFileVersion>0</PatchExpansionFileVersion> <PublicKey>[Long Key]</PublicKey> </AppDataModel>
This is a custom Unity editor script used to edit appdata.xml.
I use the AssetBundle Browser provided by unity to package the AssetBundles. Link to Unity AssetStore: https://www.assetstore.unity3d.com/en/#!/content/93571
This is an editor script for generating our own manifest and bundling all the AssetBundles into zip files which are used as out expansion files.
The expansion file name is generated through a property by using the version information in the AppDataModel class.
Generated Manifest file:
<?xml version="1.0" encoding="utf-8"?> <ArrayOfAssetBundleMapping xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://spacecastlegames.com/assetbundlemapping"> <AssetBundleMapping> <AssetBundleName>hockeyrink</AssetBundleName> <AssetName>HockeyRink.unity</AssetName> <AssetPath>Assets/_Project/Scenes/AScenes/Levels/HockeyRink.unity</AssetPath> <Dependencies xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" /> <IsPatched>false</IsPatched> </AssetBundleMapping> <AssetBundleMapping> <AssetBundleName>mainmenu</AssetBundleName> <AssetName>MenuScene.unity</AssetName> <AssetPath>Assets/_Project/Scenes/AScenes/MenuScene.unity</AssetPath> <Dependencies xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" /> <IsPatched>false</IsPatched> </AssetBundleMapping> <AssetBundleMapping> <AssetBundleName>paddles</AssetBundleName> <AssetName>TempPaddle.prefab</AssetName> <AssetPath>Assets/_Project/Prefab/Models/TempPaddle.prefab</AssetPath> <Dependencies xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" /> <IsPatched>false</IsPatched> </AssetBundleMapping> </ArrayOfAssetBundleMapping>
During initialization of the App Loader our generated manifest is loaded. When the program requests an asset to be loaded the asset name is searched for in the manifest to see which AssetBundle its part of. If the patch flag is set the program uses the patch expansion file instead of the main file. This allows me to make changes to assets and only have to update the patch file and the APK.
|\AssetBundles||Root Directory for AssetBundles.|
|\AssetBundles\Android||Android AssetBundles are created here by Asset Bundle Browser.|
|\AssetBundles\StandaloneWindows||Windows AssetBundles are created here by AssetBundle Browser|
|\AssetBundles\obb||Windows expansion files are created here by AssetBundle Mapping Generator.|
|\AssetBundles\obbAndroid||Android expansion files are created here by AssetBundle Mapping Generator.|
The asset loading happens asynchronously and seems to be working quite well. I don’t have any automated cleanup for assets that are no longer being used by the app, so that might be something to add in the future.
I didn’t get a chance to deep dive into the code but if anyone is interested in a particular part let me know and I’ll make another post on it.
I’d love to know how others have dealt with this.