Unity3d Asset Bundles – Unloading

This article is part of a series. If you’d like to skip ahead, you can go to the master post that links to them all.

This room is a mess. I’m constantly getting after my kids to clean their rooms. Why? Because two days later they complain they can’t find one of their favorite toys! It’s not my job to track their toys. But it is my job to track my game’s memory. We can’t leave stuff lying around in memory, or it’s just going to fill up and crash.

Memory management is of supreme importance in mobile game development. And of high importance in any other platform as well. Your game must run forever! You can’t just leave the textures from three levels ago in memory. You have to make sure things get unloaded from memory. But at the same time you don’t want to accidentally unload something you’re still using.

Obviously you will be tracking and deleting your loaded levels and GameObjects. But also remember that the asset bundles themselves have some memory overhead. In fact, on some platforms, you can only have a maximum number of bundles open at a time. You have to clear them out or the app simply crashes!

 

It’s been a year in the making of this single blog entry. Originally I wanted to discuss the unloading of asset bundles, and I will get to that. But in the course of working on my latest project I realized just how deep the rabbit hole goes.

It turns out that knowing when and how to unload a bundle is more art than science. It’s very specific to how you plan on using your assets. Even different assets within the same game have different needs. Here I’m going to describe what Unity allows, but there will need to be a bonus post on how to recognize what assets belong in what category.

The first rule to remember is that a bundle won’t unload anything that’s been loaded until the bundle is closed. I can’t stress this enough! It is not handled the same way the assets built into the player are. Assets loaded via Resources.Load() will go away when they aren’t referenced anymore and you do a Resources.UnloadUnusedAssets(). Bundles don’t do this! They will not unload until the bundle is closed, even if the assets aren’t used anymore. This makes closing bundles absolutely critical to managing memory effectively.

 

Toward this end, Unity offers two modes of closing a bundle: a) where the contained assets are left in memory and b) where the contained assets are removed from memory. This sounds so simple, but it actually creates significant potential complication.

A naive approach would always use the unloading version, so that you keep open a bundle as long as you’re using any of the assets inside it. However, there are limits to the number of bundles you can have open on some platforms. So what do we do? We unload as early as we can. But even that has a catch – an asset that was left in memory won’t be re-used if the same bundle is reopened later. It will instead make a duplicate copy. So be sure you’re really done with it before closing it!

So where do we go from here? For now, I leave it as an exercise to the reader to work out what bundles you need to keep around and which ones are safe to unload early. Remember to consider how assets shared between two bundles are also distinctly affected by whether the requesting bundles can be unloaded or not.

Automatic Texture Scaling Through Asset Bundles

This article is part of a series. If you’d like to skip ahead, you can go to the master post that links to them all.
Last time we upgraded a game to the Unity3d 5 asset bundle system, and dealt with handling shared data files. This time we will explore an unexpected use for asset bundles – automatic texture scaling for different classes of device.

On this project, the artists really outdid themselves – very high quality, high resolution artwork. Unfortunately, rather too high for most of the devices we wanted to play on! They had done a lot of work to balance each image size against its detail and how large it would be used in the game. Notice how we had to make the high detailed icon relatively large compared to the core game sprite, all because the icon had more detail.

Forced Balance

But there is a completely different way to approach the problem. A much simpler one, in fact. First you decide what device you consider the largest pixel size you want to support. In today’s market, this is typically a tablet device of some flavor. Art then designs everything at that consistent scale that looks good on that device. Now the art looks like the following image. Notice how they are all scaled based only on their visual importance in the game, with no wasted pixels. The car, a main focus of the game, is now large compared to the button icon that is only occasionally used.

Okay, but how do we support the rest of the phones and other devices that are smaller or less powerful? Trying to run those full size textures on just a different tablet is enough to drag the framerate down to unplayable. Why is that? It comes back to how mobile devices work. On a desktop machine, the video card typically has a dedicated memory space for textures. They are uploaded once (when things go right) and they never touch the main computer memory again. Every frame that is drawn uses that custom video memory. In mobile devices, there is no such dedicated memory. Every frame that is drawn pulls every texture through the same memory system the main processor is using. Only so much data can be pushed to the renderer in a given amount of time, and bigger textures means more data needed. Even if the framerate can be maintained, it takes precious battery power to transfer the data. Mipmaps can help reduce how much of the texture is used at a time, but you still need the entire texture somewhere in memory, taking up room that other applications use and need.

So back to the texture sizing. How do we handle those devices that don’t need the extra pixels? Simple – scale the textures. It’s a fairly simple thing to ask Unity to scale the textures for you, using the texture settings of the “maximum texture size”. And you can save those changes into asset bundles! Then the client once again can get the correctly sized textures by simply deciding which bundle directory to load. Easy peasy.

Things we learned while creating an automated system to do those bundle builds for us:

  • This kind of texture scaling and processing works just fine with both the pre-Unity 5 and Unity 5 asset bundle system.
  • Make sure you edit texture settings before any BuildPipeline.PushAssetDependencies. You can safely do so once you’ve balanced all the Push with Pops, however. Like if you are doing one build to generate multiple device asset sets.
  • Be sure to call AssetDatabase.StartAssetEditing/Stop only once before starting each build process. Unity does a bunch of extra processing even if you nest them. Wrap it in your own reference counting if you must.
  • It works pretty good to use the Default texture settings as the “master” value, setting the current platform’s custom settings to whatever you calculate for that bundle.
  • Remember that the default ‘max size’ of a texture is nothing like the actual size of the texture. There are ways of asking Unity for the true size of a texture to get the accurate default size.
  • Expect to scale the textures in powers of two, simplifies a lot of the task.

