Real-Time Pixels

All those pixels will be lost in time... like tears in rain...

导航

AssetBundle依赖关系

Unity AssetBundle Dependencies

In the last few weeks I’ve spent quite a lot of time with Unity’s Asset Bundle system. Understanding how dependencies were tracked. What determines GC cleanup of assets and understanding why the editor profiler produces particular results has been a bit of a struggle. I’m lucky enough to work for a group that allows me to have access to the Unity source code however so this has probably been slightly less painful than it has been for others going down this same path.

First, I’d like to acknowledge what appears to be the most useful piece of Unity Answers information that I’ve come across, located here. This seems to explain the general mechanics of how to include dependencies using the push and then pop method in an editor script.

For my purposes, I created a test project that contained several prefabs all containing simple GUI elements. These elements all used the same fonts and referenced the same textures. This was to verify that, in the editor profiler and in instruments, assets were in fact being shared.

I’ve included my example editor script below to demonstrate some of the methods I used for packing the bundles. In my case, I am traversing all assets in my prefab folder and treating elements with the word “Global” in their name as shared. If I were to have many dependency bundles instead of the one that I have in this example I would have to sort the order in which I am packing these bundles.

using UnityEngine;
using UnityEditor;

using System.IO;
using System.Collections.Generic;

public class AssetBundleBuild : MonoBehaviour
{
    [MenuItem("AssetBundle/Build Bundles")]
    private static void BuildBundles()
    {
        DirectoryInfo directory = new DirectoryInfo(Application.dataPath + "/Prefabs");
        FileInfo[] files = directory.GetFiles("*.prefab", SearchOption.AllDirectories);

        bool shouldPush;

        BuildPipeline.PushAssetDependencies();
        foreach (FileInfo f in files)
        {
            shouldPush = !f.Name.Contains("Global");
            if (shouldPush)
            {
                BuildPipeline.PushAssetDependencies();
            }

            // Get each asset path
            string path = Application.dataPath + "/Bundles/" + f.Name.Substring(0, f.Name.LastIndexOf(".")) + ".bundle";
            string assetPath = f.FullName.Substring(f.FullName.IndexOf("Assets", f.FullName.Length - f.FullName.IndexOf("Assets")));

            Object asset = AssetDatabase.LoadMainAssetAtPath(assetPath);
            //Debug.Log("Asset to pack " + asset + " , " + asset.name);

            BuildAssetBundleOptions options =
                BuildAssetBundleOptions.DisableWriteTypeTree |
                BuildAssetBundleOptions.CollectDependencies |
                BuildAssetBundleOptions.CompleteAssets |
                BuildAssetBundleOptions.DeterministicAssetBundle;

            if (!shouldPush)
            {
                Object[] d = EditorUtility.CollectDependencies(new Object[] { asset });

                List<Object> dSource = new List<Object>();
                List<string> dNames = new List<string>();

                // In this case I'm attempting to manually collect dependencies for tracking purposes
                // however this does not always seem to be necessary unless you have complex prefab heirarchies
                foreach (Object o in d)
                {
                    if (o != null && !dSource.Contains(o))
                    {
                        Debug.Log(" -- d " + o + " , " + o.name + " , " + o.GetType());

                        dSource.Add(o);
                        dNames.Add(o.name);
                    }
                }

                Debug.Log("::BUILDING DEPENDENCY BUNDLE:: " + asset.name + " , " + dSource.Count);
                BuildPipeline.BuildAssetBundleExplicitAssetNames(
                    dSource.ToArray(),
                    dNames.ToArray(),
                    path,
                    options,
                    EditorUserBuildSettings.activeBuildTarget);
            }
            else
            {
                Debug.Log("::NON DEPENDENCY:: " + asset.name);
                BuildPipeline.BuildAssetBundleExplicitAssetNames(
                    new Object[] { asset },
                    new string[] { asset.name },
                    path,
                    options,
                    EditorUserBuildSettings.activeBuildTarget);

                if (shouldPush)
                {
                    BuildPipeline.PopAssetDependencies();
                }
            }
        }

        BuildPipeline.PopAssetDependencies();

        Debug.Log("[AssetBundleBuild] Complete.");
    }
}

 

