[Check the original post at Unity Addressables: PlayFab CDN Integration]
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:
- How you're wasting days of work with every game patch you do to update content
- The secret to cutting the content iteration times from days to 1 hour with Addressables and PlayFab
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 broken 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.
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.
Quick Navigation (redirects to new tab)
Chapter 1: The Process — Revealed
Chapter 2: Preparing Your PlayFab Dashboard
Chapter 3: Integrating Unity Addressables With PlayFab
1. Import Unity PlayFab SDK for Addressables
3. Addressables Initialization
4. Asset Bundle Provider for PlayFab
7. Custom Addressables Build Script for PlayFab
Chapter 4: Unity Addressables: Example Setup for PlayFab
Chapter 5: How To Get It All — Now
CHAPTER 1: The Process — Revealed
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.
There are some requirements that almost every project can achieve.
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:
Install the Unity addressables package.
Move your desired assets from a direct-reference workflow to a indirect-reference system.
Update your code to accommodate for indirect references and import the Gamedev Guru's PlayFab toolkit from this post
Tweak your addressable asset groups to point to your PlayFab CDN
Build. Upload. Profit
Steps to Reducing Content Update Iteration Times With PlayFab
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)
Unity Addressables - Learn Addressables the Guru's Way
CHAPTER 2: Preparing Your PlayFab Dashboard
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:
In your dashboard, create a new game title
Click on Engage → Content
Open the File Manager
Create a new directory named after your target platform — e.g. Standalone64 or Android
Enter the new directory. Here's the place where you'll upload the asset bundles that contain your addressables content (keep this browser tab open for later).
CHAPTER 3: Integrating Unity Addressables With PlayFab
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:
First, we'll import the PlayFab Unity Extensions to integrate the SDK
Then, you and I will create a custom initialization function to set up the addressables system
Third, we'll create custom addressables providers for asset bundles, JSON documents and hash files. This way, we teach Addressables how to download that content type from PlayFab CDNs.
Finally, we'll add a custom addressables build script to tell the addressables catalog generator that we actually want to use PlayFab.
1. Import Unity PlayFab SDK for Addressables
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).
Unity Addressables PlayFab: Plugin Setup
By now, you have the Unity PlayFab SDK eager to work. So let's give it some fuel.
2. Login to PlayFab
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:
PlayFab LoginWithCustomIDRequest Result
For more info, check the documentation.
3. Addressables Initialization
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.
4. Asset Bundle Provider for PlayFab
Here we create our custom PlayFab asset bundle provider:
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).
5. JSON Provider for PlayFab
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.
6. Hash Provider for PlayFab
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.
7. Custom Addressables Build Script for PlayFab
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.
CHAPTER 4: Unity Addressables: Example Setup for PlayFab
Congratulations.
Your 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.
- In line 1, we declare an indirect reference to our sprite
- In line 5, we start the sprite loading process
- Finally, in line 6 we reap the rewards and assign the newly loaded sprite to our UI Image
Once you mark your sprite to be an addressable asset, assign it to your new script like shown below:
Addressables PlayFab: Sprite Demo
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:
Addressable Profile Setting for PlayFab
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.
Addressable System Settings: Adding PlayFab Builder
» 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.
Building Your Addressable Assets for PlayFab Distribution
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.
CHAPTER 5: How To Get It All — Now
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.
Get the example project with instructions now here.