Next week we will wrap up our series by demystifying the unload process!

Unity3d Asset Bundle Data Sharing

This article is part of a series. If you’d like to skip ahead, you can go to the master post that links to them all.
Last week on our asset bundle journey we got started converting our old asset bundle system to the new Unity 5 one. It was actually really simple. But there is still a big problem that needs to be solved.

When you build bundles, you mark each asset that the game is going to ask for when you run the game. When you flag a single texture, it works just fine. But what about shared assets? For example a prefab holding a sprite that uses a texture atlas. Unity will gladly pick up the texture for the atlas and add it to the bundle for you. No problem there. But what happens when you add a different prefab in another bundle that wants the same atlas?

That’s when things get confusing. Will unity add the texture to both bundles? Yes. Uh-oh. Then you have wasted some memory on a duplicate texture. Worse, Unity seems to get confused by loading the same texture twice! No, Unity does not figure out that it is the same texture for you, loading it only once. Bad times. But I thought Unity handles dependencies for you in the new system? It does, but only considers each bundle separately. It does not figure out sharing data to other bundles.

But you can do something about it. All you have to do is mark the “shared” atlas as another asset bundle. In our example, the first prefab goes in bundle A, the second prefab in B. Even though the game does not ask directly for it, we set the atlas to be in new bundle C. Unity will then properly take over, making sure to load only one copy of the atlas, and only unload it after both prefab bundles are unloaded.

The more I think about it, the more it feels like there should be some automatic way of dealing with this resource sharing. Both in new projects and legacy ones that we are upgrading. However, this is something Unity does not provide. Why not? It’s easy enough to determine what assets are shared and used by what other assets. The catch is, what’s the most efficient way to assign which asset to which bundle? That will be different for every project.

That said, you can still write some tools that help in the process. For example, something that examines your bundle assignments and works out what should be shared. It could even go so far as to suggest what should share to what.

So there you go! That is how to deal with shared asset bundle data. Join us next week as we explore a useful ability that asset bundles make super simple: automatic texture scaling.

Upgrading to Unity3d 5 Asset Bundles

This article is part of a series. If you’d like to skip ahead, you can go to the master post that links to them all.
In my first installment of this asset bundle series, let’s talk about upgrading an older project to Unity 5 bundles.

On this latest project I’ve been working on with ClutchPlay Games, I was asked to help straighten out a project that was based on the pre-Unity 5 bundle system. We did a lot to help it. First we added the Android GPU class support first, making sure everything worked. But our build times were taking forever, even with code that checksummed the source assets and only building what changed. So I decided to solve it with a crazy idea: let’s move this project up to Unity 5’s new bundle system.

In Unity 5’s asset bundles, they take care of tracking all the source asset changes for you. It has many upsides: it’s really fast to make small changes. It’s super easy to add assets to any given bundle with the new UI. Building them is a single call, and it works reliably. The sample code for loading the bundles is clean and useful, took less than a day to integrate, and only a week to make it solid. If you’re starting a new project, I highly recommend adding support for this early, it will definitely make certain distribution problems very easy.

But what is the value added for projects that still use the older system? On the surface they look completely incompatible. Assets were always added from code, which is perfect for build automation, very wise in large projects. You don’t want to manually set every asset’s bundle identifier through the UI. That alone would simply take too long. Then you notice the new BuildAssetBundles call that takes the AssetBundleBuild objects. Surely this is how.

But lo! ‘Tis not true. It’s simply not well documented. You can have the best of both worlds. Simply replace your old calls to BuildPipeline.BuildAssetBundle() with a method that changes the asset bundle assignments for each asset that you would pass to BuildAssetBundle. How, you say? Simple: it’s now part of the AssetImporter interface.

AssetDatabase.StartAssetEditing();

for ( string assetPath in desiredAssets ) {
	var importer = AssetImporter.GetAtPath(assetPath);
	// note that you must set the bundle name before the variant.
	// If you want to clear the bundle name, set it to null - it will clear variant for you.
	if (importer.assetBundleName != bundleName)
		importer.assetBundleName = bundleName;
	if (importer.assetBundleVariant != variant && bundleName != null)
		importer.assetBundleVariant = variant;
}

// this might be a little extreme but we've had issues with old versions being used in builds
AssetDatabase.StopAssetEditing();
AssetDatabase.SaveAssets();
EditorApplication.SaveAssets();
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);

At that point, everything will be assigned to the bundle names you want. As a separate step you then actually build the bundles through the simplified BuildPipline.BuildAssetBundles(). As a bonus, you can then examine and tweak the bundle assignments through the Editor UI!
Not all is wine and roses, however. There are a few not-so-obvious rules with the names you use for the bundles.

  • Periods confuse the loading system for some reason. The asset extension is ignored during loading, but extra periods in the asset filename or the bundle names confuse things.
  • Underscores have special meaning in bundle names. It’s safest to avoid using them. Likewise, be sure not to use them in variant names.
  • Bundle names will always be stored lowercase. This prevents case confusion, and is not a bug.
  • Forward slashes create a “group” in the bundle name selection popup. This is highly useful. Be careful not to accidentally put them right together, like “textures//thing2”. It breaks the menu.

There we are! With the new bundle system in place, our build times are down to a few minutes instead of an hour. With this new design, you can do a pass with the bundle assignments, and then edit a specific asset many times, doing only the bundle update over and over again. Because Unity tracks it for you, each bundle update takes only seconds.

There is one more major piece to upgrading an old project. Join us next week as we work out how to handle shared assets!