SpriteAtlas + AssetBundle unable to use sprites

I am attempting to load a prefabs (Projectile.prefab) from another AssetBundle (gun.unity3d). All the sprites and SpriteAtlas that are being used by the Projectile prefab are in another AssetBundle (vfx_projectiles.unity3d).

The problem I am having is that the sprite is never shown on the prefab. This happens with other SpriteAtlas I am using in the project. If I delete the SpriteAtlas from the project and AssetBundle, I can see the Sprite on the prefab(s). I am using Unity 2017.1.0p5

When I load the projectile, the sprite is missing. But the image is assigned:

And I get this error when I click on the sprite via the SpriteRenderer:

Also I get another error when I load the Unity scene where the assetbundle and prefab are needed:

In the Editor,
All the sprites are tagged with the Atlas name i.e (Projectiles)

I also unchecked including the SpriteAtlas in a build, therefore; I should be loading it through an AssetBundle and/or resources

Then I wait for my services to load, and attempt to load the atlases. This is during boot/load time of the application:

    /// <summary>
    /// Loads sprite atlases from assets bundles
    /// </summary>
    public sealed class AtlasLoader : SBMBehaviour, IAtlasLoader
    {
        #region Members
        /// <summary>
        /// Reference to the resource service
        /// </summary>
        private IResourceService _resourceService;

        /// <summary>
        /// Reference to the asset bundle service
        /// </summary>
        private IAssetBundleService _assetBundleService;

        /// <summary>
        /// Collection of the sprite atlases and their callbacks to load
        /// </summary>
        private Dictionary<string, Action<SpriteAtlas>> _spriteAtlasToLoad;
        #endregion

        #region Injection
        /// <summary>
        /// Inject the resource service
        /// </summary>
        [Inject]
        public IResourceService ResourceService { get { return _resourceService; } set { _resourceService = value; } }

        /// <summary>
        /// Inject the asset bundle service
        /// </summary>
        [Inject]
        public IAssetBundleService AssetBundleService { get { return _assetBundleService; } set { _assetBundleService = value; } }
        #endregion

        #region Life Cycle
        /// <summary>
        /// Allocate members and register for event
        /// </summary>
        protected override void Awake()
        {
            base.Awake();

            _spriteAtlasToLoad = new Dictionary<string, Action<SpriteAtlas>>();

            SpriteAtlasManager.atlasRequested += OnSpriteAtlasRequested;
        }

        /// <summary>
        /// Initialize members
        /// </summary>
        public void Initialize()
        {
            if (_resourceService == null)
                LogError("Resource service not initialized!");
        }

        /// <summary>
        /// Load SpriteAtlases if requested
        /// </summary>
        private void Update()
        {
            if (_spriteAtlasToLoad != null && _spriteAtlasToLoad.Count > 0 && _resourceService != null && _assetBundleService != null && _assetBundleService.Loaded) {
                foreach (KeyValuePair<string, Action<SpriteAtlas>> pair in _spriteAtlasToLoad) {
                    string tag = pair.Key;
                    Action<SpriteAtlas> onAtlasLoaded = pair.Value;
                    string assetbundle = AssetBundleUtility.GetAssetBundleName(AssetBundleType.vfx, tag);

                    Log("Attempting to load atlas: {0} in AB: {1}", tag, assetbundle);

                    _resourceService.GetAsset<SpriteAtlas>(assetbundle, tag, (spriteAtlas) => {
                        onAtlasLoaded(spriteAtlas);
                    });
                }

                _spriteAtlasToLoad.Clear();
            }
        }

        /// <summary>
        /// Unregister for the event
        /// </summary>
        private void OnDestroy()
        {
            if (_spriteAtlasToLoad != null && _spriteAtlasToLoad.Count > 0)
                _spriteAtlasToLoad.Clear();

            SpriteAtlasManager.atlasRequested -= OnSpriteAtlasRequested;
        }
        #endregion

        #region Event Handling
        /// <summary>
        /// Handle the event when an atlas is requested
        /// </summary>
        /// <param name="tag">tag name of the sprite atlas</param>
        /// <param name="onAtlasLoaded">callback after atlas has been loaded</param>
        private void OnSpriteAtlasRequested(string tag, Action<SpriteAtlas> onAtlasLoaded)
        {
            Log("Loading Sprite atlas! SpriteAtlas: {0}", tag);

            if (_spriteAtlasToLoad != null && !_spriteAtlasToLoad.ContainsKey(tag))
                _spriteAtlasToLoad.Add(tag, onAtlasLoaded);
        }
        #endregion
    }

