Addressables and locale-specific data

by | Jun 30, 2021 | Programming, Tech, Unity3d

I decided to record my learnings about the Addressables system in Unity. In this post, I’ll talk about packing and downloading locale-specific data. It took me a while to figure this out so someone else may benefit from it.

Marci, one of the characters in  Flatstone Grove.

A couple of months ago we added support for Addressables to Flatstone Grove; the project Sponge Hammer Games are working on. The initial integration went fine. The game initialises itself, loads all addressable resources and the startup scenes and starts the game. It was a significant first step, but we reached the point of adding localisation to the app, so we need to extend the scope of the implementation.

The goal

We will localise the app to quite a few languages, and most of the locale-specific data will be audio. Also, running on mobile means, we must be careful how much we download to the user’s device. We need to organise the data in a way that allows achieving the following goals. Firstly, to pack locale-specific data together separate from the shared data set. Secondly, to keep download sizes to a minimum. 

The prototype

I think prototyping is a great tool. Creating one allows me to isolate a problem, work in a much smaller code-base and ignore any development rules we have set up in favour of learning something. This time I needed to get my head around the mechanism for defining, building and downloading content.

I set up an empty unity project. I created a scene, added a Button and a new MonoBehaviour, that responds to the button’s click. The code then performs the download operation.

In the project I created the following folder structure

|- Assets
   |- Data
      |- Dynamic
          |- book1
             |- Book1.prefab
             |- Some other data
          |- book1_en
             |- sc001.ogg
          |- book1_hu
             |- sc001.ogg

The idea is simple. The user can select their language (the language code: en, hu, etc). I use the same convention in naming the folder. I also use the code to identify the relevant parts of the data and download them.

I set up three asset groups that correspond to the folder structure above. 

Mapping content to Addressable groups.

There is one for the shared data (book1), one for the English audio (book1_en) and one for the Hungarian one (book1_hu). Then I dropped the folders (from the project view) to the group representing them. I also added three labels (lang:any, lang:en and lang:hu) and assigned those to the groups accordingly.

I set up a Google Cloud storage bucket to store the built data.

Added a new Addressable profile, named it GoogleCloud. I made sure the RemoteLoadPath setting corresponds to the storage bucket location. The last bit in square brackets (BuildTarget) identifies the platform that will use the built data. In my case, it resolves to the string StandaloneWindows64.

Addressables profiles

I built the data using the Addressables UI and uploaded files in the ProjectRoot/ServerData/StandaloneWindows64 folder to my Google storage bucket.

Code

Added the following function to the MonoBehaviour, and hooked up the button’s click event to call it via the unity UI.

public async void InitialiseAndDownload() { Debug.Log("Button clicked"); }
Code language: C# (cs)

Implementation time

Initialising the system is just a call to Addressables.InitialiseAsync(). In my first attempt, I used the Result field of the object to determine which resource locations (keys) are available. Then changed the code over to use async/await and spent a day tearing my hair out because the same code started throwing exceptions. It turns out internally the await path auto releases the result instance. So I got null reference exceptions all over the place.

public async void InitialiseAndDownload() { // This clears all downloaded asset bundle data // to force a new download every time I start the test. Caching.ClearCache(); // Initialise the system var initOp = Addressables.InitializeAsync(); await initOp.Task; // This downloads all common and language specific stuff. var keys = new[] { "lang:any", $"lang:{currentLangId}" }; await DownloadIfNeeded(keys); }
Code language: C# (cs)

Another point to note is the keys I use in the code above. It is not immediately clear from the Addressables documentation what the resource keys are. They can be addresses, file paths or labels you set up.

Addresses, by default, are generated from the file or folder path. You can simplify them by renaming the file entries in the AddressableGroups window.

So in my example, I can reference the English audio file lang:en, book1_en/sc001.ogg or Assets/data/dynamic/book1_en/sc001.ogg. The difference is the first will reference ALL assets that have the label lang:en associated with them.

What to download

The second function I implemented is the one that determines the expected download size and then kicks off the download operation.

private async Task DownloadIfNeeded(string[] keys) { var sizeOps = new Dictionary<string, AsyncOperationHandle<long>>(); // Determine the download size for all the keys // in parallel foreach (var k in keys) { var op = Addressables.GetDownloadSizeAsync(k); sizeOps[k] = op; } // Wait for all to complete await Task.WhenAll(sizeOps.Select(x => x.Value.Task)); // There should be some error handling here. var size = sizeOps.Sum(x => x.Value.Result); foreach(var k in sizeOps.Keys) { Debug.Log($"Size: {k} - {sizeOps[k].Result} bytes"); } Debug.Log($"Downloading total bytes {size}"); // Perform the download sequentally foreach (var k in keys) { Debug.Log($"Downloading {k}"); await Addressables.DownloadDependenciesAsync(k).Task; } Debug.Log("Download finished."); }
Code language: C# (cs)

Many other operations work with collections of IResourceLocations, but the Addressables.GetDownloadSizeAsync() function doesn’t. It needs calling for each key. In my case, the keys are lang:any and lang:en or lang:hu – depending on the value of currentLangId member variable. 

The first part performs async operations to determine the total download size. Then kicks off the download operations one by one. 

Conclusion

After some time, I managed to understand the system, set up and downloaded optional content packs. So my prototype is a success. I’ll take the learnings and turn this into production-ready form and add it to Flatstone Grove.

The system has been around for a few years now it has quite a bit of documentation. Which I really like but I think it is not beginner-friendly. For someone trying to learn, it can be somewhat confusing. Without it, I wouldn’t have been able to do this.

The sample project on GitHub seems to be well maintained. The only criticism I have for it is the lack of detailed documentation. Especially the advanced samples. There’s a top-level readme, but the code is entirely undocumented, making it much harder to read and use.

When encountering errors, the system throws all sorts of exceptions, but they are too generic and don’t necessarily contain any context explaining why they occur: invalid keys, null references, etc.

On the positive side, once I understood how the system works, I found it pretty cool. It hides a lot of the complexity behind managing game files for DLC. I think the Unity teams have done a pretty good job, and I hope the system will continue improving.

What is Flatstone Grove anyway?

Flatsone Grove

Flatstone Grove is an interactive storybook for three to six-year-old children in the form of an app running on Android and IOS devices. We designed our stories to teach children core values, such as bravery, love and caring. We take children’s safety very seriously. The app has no in-app purchases or ads, and the content is suitable for small children.

Want to learn more? Subscribe to our newsletter on the Sponge Hammer Games website, follow us on Facebook and Instagram

Related posts