Now, from the standpoint of packing shared dependencies, this seemed to work with most asset types such as textures or prefabs but when I included fonts, while they wouldn’t be duplicated across multiple bundles, I would always see two copies of my fonts. One set would be flagged as being ‘used by scripts and native code’ and the other would only be flagged as ‘used by native code’. After performing numerous tests, I discovered that upon opening the dependency bundle containing the font I was interested in, if there was a copy of the font in the project directory as well, both versions would be loaded into Resources. I haven’t been able to verify whether this happens on device as well but my inclination is that it only occurs in the editor.

The Problem of the Decompression Buffer

Another problem that became visible when working with large quantities of bundles that (based off of conversations with Unity technical representatives and the 4.2.0b3 beta client) may have changed in such a way as to render the problem less dire is that Unity, as of 4.1.5, allocates an 8mb decompression buffer per each asset bundle being opened. If there is an attempt to parallelize these requests, each request will allocate it’s own decompression buffer. This can be rather disconcerting as one can see how the allocation amount (especially on restricted memory devices) can really get a guy down.

Although 4.2.0b3 seems to be reducing the decompression buffer size down to 0.5mb per bundle the problem of parallelization still persists. The only immediate solution for individuals loading any quantity of bundles seems to be amortizing the requests in such a way as to prevent too much overlap. If someone out there has a suggestion to mitigate this problem otherwise please drop me a line mcelroy.jon[at]gmail.com

 

Please explain BuildPipeline.PushAssetDependencies

I've read the docs on BuildPipeline.PushAssetDependencies and gone through the example code several times and it just doesn't make sense to me. Can someone explain it in more detail and clearer language?

For instance, when you call PushAssetDependencies, what gets pushed, and onto where?

In the example code athttp://unity3d.com/support/documentation/ScriptReference/BuildPipeline.PushAssetDependencies.html what does that first call to PushAssetDependencies do? Does it cause Lerpz.unity3d, explosive.unity3d, and AdditiveScene.unity3d to depend on shared.unity3d? (i.e. if you didn't call PushAssetDependencies then lerpzuv.tif would get duplicated into each asset bundle?)

If so, why is every build after that preceded by a push and followed by a pop? I read the comment that says, "By pushing and popping around the asset bundle, this file will share resources but later asset bundles will not share assets in this resource" but I don't understand that sentence. This file will share which resources? Does share mean "make available" or "take from somewhere else"? How does the push and pop accomplish that?

Are there some unnecessary calls to push and pop in the example?

 
more ▼

asked Mar 30 '11 at 05:05 PM

duggulous gravatar image

duggulous 
148  3  4  11

 

 

 

I'll give a shot at answering this based on my own understanding, though please chime in if I have this wrong in some way...

Push and PopAssetDependencies let you control which asset bundles are dependent on another (ie which assets can be excluded). When you call PushAssetDependencies, it instructs BuildPipeline to exclude assets in the next bundle it builds that were already included in the previous one. Dependent asset bundles have to be built in order and then loaded in order. For PushAssetDependencies to work you have to build all of your asset bundles in one script as there's no way (as far as I know) to build an asset bundle with dependencies to a pre-existing bundle.

Here's an example. Suppose you want to create 2 bundles (Bundle1 and Bundle2) and you want Bundle2 to reference assets in Bundle1, instead of including them twice separately. For the sake of visualization, let's say that there's a huge texture file "humungous.png" used by both bundles, but you really only need to have it included in Bundle1. Here's how you would build the bundles with an editor script:

var mode : int = BuildAssetBundleOptions.CollectDependencies |
        BuildAssetBundleOptions.CompleteAssets |
        BuildAssetBundleOptions.DeterministicAssetBundle;
var path : String = Application.dataPath+"/Bundle1.assetbundle";

BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Prefabs/myObject1.prefab"), null, path, mode, BuildTarget.iPhone);


path = Application.dataPath+"/Bundle2.assetbundle";
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Prefabs/myObject2.prefab"), null, path, mode, BuildTarget.iPhone);
BuildPipeline.PopAssetDependencies();