After doing all this, I tried to get the same sprite from the loaded Atlas via SpriteAtlas.GetSprite. It came back as a valid sprite and the same NullReferenceException occurred.

Bump.

I am experiencing what seems like the same problem. I am attempting to load a scene that exists in AssetBundle1 which contains references to sprites that I have atlased in AssetBundle2. When the scene loads, the sprites don’t appear. I tried some similar debugging as you and have gotten things to show up correctly by doing a couple different things…

  • Deleting the atlas altogether causes the sprites to appear correctly. So if there’s no atlasing happening, it works.
  • Explicitly loading the sprite atlas by calling AssetBundle.LoadAssetAsync before I load the scene causes the sprites to appear correctly.

Seems like Unity isn’t handling the packed sprites correctly across asset bundles.

Edit: I am using 2017.1.1p1

Same here, would love to know if Unity is aware of the issue, and if there’s a fix or workaround.

For now I’ve switched back to the legacy Sprite packing which seems to handle bundles correctly. I created a bug report for it with a stripped down project, I will post if I get any response from that.

1 Like

Having same issue, prefabs from bundle one don’t have valid references to atlassed sprites in bundle 2

Plz fix!!

Hello!

@hectorSBM . It will be really helpful if you can send me your project (or if it’s too big, a stripped down version which able to reproduce the issue). I created a simple project that I hope resembles your setup and it works for me, so, I really need to look into your project. I might be missing something. I attached my project, you can have a look and see if it works for you.

Here are the key points to note it works for me.

  • Load Atlas bundle first then load prefab bundle. This will allow auto-relink (if atlas is marked as ā€œInclude in buildā€)
  • If atlas has unchecked ā€œInclude in buildā€ then you must install the callback. Or use ā€œSpriteAtlas.GetSpriteā€

Both examples are shown the sample project. If you can’t send your project, please detail your order of execution.

For asset bundle, it’s better to test it with standalone player. In Editor, it can be misleading. This error is cause by Unity trying to locate the asset in the project (where the asset is loaded from the asset bundle).

@cjuillard Your ā€œdifferent thingā€ no.2 is actually the solution for this kind of Asset Bundle dependency issue. Sprite depends on the Sprite Atlas if it is packed into one. So it need SpriteAtlas to be available before loading the Sprite.

Asset bundle does not load for you any of its dependencies. You must do loading yourself somehow. See details here.

That’s why it works when you load the atlas bundle first.

3229418–247803–Bundle_bunlde.zip (178 KB)

Thanks for the reply! From the link you referenced it says that both bundles need to be loaded before loading the material. In your example that means both bundles must be loaded and then you should be able to instantiate the prefab. It says nothing about needing to explicitly load the dependent texture. So carrying that same logic to your example you should not need to explicitly load the sprite atlas dependency. Is there something wrong with that logic? In 3.5.2 of this documentation it seems to back up that idea - https://unity3d.com/learn/tutorials/topics/best-practices/assetbundle-fundamentals

From your example project…

// Pre load the asset bundle with sprite atlas
var atloadOp = new WWW("file://" + Application.streamingAssetsPath + "/" + saBundle);
yield return atloadOp;

// I would think based on the documentation this should be unnecessary, right!? In my test
// it was necessary. If you comment it out it actually works in the player, but if you make a
// build it won't load properly.
atloadOp.assetBundle.LoadAsset<SpriteAtlas>("sa");

// Then load the prefab
var loadOp = new WWW("file://" + Application.streamingAssetsPath + "/" + prefabBundle);
yield return loadOp;

