February 24 2020
How would you cut your content update iteration times by 10x? You know, these 5+ days you may spend to ship a new build with updated game assets. Let’s see how to improve these times with addressables and PlayFab.
In this post, you’ll learn:
Years ago, I was well immersed in the hyper casual games sector. In this market, you have to continuously push new content updates to keep your audience engaged — i.e. ultra-short iteration cycles.
Maintaining short iteration cycles is a challenging process, as each application content update has a high base human cost to it.
It’s not always the development cycle that takes long, but the whole update process. That includes QA, store uploads, reviews, waiting for players to update to the new version on time (if they do at all)… that all takes time.
Often enough, issues you find in any point of that process requires starting from the beginning of the cycle. A wbroken build, a bug, an Apple employee not liking your new build, cosmic rays… it can be all sort of things.
To put it simply: just changing a few visual elements could easily take 5 working days (best case). That’s above $3000.
That’s an expensive pipeline.
You see, after some time I understood the need for being so cautious. It becomes easy to justify its price when you think of the alternative: to mess up the experience of 50+ million players. And as long as you’re making good cash, you can afford to pay these numbers.
But at the same time, I was sure there had to be a way to cut corners and use my time better.
Often enough, the game changes were purely cosmetic… and I really wondered if we had to go through the whole release process again. I had then wished we could just upload the new sprite somewhere and let the client update, skipping the rest of the process. Or something like that.
A few months in I discovered the power of asset bundles. The idea was simple: put content into some kind of ZIP files and let players download these. New content, new ZIP file. Easy, right?
The problem of this idea lied on its execution. Asset bundles are extremely complicated to get right and the slight mistake would cost you weeks worth of time to fix.
But here’s the key: Unity noticed and reacted to the complexity of asset bundles and decided to engineer a developer-friendly technology. They called it Addressables.
The Unity Addressable Assets system makes using asset bundles pretty much straight-forward. It allows you to cut the biggest obstacles when updating game content.
You tick a few checkboxes here and there, make some code adjustments and you’re suddenly ready to update your content as you go.
That means: you will stop spending 40+ hours to update your content and stick to 1 hour.
Traditional Content Update Pipeline | Addressables-Based Content Update Pipeline |
---|---|
1 Make the content changes | 1 Make the content changes |
2 Make a game build for each target platform | 2 Make an addressable asset bundle build for each target platform |
3 Go through QA (if failed: go to step 1) | 3 Upload asset bundles to your CDNs |
4 Upload to several application stores: Android, iOS, Steam, PS4, Nintendo (if failed: go to step 1) | 4 Your game updates its contents on the fly |
5 Wait for store version approval (if failed: go to step 1) | 5 Enjoy your update |
6 Wait for players to update on time (if failed: you missed this player) | -. Do something else |
7 Enjoy your update | -. Do something else |
However, there’s a small catch with Addressables…
Out of the box, this system only supports HTTP(s) servers that serve files over static URLs. Addressables won’t work with services that serve their content over download URLs generated dynamically, such as Dropbox or most CDNs.
Luckily, here’s the secret sauce of this blog post… I found a reliable way to make Addressables work with popular CDNs. I’ll show you how to do this with one of my favorite technology stacks: PlayFab by Microsoft.
Today, I’ll show you how to start serving your game assets through PlayFab CDNs to achieve light-speed game content updates.
Let’s get to it.
In this chapter, I’ll show you the entire process from a very high-level view to cut out content update iteration times by 10x.
You can get to meet the technical requirements by investing a bit of time in your project.
Spoiler: the process requires just a bit of your time.
The first step in the journey is to move from a traditional asset pipeline to an addressables-based asset pipeline.
Don’t worry, that sounds more complex than it is.
Here’s the process:
I’ve written tons of posts about how to do most of these steps. If you’re new to this, I suggest you to first follow my Unity Addressables Tutorial to help get you started.
In this post, I’ll put most of my focus on the fourth step — i.e. adding PlayFab support to your addressables-based asset pipeline.
(I’ll assume you have your addressable groups already set up)
You have new game content. Great.
So, where are you going to store it? Your users must download it from somewhere.
So this is where PlayFab CDN comes into play.
What do you need to set up in the PlayFab backend? It turns out… not much!
You can skip this chapter if you are already familiar with PlayFab.
The first step is to get a PlayFab account, if you don’t have one already. This hopefully won’t need a tutorial 🙂
Once you went through the process, go to your dashboard and navigate to the file manager. This is where we will upload our game assets later on.
Here’re the steps:
Now that the backend system is ready for action, let’s go to the heart of the matter.
In this chapter, we are going to extend the addressables system to support content distribution over PlayFab CDNs.
This is about to get fun.
Here’s the deal: we’re going to extend the Addressables system.
Correct, we wont’t touch the original system. That’s important for your project, as upgrading your addressables package version in the future will be as easy as clicking the upgrade button.
This is how we’re doing this:
This one is easy.
Head to https://github.com/PlayFab/UnitySDK and check the quick getting started guide.
Basically, you have to download the editor extension that will install the SDK for you. Then, it’s just a matter of logging in to your account through the interface the editor extension provides.
Oh, and don’t forget to tweak the API connection settings like shown below (blurred because it’s NSFW**— Not Safe For my Wallet).
By now, you have the Unity PlayFab SDK eager to work. So let’s give it some fuel.
So far, you’ve set the credentials for the Unity PlayFab extension to work. But we’re missing an important piece: to do the login for each of your players. This helps securing your wallet, so only authorized players will be able to download your content.
The easiest approach to player login is to start a coroutine that calls LoginWithCustomID and waits for it to finish:
private IEnumerator LoginToPlayFab()
{
var loginSuccessful = false;
var request = new LoginWithCustomIDRequest {CustomId = "MyPlayerId", CreateAccount = true};
PlayFabClientAPI.LoginWithCustomID(request, result => loginSuccessful = true,
error => error.GenerateErrorReport());
return new WaitUntil(() => loginSuccessful);
}
For the curious minds: here is the gift LoginWithCustomID returns to me:
For more info, check the documentation.
Now that our players can log in the system, we’ll give the addressables system the power to download from PlayFab CDNs.First, we will initialize the system our way:
private IEnumerator InitializeAddressables()
{
Addressables.ResourceManager.ResourceProviders.Add(new AssetBundleProvider());
Addressables.ResourceManager.ResourceProviders.Add(new PlayFabStorageHashProvider());
Addressables.ResourceManager.ResourceProviders.Add(new PlayFabStorageAssetBundleProvider());
Addressables.ResourceManager.ResourceProviders.Add(new PlayFabStorageJsonAssetProvider());
return Addressables.InitializeAsync();
}
Addressables uses the concept of providers to give you a way to extend the system for different formats and algorithms. So we’re adding the new providers we are about to implement.
I know, I know… it won’t compile. It’s fine, just bear with me.
Here we create our custom PlayFab asset bundle provider:
[DisplayName("PlayFab Storage")]<br>public class PlayFabStorageAssetBundleProvider : AssetBundleProvider
{
public override void Provide(ProvideHandle provideHandle)
{
var addressableId = provideHandle.Location.InternalId.Replace("playfab://", "");
PlayFabClientAPI.GetContentDownloadUrl(
new GetContentDownloadUrlRequest() {Key = addressableId, ThruCDN = false},
result =>
{
var dependenciesList = provideHandle.Location.Dependencies;
var dependenciesArray = provideHandle.Location.Dependencies == null ? new IResourceLocation[0] : new IResourceLocation[dependenciesList.Count];
dependenciesList?.CopyTo(dependenciesArray, 0);
var resourceLocation = new ResourceLocationBase(result.URL, result.URL, typeof(AssetBundleProvider).FullName, typeof(IResourceLocator), dependenciesArray)
{
Data = provideHandle.Location.Data,
PrimaryKey = provideHandle.Location.PrimaryKey
};
provideHandle.ResourceManager.ProvideResource<IAssetBundleResource>(resourceLocation).Completed += handle =>
{
var contents = handle.Result;
provideHandle.Complete(contents, true, handle.OperationException);
};
},
error => Debug.LogError(error.GenerateErrorReport()));
}
The code is a bit verbose but the principles behind it are simple:
The addressables system calls our Provide function when it detects we need an asset bundle through our PlayFab CDN. It passes the addressable asset ID.
The first thing we do is to remove the playfab://
preffix to get the real addressable asset ID. The prefix is just a trick I created to know we’re talking about content hosted in PlayFab (more on this later).
We then ask the PlayFab SDK where we can download this asset from. This generates a dynamic URL that we can use to go through the usual download procedure.
We delegate the download responsibility to the addressables system (I know, a bit verbose).
We do the same for the JSON provider, which is needed for reading the catalog that contains information about our addressable assets:
public class PlayFabStorageJsonAssetProvider : JsonAssetProvider
{
public override string ProviderId => typeof(JsonAssetProvider).FullName;
public override void Provide(ProvideHandle provideHandle)
{
if (provideHandle.Location.InternalId.StartsWith("playfab://") == false)
{
base.Provide(provideHandle);
return;
}
var addressableId = provideHandle.Location.InternalId.Replace("playfab://", "");
PlayFabClientAPI.GetContentDownloadUrl(
new GetContentDownloadUrlRequest() {Key = addressableId, ThruCDN = false},
result =>
{
Assert.IsTrue(provideHandle.Location.ResourceType == typeof(ContentCatalogData), "Only catalogs supported");
var resourceLocation = new ResourceLocationBase(result.URL, result.URL, typeof(JsonAssetProvider).FullName, typeof(string));
provideHandle.ResourceManager.ProvideResource<ContentCatalogData>(resourceLocation).Completed += handle =>
{
var contents = handle.Result;
provideHandle.Complete(contents, true, handle.OperationException);
};
},
error => Debug.LogError(error.GenerateErrorReport()));
}
}
The code structure is similar. The biggest difference is the early exit if we’re not trying to load a json file through the addressables PlayFab CDN.
This script will provide the hash of our asset bundles to the Addressables system, so we know if the client has to download a newer asset bundle:
public class PlayFabStorageHashProvider : ResourceProviderBase
{
public override void Provide(ProvideHandle provideHandle)
{
var addressableId = provideHandle.Location.InternalId.Replace("playfab://", "");
PlayFabClientAPI.GetContentDownloadUrl(
new GetContentDownloadUrlRequest() {Key = addressableId, ThruCDN = false},
result =>
{
var resourceLocation = new ResourceLocationBase(result.URL, result.URL, typeof(TextDataProvider).FullName, typeof(string));
provideHandle.ResourceManager.ProvideResource<string>(resourceLocation).Completed += handle =>
{
var contents = handle.Result;
provideHandle.Complete(contents, true, handle.OperationException);
};
},
error => Debug.LogError(error.GenerateErrorReport()));
}
}
Not much to say about it.
This step is useful if you’re generating remote catalogs.
You’ll need these remote catalogs if you’re planning to update your content without having to push new builds (this would be ideal).
The idea behind this is to build the addressable asset bundles as usual and then to modify the catalog Unity produces to select our new providers for these assets instead of the default ones.
Here’s the code I use for a custom build script for Unity addressables PlayFab:
/// <summary>
/// Build script that takes care of modifying the settings.xml to use our json provider for loading the remote hash
/// </summary>
[CreateAssetMenu(fileName = "PlayFabStorageBuildScript.asset", menuName = "Addressables/Content Builders/PlayFab Build")]
public class PlayFabStorageBuildScript : BuildScriptPackedMode
{
public override string Name => "PlayFab Build";
protected override TResult DoBuild<TResult>(AddressablesDataBuilderInput builderInput, AddressableAssetsBuildContext aaContext)
{
var buildResult = base.DoBuild<TResult>(builderInput, aaContext);
if (aaContext.settings.BuildRemoteCatalog)
{
PatchSettingsFile(builderInput);
}
else
{
Debug.LogWarning("[TheGamedevGuru] PlayFab: Addressables Remote Catalog is not enabled, skipping patching of the settings file");
}
return buildResult;
}
private void PatchSettingsFile(AddressablesDataBuilderInput builderInput)
{
// Get the path to the settings.json file
var settingsJsonPath = Addressables.BuildPath + "/" + builderInput.RuntimeSettingsFilename;
// Parse the JSON document
var settingsJson = JsonUtility.FromJson<ResourceManagerRuntimeData>(File.ReadAllText(settingsJsonPath));
// Look for the remote hash section
var originalRemoteHashCatalogLocation = settingsJson.CatalogLocations.Find(locationData => locationData.Keys[0] == "AddressablesMainContentCatalogRemoteHash");
var isRemoteLoadPathValid = originalRemoteHashCatalogLocation.InternalId.StartsWith("playfab://");
if (isRemoteLoadPathValid == false)
{
throw new BuildFailedException("RemoteBuildPath must start with playfab://");
}
// Change the remote hash provider to our PlayFabStorageHashProvider
var newRemoteHashCatalogLocation = new ResourceLocationData(originalRemoteHashCatalogLocation.Keys, originalRemoteHashCatalogLocation.InternalId, typeof(PlayFabStorageHashProvider), originalRemoteHashCatalogLocation.ResourceType, originalRemoteHashCatalogLocation.Dependencies);
settingsJson.CatalogLocations.Remove(originalRemoteHashCatalogLocation);
settingsJson.CatalogLocations.Add(newRemoteHashCatalogLocation);
File.WriteAllText(settingsJsonPath, JsonUtility.ToJson(settingsJson));
}
}
After you created this script, make sure to create an instance of this scriptable object by right-clicking in your project view and selecting Addresables/Content Builders/PlayFab Build. You’ll need this script for the next section.
Congratulations.
Your Unity Addressables PlayFab integration is (almost) ready.
Let’s build now a simple test scene that downloads a sprite from your PlayFab CDN.
Here’s the addressables exercise we are going to do: we’ll load a sprite from our newly set up PlayFab CDN.
We can do this through a script like below:
[SerializeField] private AssetReference spriteReference = null;
[SerializeField] private Image image = null;
private IEnumerator TestRemoteAddressableAsset()
{
var asyncOperation = spriteReference.LoadAssetAsync<Sprite>();
asyncOperation.Completed += result => image.sprite = asyncOperation.Result;
}
Hey, don’t look at me like that. I don’t get paid to produce AAA blog-post code.
Once you mark your sprite to be an addressable asset, assign it to your new script like shown below:
Interesting enough, we didn’t specify where the user should download it from.
That’s exactly the power of addressables: we don’t specify the download location from code…
Instead, we specify the download URL through the addressables profiles window(Window → Asset Management* *→ Addressables).
Open the profiles and set the RemoteLoadPath to playfab://[BuildTarget]* so it looks like below:
By using this convention, addressables will know it is time to call our custom addressables PlayFab providers.
☻ Custom build script: let’s make addressables be aware of our new PlayFab build script. Open the addressables settings (AddressableAssetSettings file) and add a reference to the custom build script instance we created in the previous chapter.
☻ Addressables group settings: in the Addressables Groups window, we inspect the group the sprite belongs to. That will redirect us to the sprite group settings, where we’ll set it up so its loaded from the PlayFab CDN. Adjust the three highlighted settings as shown below:
That was the hardest part.
Now it’s time to build our addressable asset bundle. In Play Mode Script, we select Use Existing Build to force the editor to use our asset bundles. Then, we click on Build → New Build → PlayFab Build.
After you built the asset bundles, you can upload them to your PlayFab CDN through the PlayFab File Manager shown in chapter 2. You can find the asset bundles in your local ServerData directory.
With this done, pressing the editor play button will make addressables load your sprite from the PlayFab CDN.
Now, your clients will download the latest copy of your all addressable assets whenever you update them. That’s powerful because now you can skip the whole application build process.
Just change your assets, update your addressable asset bundles and upload them.
While you can clearly profit from such a refined process, it might leave some questions unanswered.
Hopefully, by giving you access to an example project you’ll find it easier to integrate the new pipeline in your project.
After all, we are all in the business of saving time.
All that long journey… what for? Just to save at least 35 hours of work and frustration each time you have to update game content?
Yeah… that might be worth.
Now, let me make it easier for you.
Get the example project with instructions now by subscribing below.