BuildPipeline.PopAssetDependencies();

Because PushAssetDependencies was called before building Bundle2, the "humungous.png" file would be in Bundle1, and Bundle2 would only store a reference to it.

Even if you don't need to redistribute Bundle1, it needs to be rebuilt in the script to correctly prepare Bundle2's dependencies. Each call to PushAssetDependencies tells the BuildPipeline to exclude the shared dependencies of the previous asset bundle in the one created next. While PopAssetDependencies removes the exclusion. The Push and Pop commands are hierarchical, meaning you can nest dependencies. For example, consider if you had a complex project with a bundle for base assets, then other sets of bundles of shared assets for specific environments. Using Push and Pop you can create 'layers' of dependencies.

The use of DeterministicAssetBundle means that each time asset bundles are created, the same internal IDs for objects will be used. This ensures that when you build bundles, the assets are referenced the same way every time. If you don't use DeterministicAssetBundle, then there's a risk that there will be conflicts with new bundles and previously distributed bundles.

Hopefully that helps some.

more ▼

answered May 10 '11 at 02:31 AM

Steven Walker gravatar image

Steven Walker 
1.1k  30  34  50

 

We have prepared a tutorial about building asset bundles with our uTomate build tool. This tutorial also covers the asset bundle dependency stack and how Pushing and Popping from it affect what ends up in your asset bundles, so it's probably going to help you understand the PushAssetDependencies and PopAssetDependencies commands even if you don't use uTomate for building:http://docs.ancientlightstudios.com/display/UT/How+to+build+Asset+Bundles+with+uTomate

more ▼

answered Apr 19 '13 at 07:03 AM

kork gravatar image

kork 
101  1  2

 

How to build asset bundles with uTomate

What are Asset Bundles and why would I want to use them?

The simplest way to answer this question is having a look at the Unity manual.

AssetBundles are a collection of assets, packaged for loading at runtime. With Asset Bundles, you can dynamically load and unload new content into your application. AssetBundles can be used to implement post-release DLC. They can also be used to reduce the amount of space on disk used by your game, when first deployed. It can also be used to add new content to an already published game.

— Unity Manual

Asset Dependencies - The Theory

When you build asset bundles you usually want them to be of minimal size. Therefore you don’t want to include all dependencies of a scene or an asset into each asset bundle, as this will duplicate all the assets and will unnecessarily add to the size of your bundles.

Unity provides a concept called Dependency Stack for this. This concept is not necessarily intuitive (see the Unity manual for the gory details) but it allows for a great deal of flexibility. The basic idea of the dependency stack is that all bundles, which are on the same or higher stack level as previously built bundles, will share assets from previously built bundles. Now that is probably somewhat abstract so let’s use an example for this.

Let’s assume you got a level called "Level1" from which you can access two versions of another level, called "Level 2a" and "Level 2b". Let’s also assume that in Level 1 you need a robot mesh and textures and Levels 2a and 2b require the same robot mesh and texture but additionally require a car model and textures. Now we want to build asset bundles for Levels 1, 2a and 2b. If you simply build asset bundles without using the dependency stack, each bundle will contain all the dependencies of the level, that is, you get three bundles looking like this:

Asset bundle without dependency management

This is very inefficient because it includes the robot model and textures into Levels 2a and 2b even though it is already known since Level 1 has been loaded before Levels 2a and 2b. To change this, you can use the dependency stack. You enable the dependency stack by callingBuildPipeline.PushAssetDependencies before building a bundle. This will create a new level on the dependency stack.

Now each bundle that is built will share it’s dependencies with previously built bundles that are on the same or a lower stack level. Let’s see how the dependency makes the bundles smaller:

Asset bundle with simple dependency management

As you can see, the Level 2a bundle now no longer contains the robot model and textures but shares them with the Level 1 bundle. Unity will do no magic for you though, so you are responsible for loading these bundles in the correct order in your application. Level 2b get’s even smaller as it shares the car model and textures with Level 2a (as 2a got built before 2b on the same stack level).

