Friday 26 February 2021

Coming back (with asset bundles)

 Oh dear, to say that it's been a while it's very much an understatement.

Last time I posted here was something like 3 years ago.

Well, I have been up to some other things, mostly completing my computer science degree, and getting a job in the industry. Also, other personal stuff that kept me away from all this.

Turns out, I may have some extra spare time now to dedicate to this blog again, especially because I realized I actually missed it. I don't really know how often I'll be able to post here, but I'll try to put up something of interest every now and then. In fact, we started using Unity at my company now, and I got to learn some things.

With that out of  the way, today's topic is....

Asset bundles


A relatively less exciting topic probably, but something I had to deal with in a couple of occasions.

In Unity, the only way you can load external assets at runtime is through asset bundles. Don't get confused here: I'm talking about assets that are not part of your project, files that are not in your "Assets" folder when the game starts. The idea is to load in those assets that reside outside of your project.

Please, start your "bundle" word count now.

Creating asset bundles

First thing to do is of course, create the asset bundle(s).
Let's say we want to export a prefab, a simple sphere (classic). 
After creating a simple sphere prefab, take a look at the bottom of the inspector window for the prefab:




Yes, there's an asset bundle property. All you have to do is to assign a name (as shown in the image) to that property, and that will be the asset bundle name.

Once you assigned all the names to all your assets you want to "bundle", it's time to build them. To do so, we use a very simple script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using System.IO;
using UnityEditor;

public class AssetBundleCreate 
{
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundles()
    {
        const string assetBundleOutputDir = "Assets\\AssetBundles";
        if (!Directory.Exists(assetBundleOutputDir))
        {
            Directory.CreateDirectory(assetBundleOutputDir);
        }
        BuildPipeline.BuildAssetBundles(assetBundleOutputDir, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
    }
}

Keep in mind that this is an editor script, so it should be placed in a folder called Editor, under Assets.

This will create a menu option which will export your bundles in the specified folder:


Ok, I have a question: what if I want to bundle something that is actually not in the project?

Well, that's possible to do, all we have to do is to sismply copy the content in the project first, and assign it a bundle name and that's it, when building the bundles, it will be exported.

You can use any method to copy files/folder into your project, check  this directory copy method.

Then, let's say you copy a folder and you want to turn all your .PNG to asset bundles, because why the hell not, you can change the previous method to this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void BuildAllAssetBundles()
    {
        const string assetBundleOutputDir = "Assets\\AssetBundles";
        const string localPNGFolder = "Assets\\PNGs";
        if (!Directory.Exists(assetBundleOutputDir))
        {
            Directory.CreateDirectory(assetBundleOutputDir);
        }

        DirectoryCopy("sourceFolder", localPNGFolder, true);
        AssetDatabase.Refresh(); //Important

        string[] files = Directory.GetFiles(localPNGFolder, ".PNG", SearchOption.AllDirectories);

        //Assigne bundle name to imported asset
        foreach(string f in files)
        {
            string name = Path.GetFileNameWithoutExtension(f);
            AssetImporter.GetAtPath(f).SetAssetBundleNameAndVariant(name, "");
        }

        BuildPipeline.BuildAssetBundles(assetBundleOutputDir, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
    }

The method will do the following:
  • Grab the external folder
  • Copy it into the project (in the folder specified at line 4)
  • Grab all .PNG files from the local folder
  • Assign a bundle name to each, using the name of the file itself (or use any custom name you need)
  • Build bundles
That's it, now all your PNGs are exported as asset bundles.

Loading asset bundles


Ok well, if you export asset bundles is likely because at some point you want to import them. 
The whole idea behind asset bundles is that they can be loaded in at runtime. So, we can create a MonoBehavior script that will do just that:


1
2
3
4
5
6
7
8
9
 public void LoadBundlePrefab()
    {
        var bundle = AssetBundle.LoadFromFile("pathToTheAssetBundleFile");
        if(bundle != null)
        {
            var prefab = bundle.LoadAsset<GameObject>("Sphere");
            Instantiate(prefab);
        }
    }

That's all you need. One important thing to mention: notice how I passed the name "Sphere" when I grab the pfefab in the bundle. That;s because it's the name of the actual GameObject that we exported, so it's important that, if you need to get the actual prefab from the bundle, you keep track of the name. Remember, the name doesn't have to be the same as the one you gave to the asset bundle, rather, is the name of the prefab itself you created before exporting.

Ok, I have another question: the prefab in my asset bundle is enormous, instantiating it takes 5 seconds, which hangs my game and breaks everything. Can I load it async?

Yes, you can load it async:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void LoadPrefabAsync()
{
    StartCoroutine(LoadBundlePrefabAsync()):
}

    public IEnumerator LoadBundlePrefabAsync()
    {
        var bundleRequest = AssetBundle.LoadFromFileAsync("pathToTheAssetBundleFile");
        yield return bundleRequest;

        var bundle = bundleRequest.assetBundle;
        if (bundle != null)
        {
            var prefabRequest = bundle.LoadAssetAsync<GameObject>("Sphere");
            yield return prefabRequest;

            var prefab = prefabRequest.asset;
            Instantiate(prefab);
        }
    }

The method above is just the async version of the previous one, plus that simple function that creates the coroutine. This won't hang your game as the bundle is loading.

Conclusion

If you need to use asset bundles, this post is for you.
There's something I haven't mentioned yet, which is the asset bundle variant. That is the second input field you see in the inspector, which you can ssign via code but we left empty

1
AssetImporter.GetAtPath(f).SetAssetBundleNameAndVariant(name, "");

The documentation doesn't say much about it besides that you can have different variations of your bundle. If you assign a value, the name of your bundle file will have that value as the extension. I haven't experiemented much with it yet, so for now, I'll let you explore this on your own in the wilderness of the internet. Should I understand fully how to use it, I will update this post!