var prepre = loadOp.assetBundle.LoadAsset<GameObject>("prop_crate_ammo");
GameObject.Instantiate(prepre);

@Kuan Thanks for the reply, I was taking a look at your project, and IT WORKED! Event thought the project you created was in 2017.3.0a6… the same concept worked, by loading the sprite atlas first. Kuan, If you are Unite Austin, I owe you your favorite drink!

I feel for a feature, that you shouldn’t have to load the SA if it already a dependency to another AssetBundle

anyway, here are my changes:

        #region Sprite Atlas
        /// <summary>
        /// Load the sprite atlas from an asset bundle
        /// </summary>
        /// <param name="tag">asset bundle to load</param>
        /// <param name="onAtlasLoaded">callback when the asset is loaded</param>
        private IEnumerator LoadAtlas(string tag, Action<SpriteAtlas> onAtlasLoaded)
        {
            if (_assetBundleService != null) {
                while (!_assetBundleService.Loaded)
                    yield return null;

                var operation = _assetBundleService.GetAssetAsync<SpriteAtlas>(AssetBundleUtility.GetAssetBundleName(AssetBundleType.vfx, string.Empty), tag);

                if (operation != null) {
                    yield return operation;

                    onAtlasLoaded(operation.Asset);
                }
                else
                    LogError("Unable to get assetbundle for sprite atlas! Tag: {0}", tag);
            }
        }
        #endregion

        #region Event Handling
        /// <summary>
        /// Handle the event when an atlas is requested
        /// </summary>
        /// <param name="tag">tag name of the sprite atlas</param>
        /// <param name="onAtlasLoaded">callback after atlas has been loaded</param>
        private void OnSpriteAtlasRequested(string tag, Action<SpriteAtlas> onAtlasLoaded)
        {
            Log("Loading Sprite atlas! SpriteAtlas: {0}", tag);

            StartCoroutine(LoadAtlas(tag, onAtlasLoaded));
        }
        #endregion

About that bug, when are Unity planning to fix it?
It is painful not to be able to check with Editor.

1 Like

Hello. It’s been 2 years. Are there any workarounds ?
I really want to be able to test the game in Editor instead of making a build every time. It’s critical for me because WebGL takes 4 minutes to build.

Came up with this, but I wish there was a proper solution.

using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Linq;

[InitializeOnLoad]
public static class AtlasBugWorkaround
{
    static AtlasBugWorkaround()
    {
        EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    }

    private static List<string> _spriteAtlasAssets;

    private static void OnPlayModeStateChanged(PlayModeStateChange state)
    {
        if (state == PlayModeStateChange.EnteredPlayMode)
            HideAtlasAssets();

        if (state == PlayModeStateChange.ExitingPlayMode)
            UnhideAtlasAssets();
    }

    private static void HideAtlasAssets()
    {
        _spriteAtlasAssets = AssetDatabase.FindAssets("t:spriteatlas").Select(AssetDatabase.GUIDToAssetPath).ToList();

        foreach (var spriteAtlasAsset in _spriteAtlasAssets)
        {
            var fileName = Path.GetFileName(spriteAtlasAsset);
            var directoryName = Path.GetDirectoryName(spriteAtlasAsset);
           
            FileUtil.MoveFileOrDirectory(spriteAtlasAsset, directoryName + "/." + fileName);
            FileUtil.MoveFileOrDirectory(spriteAtlasAsset + ".meta", directoryName + "/." + fileName + ".meta");
        }

        AssetDatabase.Refresh();
    }

    private static void UnhideAtlasAssets()
    {
        foreach (var spriteAtlasAsset in _spriteAtlasAssets)
        {
            var fileName = Path.GetFileName(spriteAtlasAsset);
            var directoryName = Path.GetDirectoryName(spriteAtlasAsset);

            FileUtil.MoveFileOrDirectory(directoryName + "/." + fileName, spriteAtlasAsset);
            FileUtil.MoveFileOrDirectory(directoryName + "/." + fileName + ".meta", spriteAtlasAsset + ".meta");
        }

        AssetDatabase.Refresh();
    }
}

Any solution for this for 2019.3?

4 Likes