1
\$\begingroup\$

Here are current conditions for a game I'm building in Unity 5.6.3:

  • SpritePacker is used to create atlases (it would be ideal if that didn't change, but it could, if no other options become evident).
  • Config file describing game state which specifies IDs referring to specific sprites, is loaded.
  • Resources.Load() worked fine with individual sprites, but doesn't work properly with SpritePacker - can't access elements by ID, programmatically.
  • Programatic access to SpritePacker sprites is done via an object in the scene that has inspector-set arrays of all correctly-named assets. From these, the script on that object creates a public static dictionary keyed by those IDs. This dictionary supplies sprites, by key, to rest of game.
  • Memory use and draw calls are reduced, but Android crashes about 50% of the time on startup (game killed by the OS), which seems to result from a delay caused by loading the large Atlas element in the scene... could be wrong though.

So...

How do I use SpritePacker AND get dynamic access to atlas entries? (AFAIK, it is not possible in 5.6)

OR

What other solutions can solve this without a major overhaul?

\$\endgroup\$
  • \$\begingroup\$ Do your sprites have any groupings that tend to be used together more often than others? If so, you might be able to break down your single large parent dictionary asset into several smaller ones, and asynchronously load just the ones you need for a given collection of sprites to avoid a large hitch on startup from loading everything. \$\endgroup\$ – DMGregory Aug 10 '17 at 11:04
  • \$\begingroup\$ @DMGregory I do have such groupings; specifically, the rooms that the player enters. How would I (without using separate scenes) load those mini-atlases asynchronously, and have the sprites accessible by ID? \$\endgroup\$ – Engineer Aug 10 '17 at 11:08
  • \$\begingroup\$ Why the constraint to not use extra scenes? Unity can load a scene additively and asynchronously, so it seems like that would let you pull in the indexes you want without a big hitch. You could also make your index object a ScriptableObject and load it with Resources.LoadAsync, I think. \$\endgroup\$ – DMGregory Aug 10 '17 at 11:13
0
\$\begingroup\$

Here's a rough sketch I have not yet tested, based on our comments above. I've omitted a bunch of error checking and convenience items so it still needs work to refine into a complete solution.

First, we take each grouping of sprites that are commonly used together and make a small directory asset just for them. (You could also auto-generate these assets based on packing tags of your sprites)

When we load this directory at runtime, we'll load only the atlasses it references, not every sprite atlas in the game, which can speed up loading. To keep peak memory use down, you can also unload directories you're no longer using, but I haven't shown that here.

[CreateAssetMenu]
public class SpriteDirectory : ScriptableObject {

    // Populate this in the inspector to map your sprite IDs
    // to their corresponding packed Sprite assets.
    public KeyValuePair<SpriteID, Sprite> [] idMaping;

    Dictionary<SpriteID, Sprite> _index;

    void Awake () {
        // Unpack our id mappings into a dictionary for O(1) lookups.
        _index = new Dictionary<SpriteID, Sprite>(idMaping.Length);
        foreach (var entry in idMaping)
            _index.Add(entry.Key, entry.Value);
    }

    public Sprite GetSprite(SpriteID id) {
        return _index[id];
    }
}

Next, we need a way to find the right directory for the sprite we want to load, given only its ID. (If your IDs for each group are integers in non-overlapping ranges, or if your sprite-consuming logic has knowledge of what sprite set it's working with, this can be much simpler, but here I'm assuming the most general case of random sprite access)

Here I've got a master directory that doesn't have direct links to the individual sprite directories (so it doesn't pull them all into the scene's dependency list and force all their atlases to be loaded before the game starts), instead, it just knows where to find them, and handles loading them asynchronously when needed:

public class SpriteMasterDirectory : MonoBehaviour {

    [System.Serializable]
    class DirectorySlot {
        public string path;
        public SpriteDirectory asset;
        public bool pending;
    }

    Dictionary<SpriteID, DirectorySlot> _index;

    [SerializeField]
    [HideInInspector]
    List<KeyValuePair<SpriteID, DirectorySlot>> _flattenedDictionary;

#if UNITY_EDITOR
    // Use this in the editor to link the master directory to the
    // individual directories it needs to pull from. The list will
    // be stripped from the built game so it doesn't load *everything*.
    public SpriteDirectory[] directories;

    void Reset() { OnValidate(); }
    void OnValidate() {
        // When the asset is deserialized or changed in the editor,
        // update its mapping of sprite IDs to their corresponding directories.

        _flattenedDictionary.Clear();
        foreach (var directory in directories) {
            var slot = new DirectorySlot();
            slot.path = UnityEditor.AssetDatabase.GetAssetPath(directory);
            slot.path = slot.path.Substring(
                slot.path.LastIndexOf("Resources/") + 10);
            foreach (var entry in directory.idMaping)
                _flattenedDictionary.Add(
                    new KeyValuePair<SpriteID, DirectorySlot>
                        (entry.Key, slot));
        }
    }

#endif

    private void Awake() {
        // Unpack our flattened dictionary into one we can search in O(1).
        _index = new Dictionary<SpriteID, DirectorySlot>
                     (_flattenedDictionary.Count);
        foreach (var entry in _flattenedDictionary)
            _index.Add(entry.Key, entry.Value);

        // Don't need this anymore.
        _flattenedDictionary = null;
    }

    public class SpriteRequest {
        public Sprite sprite;
        public bool isReady;
    }

    // Call this when you want to access a sprite that might not be loaded yet.
    public bool LoadSprite(SpriteID id, out SpriteRequest request) {
        request = new SpriteRequest();

        var slot = _index[id];
        if (slot.asset != null) {
            request.sprite = slot.asset.GetSprite(id);
            request.isReady = true;
        } else {
            // Currently we'll start multiple coroutines & load requests
            // if the same directory is requested multiple times before
            // it's loaded in. Hoping Resources.LoadAsync is smart enough
            // to only load it once, but more sophisticated queuing here
            // is another potential solution.
            StartCoroutine(LoadFromPendingDirectory(slot, id, request));
        }

        return request.isReady;
    }

    // Asynchronously load a sprite directory (and all its sprite atlasses).
    IEnumerator LoadFromPendingDirectory(
        DirectorySlot slot,
        SpriteID id,
        SpriteRequest request) {

        var loading = Resources.LoadAsync(slot.path);

        yield return loading;

        slot.asset = (SpriteDirectory)loading.asset;
        request.sprite = slot.asset.GetSprite(id);
        request.isReady = true;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Need the time / space to read this thoroughly and query further anywhere necessary. Thanks for getting this answer up so quickly, I posted the question so I'd have answers by next week. I'll be back. \$\endgroup\$ – Engineer Aug 10 '17 at 12:39
  • \$\begingroup\$ No rush! It's a fascinating problem so my brain got stuck on it, even though the sketch ended up being a lot longer than I'd expected. Not being able to serialize dictionaries makes for a bunch of extra noise... ;) \$\endgroup\$ – DMGregory Aug 10 '17 at 12:46
0
\$\begingroup\$

This problem was caused by having a large, in-scene dictionary of SpritePacker sprites (around 400 textures in total) in the startup scene. Initial testing show that the moment this figure was reduced to around 200 textures, the game begun loading again without hassles on an LG G3.

Since the total number of in-game sprites could not be reduced, it remained to be seen how this could be eliminated from the scene altogether.

The simplest path to a solution was to break the single, monolithic prefab down into smaller atlas prefabs which were then loaded using Resources.Load(), and Instantiate()d at the appropriate time: These smaller units were pertinent to each "location" in the game which is a series of connected "rooms", so were loaded as the player entered each location. On leaving each location, that location's sprites were then unmapped from the global static sprite dictionary, the instantiated prefab was Destroy()ed, and lastly, Resources.UnloadUnusedAssets() was called to clean up.

This left a relatively flat texture memory profile of about 400mb regardless of where in the game the player went, or when, as opposed to the over 1 gigabyte previously seen.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.