Phoenix Point

View Original

Unity References Meet Asset Bundles

As a follow-up to our last Unity References Basics post, Snapshot Games’ Filip Slavov shares some more behind-the-scenes tips for aspiring developers coding in Unity, and explains how in our game, Phoenix Point, a bug around the Technician’s mech arms animation happened!

TL;DR

  • There are 4 ways assets are packed and loaded: Scenes, Resources, Bundles, StreamingAssets

  • While Unity packs scenes during build, when a shared asset is met it will be packed in the first level in the build list that is met. All subsequent level files will just refer to the first one and load it on demand.

  • An asset packed by scenes AND bundles is actually duplicated.

  • Unity bundles AND SCENES have 4 GB (2 GB for older versions) size limitation.

  • Indirectly referenced stuff remains in memory until all references are released.

  • Comparing asset references using == or Equals WILL NOT WORK if:

    • First asset is referenced in a scene and the second one is in a bundle.

    • First asset is referenced in one bundle while the second one is in another bundle.

  • Comparing asset references WILL WORK if:

    • First asset is referenced by one scene and the second one is referenced in another scene.

    • First asset is referenced in a bundle and the second one is referenced in the same bundle.

If you wanna know more about how asset references work in Unity, read our previous article: Unity References Basics.

Who is this for: game developers enthusiasts with some basic knowledge of assets project organization.

Packing your assets

Your assets are ready, your code is working, your levels are polished! Time to build the final game and release it to the world! But how is that actually working? Will Unity just ship all assets in the project and load them on start up?

Loading everything on start up is silly as the user device will quickly run out of memory.

Packing everything in your project is also bad - often there are textures and models that are not supposed to get into the game and are not actually used, i.e. experiments and leftover junk.

How does it know what and when to pack & load?

Unity offers 4 ways of packing and managing your assets (and their dependencies):

  • Scenes (or levels)

  • “Resources” folders

  • Bundles (Addressables System)

  • “StreamingAssets” folder

Scenes

Unity has a concept for a scene or level. You put a bunch of game objects in a scene and run that scene in the editor. Your game may switch scenes when the player progresses. When your scenes are ready, you have to add them manually into the build list so they get included into the final game. Here is how our list looks at the moment with over 800 scenes:

Everything that is referenced by the scene (and their dependencies) gets packed into a single file and loaded when the scene is started in the final build. You can actually see the packed scene files for Phoenix point in the game folder: “Phoenix_Point\PhoenixPointWin64_Data\”

PRO TIP:

During build Unity searches for duplicating assets in all the scenes from the list. When found it packs them in the “sharedassets[SceneIndex].assets” file of the first scene they were discovered in. This means that the higher list index the scene has the lower its size on disk will be. First few scenes will probably have most of the game assets.

PROS:

  • Everything happens (almost) automatically. The only thing needed to manage is the build list.

  • Assets are never duplicated, so minimal disk space is used.

  • Everything referred to by the scene will be loaded in memory for you. This may be a problem for huge scenes.

CONS:

  • Having a lot of scenes slows down the build considerably as it needs to open each scene and gather its assets.

  • You can’t build “just that” scene since duplicate assets are packed in some earlier scene file and it would require repacking all dependencies. You need to make a full build for even the smallest change. This slows down iteration and testing considerably. Reordering the list is also a problem.

  • Hard-coded scene names are used for loading (scenes can’t be referenced).

  • Not practical when aiming for small update patches (e.g. mobile games).

  • You can’t prioritise which files to download first in a streaming install system (like Xbox & Playstation) since you have no idea what assets were packed where.

Resources

As mentioned before this is the old way of loading assets manually by hard-coded asset paths:

var prefab = Resources.Load("Characters/Alien/CrabmanPrefab");
var crabman = Instantiate(prefab);
crabman.Kill();

To use it your assets need to be located in any folder named “Resources”. The hard-coded paths used are relative to the “Resources” folder.

Unity build process finds all “Resources” folders and packs all assets inside.

PROS:

  • Simple to use.

  • Full control on when assets are loaded in memory.

  • Loading assets by path is useful when names are constructed dynamically. For example: $“LoadingScreen-{scene}.png”

CONS:

  • You can’t have many “Resources” bundles. The Resources bundle (any bundle actually) has a size limitation of 4GB, so there is a limit to this madness.

  • Can’t build “just Resources”. But when built, you can replace “just that” file of the old build for a quick patch. Just don’t do this when the code has changed.

  • Hard-coded asset paths. You’ll burn in hell for this.

Bundles (Addressables System)

Bundles are basically assets packed together by Unity in a file. You can manually select what assets are packed in which bundle by using Unity’s Addressables system. In-game you have to load and unload the assets when needed by code. As uncle Ben said, “With great power comes great responsibility”, so use it wisely.