Now while this yielded some nice savings in bundle space it isn’t exactly what we need. Using this structure makes Level 2b depend on Level 2a. So you will always have to load Level 2a before you can switch to Level 2b otherwise the car model and textures will be missing. Since you can go to either Level 2a or 2b from Level 1 this setup is not useable for our example.

Basically we need the car model and texture in both bundles, the one for Level 2a and the one for Level 2b. To do this, you can use BuildPipeline.PushAssetDependencies once before you build the bundle for Level 2a. This will move this onto a higher dependency stack level. It will still share resources from the Level 1 bundle (as this is on a lower stack level). After you have built the bundle for Level 2a, you call BuildPipeline.PopAssetDependencies. This will remove the higher dependency stack level and therefore future bundles will not share resources with the Level 2a bundle (remember, bundles only share dependencies with bundles on the same or a lower stack level).

Asset bundle with advanced dependency management

This approach works for streamed scene asset bundles as well as for simple asset bundles and players.

How to do it with uTomate

Usually when you want to build asset bundles there is no way around scripting this. Unity provides a simple UI for building asset bundles which lets you select a few files and build an asset bundle from the selection. But this is not sufficient in two ways. First, if you happen to forget to select a certain file that should be in your asset bundle you will notice that only when you test your game online - which less than optimal. Second, the Unity UI does not support modifying the dependency stack, so building asset bundles that depend on each other is not possible using the UI. So scripting this is the way to go (and even the Unity documentation recommends that you build a function for creating your asset bundles).

With uTomate you can do all of this without scripting with the familiar Unity inspector UI. uTomate provides two actions that can interact with asset bundles - the obvious link:/utomate/documentation/actiondocs/build_asset_bundle .html[Build Asset Bundle] action and the Build Player action. Both actions provide options to modify the dependency stack:

Options for modifying the dependency stack

Basically if you tick Push Dependencies uTomate will put a new level on the dependency stack (by calling PushAssetDependencies). When you tick Pop Dependencies uTomate will drop a level from the dependency stack (by calling PopAssetDependencies). The Pop All Dependencies flag will clear the dependency stack.

So to build the asset bundles from the example, we need three actions that build the asset bundle for the Levels 1, 2a and 2b. Let’s start with the action for Level1:

Building Level 1

Its building an asset bundle for the full Level1 scene and all it’s dependencies. To make sure that the dependency stack is opened, we have ticked the Push Dependencies flag. Now lets continue with the bundle for Level2a:

Building Level 2a

To make sure that everything that is included in this bundle will not be skipped by the Level2bbundle (since we want both bundles to be independent of each other) we open a new level on the dependency stack by ticking Push Dependencies. We also tick Pop Dependencies, to make sure that new level on the dependency stack is cleared once we finish building the bundle. Apart from this, the setup is basically the same as for Level1, except that we use a different scene here. Finally the action for Level2b:

Building Level 2b

Here we tick Pop Dependencies to clean up the dependency stack. This is just good practice to ensure that other actions that might come afterwards don’t accidentally reference to asset bundles to which they should not reference. The rest of the setup is the same as for Level2a, except again we use a different scene here.

Now that our actions are set up, all we need to do is putting them into an automation plan:

The final automation plan
I have added a few notes and arranged the actions to visualize the dependency stack levels. This is just a visual help to show how the plan is supposed to work, it is not required to format it this way to make it work. However having a good documentation on your automation plans makes it easier for everyone on the team to understand how it’s supposed to work (see also our article on building robust automation plans).

Now with the plan set up, all you need to do is to run the plan and have your asset bundles built. Since you automated this you will get repeatable, consistent results as opposed to the error-prone manual building of asset bundles. Automating this with uTomate instead of scripting also provides a few bonuses:

  • You don’t need any scripting experience to do it, so every team member can understand and modify the building process.

  • The automation plan provides a nice visual documentation on what asset bundles you build and how they depend on each other.

If you would like to give us feedback on this tutorial shoot us a mail. We’d also like to hear from you if you would like to see a tutorial about a special topic here.


posted on 2014-04-08 15:26  yaol  阅读(6282)  评论(0编辑  收藏  举报