Search Unity

Loading a sprite via resources.Load gives spike,.alternatives?

Discussion in 'Scripting' started by Reedex, May 20, 2019.

  1. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    I have this as " shell " for item,
    Code (CSharp):
    1. public class ItemScript : MonoBehaviour {
    2.  
    3.     public Sprite spriteNeutral;
    4.     public Sprite spriteHighlight;
    5.  
    6.     private Item item;
    7.     public Item Item {
    8.         get {    return item;    }
    9.         set {    item = value;  
    10.  
    11.             spriteHighlight = Resources.Load<Sprite> (value.SpriteHighlighted);
    12.             spriteNeutral = Resources.Load<Sprite> (value.SpriteNeutral);
    13.         }
    14.     }
    15.  
    16.     public string GetTooltip(Inventory inv)
    17.     {
    18.         return item.GetTooltip (inv);
    19.     }
    20.  
    21.     public void Use(Slot slot)
    22.     {
    23.         item.Use (slot,this);
    24.     }
    25. }
    but those sprites in Item set, are causing much performance trouble...Is there any suggestions please?
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Loading sprites is one area where it's difficult to avoid performance spikes, because the class is not thread-safe and thus they can't be offloaded, which would be the ideal solution. Best thing I can recommend is to separate the loading so that it's only loading one sprite in any given frame, using coroutines for example.
     
  3. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    so , make this threaded? right now i am trying divide folder called equipment, into helms,shields,armor etc..
     
  4. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    @StarManta sorry to bother again, could you help me how to offset it onto another thread, if it's not overcomplicated for me from your perspective of course. :- )
     
  5. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    You cant use the main unity API in a seperate thread, you cant do this in a thread.
    You can try with a coroutine, but that can only break down your functions to multiple frames on the main thread.

    You can get all the sprites ahead of time(on level load for example) and get a reference instead of calling Resources.Load.
     
  6. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    yes that's what i'm gonna do . :¨ )
     
  7. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    so i created a class
    Code (CSharp):
    1. public class Sprites : MonoBehaviour
    2. {
    3.     private static Sprites instance;
    4.     public static Sprites I {
    5.         get {
    6.             if (instance == null)
    7.             {
    8.                 instance = GameObject.FindObjectOfType<Sprites> ();
    9.             }
    10.             return instance;
    11.         }
    12.     }
    13.     void Start()
    14.     {
    15.         GetAllSprites ();
    16.     }
    17.  
    18.     public List<Sprite> AllSprites = new List<Sprite>();
    19.  
    20.     public void GetAllSprites()
    21.     {
    22.         AllSprites.AddRange(  Resources.LoadAll ("AllSprites",typeof(Sprite)).Cast<Sprite>());
    23.     }
    24.     public Sprite GetSprite(string Name)
    25.     {
    26.         return AllSprites.Find (x => x.name == Name); // ?
    27.     }
    28. }
    this in Item
    Code (CSharp):
    1. //spriteNeutral = Resources.Load<Sprite> (value.SpriteNeutral);
    2.             spriteNeutral = Sprites.I.GetSprite (item.SpriteNeutral);
    Anyway is this way of doing it the same you all meant?
     
    Last edited: May 21, 2019
  8. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    Didn't mean anything specific, but yeah that seems to be the general vibe.

    Did you test it?, how does it preform?
     
  9. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    i tested , and it seems better.
    Although, isn't this new method kinda the same as resources.load?
    that is, looking for string in a list?
     
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    I think the most reliable way to handle this will use a manager singleton (which will operate the "coroutine" - you'll see later why that's in quotes), and callbacks. Callbacks basically involve passing a function call as a parameter, and then when the job is completed, the callback is called. In this case, the callback will actually assign the loaded sprite data to where it needs to be assigned to. We're using a manager so that all sprite-loading operations will be funneled into single queue (so if a bunch of objects use this loader, it won't attempt multiple in parallel and counteract the point of this whole thing). Plus: If Unity does actually make sprite-loading thread-safe later, this method will be trivial to convert to take advantage of that!

    So as far as the usage of this goes, the goal will be code that looks like this, using your initial code as the kickoff point:
    Code (csharp):
    1.  
    2.     public Item Item {
    3.         get {    return item;    }
    4.         set {    item = value;
    5.  
    6.             SpriteLoadManager.LoadSprite( value.SpriteHighlighted, (loadedSprite) => {spriteHighlight = loadedSprite;});
    7.             SpriteLoadManager.LoadSprite( value.SpriteNeutral, (loadedSprite) => {spriteNeutral = loadedSprite;});
    8.         }
    9.     }
    Importantly, this code will not result in spriteNeutral or spriteHighlight having valid values right away, so your code can't rely on that. Now to make sense of these lines, we'll focus on the second parameter of that function call, which is:
    (loadedSprite) => {spriteNeutral = loadedSprite;}
    Yeah, that's a weird parameter. It's something called a lambda expression, and it's basically a single-purpose, one-off, and in this case single-line function whose purpose is to be called some other time. loadedSprite is the parameter for this function.

    In order for this to work, we're going to have to a) define the form of this function so the compiler can make sense of it, b) put that definition into the parameter of the LoadSprite function we're about to create, and finally c) call the lambda function. We'll start this by just creating a version that does just instantly load the sprite, just to demonstrate how the lambda function/callback is used.
    Code (csharp):
    1. public class SpriteLoadManager : MonoBehaviour {
    2.  
    3. public delegate void SpriteLoadCallback(Sprite loadedSprite);
    4.  
    5. public static void LoadSprite(string spritePath, SpriteLoadCallback callback) {
    6. Sprite loadedSprite = Resources.Load<Sprite>(spritePath);
    7. callback(loadedSprite);
    8. }
    Conceptually, this isn't much different (currently) than just using a return value, but hopefully it's useful to illustrate how callbacks work. When you call LoadSprite, it loads the sprite, and then, it executes the callback, sending the loaded sprite as the parameter for the callback. The delegate line in the middle there simply defines the form that the callback function will take (e.g., that it's type "void" and has a Sprite parameter). By the way, if you have a more-traditionally-defined function that matches that form, you can pass that function as a callback by just omitting the () after it; that's just less useful for this particular use case than creating the lambda function was.

    So now that the framework is in place, let's make it useful. In order to run code across multiple frames (at least without the benefit of threads), we can't have this code running as a static function, which doesn't need a SpriteLoadManager object to exist in the scene. We're going to create a singleton. And because we don't need to do anything in this singleton but take advantage of its update loop, we're just going to have the singleton spawn itself on demand.
    Code (csharp):
    1. public class SpriteLoadManager : MonoBehaviour {
    2. private static SpriteLoadManager _instance;
    3. private static SpriteLoadManager instance {
    4. get {
    5. if (_instance == null) {
    6. GameObject newGO = new GameObject("SpriteLoadManager");
    7. _instance = newGO.AddComponent<SpriteLoadManager>();
    8. }
    9. return _instance;
    10. }
    11. }
    12. ....
    Now we have an object that will spawn itself, and all we have to do is use the name "instance", and it will either find the one that already exists, or make a new one and return that.

    Now it's time to use this object to run our "coroutine", which again, is in quotes for a reason. The original concept was to load things in a coroutine, but when implementing that, we'd run into a number of challenges, especially when calling the coroutine from multiple sources. The coroutines we spawn would run into each other, have to wait for each other to finish, etc. So instead of using actual coroutines, we're going to just put this stuff in "Update", and store the stuff we need in a queue. That's another cool thing about delegate/callbacks: they can not only be passed around, they can be stored for later, just like any variable! To do this, we're going to create a class (to store the path and the callback together), then make a queue of this class:
    Code (csharp):
    1. public class LoadingQueueItem {
    2. public string path;
    3. public SpriteLoadCallback callback;
    4. }
    5. public Queue<LoadingQueueItem> loadingQueue = new Queue<LoadingQueueItem>();
    Now, we just need to modify our existing LoadSprite function. Instead of loading the sprite, we'll queue it up to be loaded, and then in Update, we finish the job, but only for one sprite per frame:
    Code (csharp):
    1. public static void LoadSprite(string spritePath, SpriteLoadCallback callback) {
    2. var newLQI = new LoadingQueueItem();
    3. newLQI.path = spritePath;
    4. newLQI.callback = callback;
    5. instance.LoadingQueue.Enqueue(newLQI);
    6. }
    7.  
    8. void Update() {
    9. if (loadingQueue.Count > 0) {
    10. var dequeuedLQI = loadingQueue.Dequeue();
    11. Sprite loadedSprite = Resources.Load<Sprite>(dequeuedLQI.spritePath);
    12. dequeuedLQI.callback(loadedSprite);
    13. }
    14. }
    And that's that! You can do a bunch more stuff with this. If you find your target deployment devices can handle 2 sprites loaded per frame without a hiccup, it can repeat the code in Update twice. You could use the system clock (not Unity's) to time the length it takes to load a sprite, and then decide to load another sprite if the sprite loaded particularly quickly (if you're on a fast device or if it's a small sprite, or if you're loading additional sprites from the same atlas as one that's already loaded). And as mentioned, if it ever becomes possible to load a sprite in a threadsafe way, you can do that and not impact the main thread at all. And because this is a single shared queue, you can use LoadSprite from anywhere, and no matter how many of these things you call at one time, you'll only ever be loading one sprite per frame, which makes it much less likely you'll have frame hiccups. The sky's the limit!
     
    Last edited: May 21, 2019
    ZoraMikau likes this.
  11. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    If you want to shave the string comparison off you can use an enum or maybe a dictonary.

    I don't about what @StarManta suggests, i dpnt have time to read though it.
     
  12. Reedex

    Reedex

    Joined:
    Sep 20, 2016
    Posts:
    389
    Hi, so i tried yours solution, although there were some mini-mistakes i was fortunately able to fix them.
    (f.e. public SpriteLoadCallback callback; In LoadingQueueItem class, i guess should have been
    public SpriteLoadManager.SpriteLoadCallback callback;)
    Anyway, it works to an extent...
    If a drop object spawns after enemy and i pick it up, it works.
    If i go to shop and there is bunch of items he should have, it's all blank.
    Certainly it has to do with my implementation of how that's done.
    I've been able to vaporize the lag by previous suggestion and that is, cache all sprites in the beginning,
    and in ItemScript.cs Item.set, just find them in that list by name. hopes that list-name-finding won't introduce lag later on since it's string comparsion which i presume is expensive.
    I'm gonna use your example as callback study and a look-up.
    Thanks.
     
    Last edited: May 24, 2019
  13. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    ZoraMikau and SparrowGS like this.