Search code examples
variablesunity-game-enginecoroutineyield-return

How to set variable inside of a Coroutine after yielding a webrequest


Okay I will try and explain this to the best of my ability. I have searched and searched all day for a solution to this issue but can't seem to find it. The problem that I am having is that I have a list of scriptable objects that I am basically using for custom properties to create gameobjects off of. One of those properties that I need to get is a Texture2D that I turn into a sprite. Therefor, I am using UnityWebRequest in a Coroutine and am having to yield the response. After I get the response I am trying to set the variable. However even using Lambdas it seems to me that if I yield return the response before the result it will not set the variable. So every time I check the variable after the Coroutine it comes back null. If someone could enlighten me with what I am missing here that would be just great!

Here is the Scriptable Object Class I am using.

[CreateAssetMenu(fileName = "new movie",menuName = "movie")]
public class MovieTemplate : ScriptableObject
{
    public string Title;
    public string Description;
    public string ImgURL;
    public string mainURL;
    public string secondaryURL;
    public Sprite thumbnail;
}

Here is the call to the Coroutine

foreach (var item in nodes)
{
    templates.Add(GetMovieData(item));
}

foreach (MovieTemplate movie in templates)
{
   StartCoroutine(GetMovieImage(movie.ImgURL, result => 
   {
       movie.thumbnail = result;
   }));
}

Here is the Coroutine itself

IEnumerator GetMovieImage(string url, System.Action<Sprite> result)
{
    using (UnityWebRequest web = UnityWebRequestTexture.GetTexture(url))
    {
        yield return web.SendWebRequest();
        var img = DownloadHandlerTexture.GetContent(web);
        result(Sprite.Create(img, new Rect(0, 0, img.width, img.height), Vector2.zero));
    }
}

Solution

  • From what you desribe it still seems that the texture is somehow disposed as soon as the routine finishes. My guess would be that it happens due to the using block.

    I would store the original texture reference

    [CreateAssetMenu(fileName = "new movie",menuName = "movie")]
    public class MovieTemplate : ScriptableObject
    {
        public string Title;
        public string Description;
        public string ImgURL;
        public string mainURL;
        public string secondaryURL;
        public Sprite thumbnail;
        public Texture texture;
    
        public void SetSprite(Sprite newSprite, Texture newTexture)
        {
            if(texture) Destroy(texture);
    
            texture = newTexture;
            var tex = (Texture2D) texture;
            thumbnail = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
        }
    }
    

    So you can keep track of the texture itself as well, let it not be collected by the GC but also destroy it when not needed anymore. Usually Texture2D is removed by the GC as soon as there is no reference to it anymore but Texture2D created by UnityWebRequest might behave different.

    Than in the webrequest return the texture and don't use using

    IEnumerator GetMovieImage(string url, System.Action<Texture> result)
    {
        UnityWebRequest web = UnityWebRequestTexture.GetTexture(url));
    
        yield return web.SendWebRequest();
    
        if(!web.error)
        {
            result?.Invoke(DownloadHandlerTexture.GetContent(web));
        }
        else
        {
            Debug.LogErrorFormat(this, "Download error: {0} - {1}", web.responseCode, web.error);
        }
    }
    

    and finally use it like

    for (int i = 0; i < templates.Count; i++)  
    {
        int index = i;//If u use i, it will be overriden too so we make a copy of it
        StartCoroutine(
            GetMovieImage(
                templates[index].ImgURL,
                result => 
                {
                    templates[index].SetSprite(result);
                })
            );
    }