Search Unity

Advice on sprite variants/ runtime replacement

Discussion in 'Addressables' started by CDF, Apr 21, 2020.

  1. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,313
    So I'm looking for some advice on how to deal with this situation:

    1. 2D Game that has many levels, each level is a prefab that is addressable.
    2. Levels contain nested prefabs of many different types of objects
    3. These objects contain sprites, animations, colliders etc.

    I want the ability to change the look/skin of the game at runtime based on unlockable themes.

    I need the ability to edit these levels in Editor and see what I'm doing (can't have invisible sprites)
    Ideally only the sprites that belong to a specific theme are loaded.

    How would you achieve such a thing with Addressables?
    Is it possible to put all sprites/atlas contained in a theme inside a single group, load this group first then load the level and have Addressables automatically remap the sprites?

    Appreciate any help and guidance anyone can offer :)
     
    Last edited: Apr 27, 2020
  2. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    586
    I'm actually doing a similar thing right now with versions. What you can do is point to the address of the sprite you want loaded, then filter on its label. To set it up, you can have multiple sprites share the same address, but have different labels.

    Code (CSharp):
    1. Addressables.LoadAssetsAsync<T>(new string[] { address, label }, null, Addressables.MergeMode.Intersection)
    Unfortunately, this means using a string as a reference instead of using an AssetReference, so bugs are more likely to occur.
     
  3. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,313
    ok, so your sprite prefabs with Sprite Renderers don't actually reference a sprite?
    You have some custom component that has a string reference and dynamically load that sprite in at runtime?

    How do you go about editing levels without seeing the sprites on the Sprite Renderer, if the above is true?
     
  4. ProtoTerminator

    ProtoTerminator

    Joined:
    Nov 19, 2013
    Posts:
    586
    Well that part is up to you. You could have the sprites already loaded when you do your edits and leave them serialized then just overwrite them with the custom loader. Or you could dynamically load them in edit mode with an editor script (and maybe a function to delete them when you're finished editing so that you don't need to load sprites that aren't used).
     
  5. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,313
    Yeah I'm looking for a solution where Unity doesn't need to load unnecessary textures/sprites.

    I think I may have a temporary solution though...
    I can keep the source prefabs referencing a sprite in Editor so I can see assets at edit time.
    Then create different theme sprite atlases that contain all the different themed sprites. Mark these atlases as "not included in build"
    Load the desired theme atlas before loading level
    Load level. Do a search and replace on all Sprite Renderers in level and replace with those in the theme atlas. This results in all Sprite Renderers having a clone of the sprite. Which kinda sucks, but I guess that's the way Unity intended.

    From what I can tell in the memory profiler. If you don't mark an atlas as "include in build" the texture won't be included, which is what I want :) and you need to load it yourself.

    What sucks though is you can't just say "Hey Unity, use this Sprite Atlas texture instead of the other one, I promise all the sprites are in the same location"

    Here's a test script I came up with:

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using UnityEngine;
    4. using UnityEngine.AddressableAssets;
    5. using UnityEngine.ResourceManagement.AsyncOperations;
    6. using UnityEngine.U2D;
    7.  
    8. public class AddressableLoader : MonoBehaviour {
    9.  
    10.     #region Properties
    11.     #endregion
    12.  
    13.     #region Fields
    14.  
    15.     [SerializeField] private AssetReferenceAtlas themeAtlasAsset = default;
    16.     [SerializeField] private AssetReferenceGameObject levelPrefab = default;
    17.  
    18.     [System.NonSerialized] private SpriteAtlas themeAtlas = default;
    19.  
    20.     #endregion
    21.  
    22.     #region Unity Methods
    23.  
    24.     private void Start() {
    25.  
    26.         StartCoroutine(Load());
    27.     }
    28.  
    29.     #endregion
    30.  
    31.     #region Public Methods
    32.     #endregion
    33.  
    34.     #region Private Methods
    35.  
    36.     private IEnumerator Load() {
    37.  
    38.         //listen for atlas requests
    39.        
    40.         SpriteAtlasManager.atlasRequested += OnSpriteAtlasRequest;
    41.  
    42.         //load theme atlas
    43.  
    44.         if (themeAtlasAsset.RuntimeKeyIsValid()) {
    45.  
    46.             var themeHandle = themeAtlasAsset.LoadAssetAsync();
    47.  
    48.             yield return themeHandle;
    49.  
    50.             //check theme loaded successfully
    51.  
    52.             if (themeHandle.Status == AsyncOperationStatus.Succeeded) {
    53.  
    54.                 themeAtlas = themeHandle.Result;
    55.  
    56.                 Debug.Log("Theme Loaded: " + themeAtlas);
    57.             }
    58.         }
    59.  
    60.         //load level
    61.  
    62.         var levelHandle = levelPrefab.InstantiateAsync(transform);
    63.  
    64.         yield return levelHandle;
    65.  
    66.         //check level loaded successfully
    67.  
    68.         if (levelHandle.Status == AsyncOperationStatus.Succeeded) {
    69.  
    70.             Debug.Log("Level Loaded: " + levelHandle.Result);
    71.  
    72.             if (themeAtlas) {
    73.  
    74.                 //replace all the sprites in the level with those in the loaded theme atlas
    75.                 //this kinda sucks :(
    76.  
    77.                 ReplaceSprites(levelHandle.Result, themeAtlas);
    78.             }
    79.         }
    80.     }
    81.  
    82.     private void ReplaceSprites(GameObject parent, SpriteAtlas atlas) {
    83.  
    84.         SpriteRenderer[] renderers = parent.GetComponentsInChildren<SpriteRenderer>(true);
    85.  
    86.         foreach (SpriteRenderer renderer in renderers) {
    87.  
    88.             if (renderer.sprite) {
    89.  
    90.                 renderer.sprite = atlas.GetSprite(renderer.sprite.name);
    91.             }
    92.         }
    93.     }
    94.  
    95.     private void OnSpriteAtlasRequest(string name, Action<SpriteAtlas> callback) {
    96.  
    97.         //atlas was requested, if we have a theme atlas then supply the callback with it
    98.         //unfortunatley this doesn't mean Unity will just use this Atlas if the sprite requesting doesn't belong on it
    99.         //in that case, we need to manually replace the sprite
    100.  
    101.         if (themeAtlasAsset.Asset is SpriteAtlas atlas) {
    102.  
    103.             callback(atlas);
    104.         }
    105.     }
    106.  
    107.     #endregion
    108.  
    109.     [System.Serializable]
    110.     private class AssetReferenceAtlas : AssetReferenceT<SpriteAtlas> {
    111.        
    112.         public AssetReferenceAtlas(string guid) : base(guid) {
    113.         }
    114.     }
    115. }
    116.  
    That kinda covers sprite renderers.
    Now to figure out how I would replace AnimationClips, SpriteShapes etc. in a similar manner.
     
  6. CDF

    CDF

    Joined:
    Sep 14, 2013
    Posts:
    1,313
    One other idea I had, which is quite unmanageable. Is to generate prefab variants of all levels for every theme at edit time. This would solve the issue of having to replace sprite renderers, sprite shapes, animations at runtime.
    So instead of loading a base level, you load the level variant that contains all the sprites for a given theme.

    I could write an editor script to bulk create all this, but I feel like the size of the app will increase significantly with all these duplicated prefab variants. Not to mention waiting a year for the "save prefab" progress bar to complete.

    Before that I might try your solution for a runtime addressable component that can temporarily load in its asset during edit time without serializing the data :eek:. But this seems like a monumental amount of work. Not to mention supporting other assets like Animators, Sprite Shape, Tilemaps etc.