Here are how our Addressable bundles look like right now:

The cool thing about the Addressable system is that the code doesn’t request the whole bundle to load for just one asset. Artists “link” their addressable asset that is needed. The code requests for the Addressables system to load just that asset unaware in which bundle it resides. Unity does some magic and supposedly loads only that asset with no additional cost. Here are some “linked” assets using the Addressables system:

PROS:

  • Full control of how assets are packed.

  • Full control on when assets are loaded in memory.

  • Assets are “linked” by kind of a weak reference (no hard-coded paths).

  • Can load assets by dynamically generated names.

  • Addressables System actually takes care of downloading the bundles and much more if you’re into such things

  • Bundles are built & packed separately from scenes and resources. So you don’t have to wait for everything just to build the bundles.

  • Updating and patching individual bundles is possible. Streaming install-friendly as well.

CONS:

  • You have to organize all the bundles yourself.

  • Artists need to register their assets as addressable and place them in the right bundle.

  • Assets packed in multiple bundles will be duplicated. There are tools to fix this by extracting them into another common bundle.

  • Old projects need to adapt their code to the new async way of doing things. When a scene is loaded it needs to wait for the required bundles to load as well before proceeding.

  • Addressable bundle info is stored in a bunch of scriptable objects which sometimes is a point of conflict when collaborating. 

  • Bundles have a limit of 4GB.

StreamingAssets

The DIY way of managing assets for hard core devs that have a lot of hair on their chests. Place your assets in the magical “Assets/StreamingAssets” and Unity will copy everything inside directly to the build final folder without any transformation. You have to manage and load those assets by yourself. Unity just gives you the means to find them with the Application.streamingAssetsPath property.

PROS:

  • Totally full control on everything.

  • Good for importing custom file formats from outside the Unity ecosystem.

  • Useful with scripting solutions like lua and javascript. Allows for runtime logic tweaking.

  • Can provide good modding support (assuming your assets are simple).

CONS:

  • Anyone can easily view, edit and steal your assets.

  • You have to load and parse the assets by yourself. Loading assets may be platform dependent.

  • Hard-coded paths.

  • Doesn’t integrate with the Unity ecosystem. You can’t have a prefab or a scriptable object.

  • You can’t blame Unity for your mistakes.

References headache

Some of you who read the last section may have spotted the problem. In case you haven’t: packing assets in different ways may duplicate them! This is a huge issue for several reasons:

  • Assets take more disk space than they should (this one is obvious)

  • Loaded assets take more memory than they should (this one is also obvious)

  • Duplicated assets are separate logical entities (this one is not obvious)

The last point means that comparison will fail for references to the same asset coming from different locations.

Example: the scene references the Crabman prefab to be spawned at specified location. The bundle with the gameplay data references the Crabman prefab so it can give it a speed bonus on spawn. When the level starts, the code spawns the Crabman in the scene and checks if it should get any bonuses. It searches through the gameplay data (most probably some list in a ScriptableObject) for Crabman match like this:

In this case the == operator will always return false since the prefab coming from the scene and prefab coming from the bundles are two separate entities in the memory. Bam! There’s your bug!

Now this isn’t that bad. If you compare Crabman prefab references located in the same scene it will work as expected. If you load several scenes additively all their Crabman references will be the same, as the asset is never duplicated. Same goes for multiple references located in the same bundle. But references are different when located in different bundles.

The Mech Arms animation bug

Here is the bug reproduction steps for the technician VVA-2 Arms (also known as Mech Arms):

  1. Recruit New Jericho technician with VVA-2 Arms mounted equipment.

  2. Enter any tactical mission with the new recruit.

  3. Move next to an enemy and use the “Electric Strike”.

  4. Watch the ability animations of the character AND mech arms.

Older players may remember that this resulted in the technician doing his animation while the mech arms stayed still the whole time. The strangest thing was that this worked fine in the editor and this didn’t make sense. After some back and forth with the Unity support it finally hit us! The references didn’t match. Here is why:

  • The Mech Arms has its own Animator that uses Animator Override Controller.

  • We override the default animation with the one provided from the ability.

  • To match the default animation, the override API asks the user to provide reference to that AnimationClip.

  • The default clip reference is stored in a ScriptableObject, referenced by the GameController that is packed in the first scene.

  • The Mech Arms animator is attached to it’s prefab. The prefab is loaded from a bundle.

In the Editor, the addressables system fakes the loading and uses the same prefab that the scenes use. In release, the animation clip is duplicated and references checks fail.

The еmbarrassing fix: compare the clip names instead of the references. This may fail if compared clips are different but have the same name. Luckily in our project all our animation clips have unique names (at least I hope they do).