Can CoRoutines return a value?

I’m new to C# with Unity, but an expert Flash/AS3 developer…

I’m trying to call a function that will

  1. Load an image, and once loaded,
  2. Return the image.

The most common method for loading external files appears to be through yeilds on StartCoRoutines:


using UnityEngine;
using System.Collections;

public class TestScript : MonoBehaviour {

	IEnumerator Start() {
		yield return StartCoroutine( loadAsset("http://sm.local/some_image.png") );
	}
	
	IEnumerator loadAsset( string url ) {
		WWW www = new WWW( url );
		float elapsedTime = 0.0f;
		
		while (!www.isDone) {
			elapsedTime += Time.deltaTime;
			if (elapsedTime >= 10.0f) break;
			yield return null;  
		}
	
		if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
			Debug.LogError("Load Failed");
			yield break;
		}
	
		// load successful
		Texture loadedTexture = www.texture;
	}
}

That’s swell – except I want to reuse the loadAsset function to return multiple images. Something more like this:


IEnumerator Start() {
	Texture image1 = yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
	Texture image2 = yield return StartCoroutine( loadAsset("http://sm.local/some_other_image.png" ) );
}

IEnumerator loadAsset( string url ){
	WWW www = new WWW( url );
	
	// ... yeild / error logic here
	
 	return www.texture;
}

But there appears to be no way to do this. There’s a hackier method to accomplish this by using a global variable for the returned value:


using UnityEngine;
using System.Collections;

public class TestScript : MonoBehaviour {
	
	Texture returnedTextureFromLoadAsset;
	
	IEnumerator Start() {    	
		yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
		Texture image1 = returnedTextureFromLoadAsset;
		
		yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
		Texture image2 = returnedTextureFromLoadAsset;
	}
	
	IEnumerator loadAsset( string url ){
		WWW www = new WWW( url );
		float elapsedTime = 0.0f;
		
		while (!www.isDone) {
			elapsedTime += Time.deltaTime;
			if (elapsedTime >= 10.0f) break;
			yield return null;  
		}
	
		if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
			Debug.LogError("Load Failed");
			yield break;
		}
	
		// load successful
		returnedTextureFromLoadAsset = www.texture;
	}
}

… but this seems very non-OOP.

Surely there is some method to have a CoRoutine return a value locally to the calling function.

You can supply a callback and use lambda expressions to deal with the result like this:

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour
{
    IEnumerator Start()
    {
        Texture2D texture;
        yield return StartCoroutine(loadAsset("somefile", value => texture = value));
        // use texture
    }

    IEnumerator loadAsset(string url, System.Action<Texture2D> result)
    {
        WWW www = new WWW(url);
        float elapsedTime = 0.0f;

        while (!www.isDone)
        {
            elapsedTime += Time.deltaTime;
            if (elapsedTime >= 10.0f) break;
            yield return null;
        }

        if (!www.isDone || !string.IsNullOrEmpty(www.error))
        {
            Debug.LogError("Load Failed");
            result(null);    // Pass null result.
            yield break;
        }

        result(www.texture); // Pass retrieved result.
    }
}

You can also ignore the temporary variable and use it directly like this:

IEnumerator Start()
{        
    yield return StartCoroutine(loadAsset("sometexture", 
                   texture => 
                   renderer.sharedMaterial.mainTexture = texture));
}

But from a code design point of view, unless you are making a very generic image loader, consider what code is different or similar between the loads. In your case, the destination variable was changing over each call, that’s why I used a lambda expression. If you were adding the assets to a List, then perhaps you would have written loadAsset to accept a List that files were added to. Perhaps you want to change some materials main texture, in that case loadAsset might benefit from a Material parameter and set the texture after it’s loaded. Using a delegate is very generic, resulting in a flexible method but also hard to understand for newbies and might suffer from garbage generation if misused. It wasn’t very clear what your intention for the texture in your Q was.

If you need the coroutine to return a value to work as a call back messenger, then you’re probably better off with the lambda from Statement’s answer.

In other case, instead of using ugly lambda and/or dangerous generics, you can simply create a class and use its instances. This will work side by side with no issues as well:

using UnityEngine;
using System.Collections;

public class TestScript : MonoBehaviour {
	void Start () {
		StartCoroutine( GetImage1() );
		StartCoroutine( GetImage2() );
	}
	IEnumerator GetImage1 () {
		LoadAsset loadAsset = new LoadAsset();
		
		IEnumerator e = loadAsset.Now("http://sm.local/some_image.png");
		while ( e.MoveNext() ) { yield return e.Current; }
		
		Texture image1 = loadAsset.texture;
	}
	IEnumerator GetImage2 () {
		LoadAsset loadAsset = new LoadAsset();
		
		IEnumerator e = loadAsset.Now("http://sm.local/some_image.png");
		while ( e.MoveNext() ) { yield return e.Current; }
		
		Texture image2 = loadAsset.texture;
	}
}

public class LoadAsset {
	Texture texture;
	
	IEnumerator Now ( string url ) {
		WWW www = new WWW( url );
		float elapsedTime = 0.0f;
		
		while (!www.isDone) {
			elapsedTime += Time.deltaTime;
			if (elapsedTime >= 10.0f) break;
			yield return null;  
		}
		
		if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
			Debug.LogError("Load Failed");
			yield break;
		}
		
		// load successful
		texture = www.texture;
	}
}

There is an ugly way. First you return the texture using yield return:

IEnumerator loadAsset( string url ){
	WWW www = new WWW( url );
	
	// ... yeild / error logic here
	
 	yield return www.texture;
}

Then you call your Coroutine this way:

IEnumerator Start() {

  IEnumerator e = loadAsset("http://sm.local/some_image.png" );
  while(e.MoveNext()) {
    yield return e.Current;
  }
  Texture image1 = e.Current as Texture;

  //...

}

There’s a little difference (1 frame) because you would have to check if e.Current is a Texture before the yield return, but I think the code is ugly enough.

You can pass pointers to objects as arguments to a coroutine. This means you can create an empty Texture2D image object first, pass the pointer in as an argument (without using “out”), then copy the contents of the loaded texture into the passed in texture. I don’t have the commands memorized, but it’s really straightforward, and how I handled this exact situation. Then your texture is automatically ready as soon as it’s done downloading. You don’t have to deal with a return or output value at all.

You’re basically telling the function, “Here, download the image at this URL and put it into this texture.”

using UnityEngine;
using System.Collections;

public class TestScript : MonoBehaviour {
 
    IEnumerator Start() {
       yield return StartCoroutine( loadAsset("http://sm.local/some_image.png") );
    }
 
    IEnumerator loadAsset( string url ) {
       WWW www = new WWW( url );
       float elapsedTime = 0.0f;
 
       while (!www.isDone) {
         elapsedTime += Time.deltaTime;
         if (elapsedTime >= 10.0f) break;
         yield return null;  
       }
 
       if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
         Debug.LogError("Load Failed");
         yield break;
       }
 
       // load successful
       Texture loadedTexture = www.texture;
    }
}

That’s swell – except I want to reuse the loadAsset function to return multiple images. Something more like this:

IEnumerator Start() {
    Texture image1 = yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
    Texture image2 = yield return StartCoroutine( loadAsset("http://sm.local/some_other_image.png" ) );
}
 
IEnumerator loadAsset( string url ){
    WWW www = new WWW( url );
 
    // ... yeild / error logic here
 
    return www.texture;
}

But there appears to be no way to do this. There’s a hackier method to accomplish this by using a global variable for the returned value:

using UnityEngine;
using System.Collections;
 
public class TestScript : MonoBehaviour {
 
    Texture returnedTextureFromLoadAsset;
 
    IEnumerator Start() {      
       yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
       Texture image1 = returnedTextureFromLoadAsset;
 
       yield return StartCoroutine( loadAsset("http://sm.local/some_image.png" ) );
       Texture image2 = returnedTextureFromLoadAsset;
    }
 
    IEnumerator loadAsset( string url ){
       WWW www = new WWW( url );
       float elapsedTime = 0.0f;
 
       while (!www.isDone) {
         elapsedTime += Time.deltaTime;
         if (elapsedTime >= 10.0f) break;
         yield return null;  
       }
 
       if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
         Debug.LogError("Load Failed");
         yield break;
       }
 
       // load successful
       returnedTextureFromLoadAsset = www.texture;
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using System.Collections;

namespace ws.winx.unity
{
    public class LoaderEvtArgs : EventArgs
    {


        public readonly object data;
        

        public LoaderEvtArgs(object data)
        {
            this.data = data;

        }
    }


	public class Loader:IDisposable
	{
        public event EventHandler<LoaderEvtArgs> LoadComplete;
        public event EventHandler<LoaderEvtArgs> Error;
        public event EventHandler<LoaderEvtArgs> LoadItemComplete;


        protected bool _isRunning=false;

        private static List<WWW> _wwwList;
        private static List<WWW> _queueList;

        protected static List<WWW> queueList
        {
            get { if (_queueList == null) _queueList = new List<WWW>(); return _queueList; }
        }

        protected static List<WWW> wwwList{
            get{ if(_wwwList==null) _wwwList=new List<WWW>(); return _wwwList; }
        }

        private static MonoBehaviour _behaviour;


        protected static MonoBehaviour behaviour
        {
            get { if (_behaviour == null) { _behaviour = (new GameObject("WWWRequest")).AddComponent<MonoBehaviour>(); } return _behaviour; }
        }

        public void load()
        {
            if (!_isRunning)
            {
                _isRunning = true;
                behaviour.StartCoroutine(check());
                if (wwwList.Count != queueList.Count)
                {
                     foreach (WWW www in queueList)
                     {
                        queueList.Add(www);
                     }
                }
            }
        }

      

        public void Add(string url){
            if (Application.isEditor)
                url = "file:///" + url;

            WWW w = new WWW(url);
            Add(w);

        }

        public void Add(WWW www)
        {
           
            wwwList.Add(www);
            queueList.Add(www);

        }


       


       
        

        IEnumerator check()
        { 
            int i;
			WWW www;
            while (true)
            {
               i=0;

               while (queueList.Count>i)
				{

					www=queueList.ElementAt(i);
                    if (www.isDone)
                    {
                        if (!String.IsNullOrEmpty(www.error))
                        {


                            if (Error != null)
                            {
                                Error(this,new LoaderEvtArgs(www));
                                
                            }
                            else
                            {
                                Debug.LogError(www.error);
                            }

							
                           

                        }else 
							if (LoadItemComplete != null) 
								LoadItemComplete(this, new LoaderEvtArgs(www));
                       
                        queueList.RemoveAt(i);
                    }

                    i++;


                }

               if (queueList.Count == 0)
               {
                   _isRunning = false;

                    if (LoadComplete != null) LoadComplete(this, new LoaderEvtArgs(wwwList));

                    yield break; 
               }


                yield return new WaitForSeconds(0.5f);
            }
        }
	
public void  Dispose()
{
    if(_queueList!=null) _queueList.Clear();
    if(_wwwList!=null) _wwwList.Clear();

 	
}
}
}

Loader as3 like and clean would look like above and you would use it like

 Loader request = new Loader();
            request.Add(Path.Combine(Path.Combine(Application.dataPath, "StreamingAssets"), "InputSettings.xml"));
            request.LoadComplete += new EventHandler<LoaderEvtArgs>(onLoadComplete);
            request.Error += new EventHandler<LoaderEvtArgs>(onLoadItemComplete);
            request.LoadItemComplete += new EventHandler<LoaderEvtArgs>(onLoadItemComplete);
            request.load();


 void onLoadItemComplete(object sender, LoaderEvtArgs args)
        {
            Debug.Log(((WWW)args.data).text);
        }


        void onLoadError(object sender, LoaderEvtArgs args)
        {
             Debug.Log(((WWW)args.data).error);
        }

 void onLoadComplete(object sender, LoaderEvtArgs args)
        {
            Debug.Log(((List<WWW>)args.data).ElementAt(0).text);
}

You can use a GameObject as additional parameter to the IEnumerator and return the value using GameObject.SendMessage. Not pretty, but works.

I’ve had a lot of success with the following paradigm.

// Utility class for returning success or failure of an operation
public class CoroutineResult {
    // whether the current operation we're waiting on is done
    public bool done = false;
    // when done is set, whether that operation was successful or not
    public bool success = false;


    // some utility methods for groups

    public static bool AllDone(IList<CoroutineResult> results)
    {
        for (int i = 0; i < results.Count; ++i) {
            if (!results*.done)*

return false;
}
return true;
}
public static bool AllSuccess(IList results)
{
for (int i = 0; i < results.Count; ++i) {
if (!results*.success)*
return false;
}
return true;
}
public static bool AllDone(IList<CoroutineResult> results)
{
for (int i = 0; i < results.Count; ++i) {
if (!results*.done)*
return false;
}
return true;
}
public static bool AllSuccess(IList<CoroutineResult> results)
{
for (int i = 0; i < results.Count; ++i) {
if (!results*.success)*
return false;
}
return true;
}
}

// Additional value parameter for returning results back to the user
public class CoroutineResult : CoroutineResult {
// optional return value to report back
public T value;
}
Here’s an example of it being used:
public class MyMono : MonoBehaviour {

private DatabaseConnection connection;

// simple coroutine that looks up usernames in a database and prints them in the log
public IEnumerator LogUserNames(params string[] userIds)
{
// start the subcoroutine
CoroutineResult<string[]> databaseCall = connection.GetUsernames(userIds);

// wait until the command is done
while (!databaseCall.done) {
yield return null;
}

// check if we were successful
if (!databaseCall.success) {
Debug.LogWarning(“Couldn’t find doug’s name!”, this);
yield break;
}

// log the resulting value
Debug.Log(“Usernames are: " + string.Join(”, ", databaseCall.value), this);
}
}

public class DatabaseConnection : MonoBehaviour {

// public function that gets usernames from a list of user ids
// the result should be guaranteed to be set, even in face of exceptions
public CoroutineResult<string[]> GetUsernames(IEnumerable userIds)
{
CoroutineResult<string[]> result = new CoroutineResult<string[]>();
StartCoroutine(GetUsernames_Coroutine(result, userIds));
return result;
}

private IEnumerator GetUsernames_Coroutine(CoroutineResult<string[]> result, IEnumerable userIds)
{
try {
// spin off a bunch of subcoroutines
List<CoroutineResult> fetches = new List<CoroutineResult>();
foreach (string userId in userIds) {
fetches.Add(GetStringFromDatabase(“users”, “username”, “user_id”, userId));
}

// wait for them all to be done
while (!CoroutineResult.AllDone(fetches)) {
yield return null;
}

// early out for failure
if (!CoroutineResult.AllSuccess(fetches)) {
Debug.LogWarning(“Database error!”, this);
yield break;
}

// report the value and success
List usernames = new List();
foreach (CoroutineResult fetch in fetches) {
usernames.Add(fetch.value);
}
result.value = usernames.ToArray();
result.success = true;
}
finally {
// always set done, even on failure
result.done = true;
}
}

public CoroutineResult GetStringFromDatabase(string table, string column, string keyColumn, string keyValue)
{
CoroutineResult result = new CoroutineResult();
// don’t write SQL like this…
string horribleInjectedSql = “SELECT " + column + " FROM " + table + " WHERE " + keyColumn + " = '” + keyValue + “';”;

// stick the sql in some queue and start sending the queued commands to the database…
QueueSqlCommand(result, horribleInjectedSql);
StartSqlQueueIfNeeded();

return result;
}
}

hello everyone

my solution is different.

public class CoroutineTexture
{
    public Texture2D data;
}

...
public IEnumerator GetTextureAsync(CoroutineTexture texture)
{
    ...
    texture.data = www.texture;
}

...
public IEnumerator LoadTextureAsync()
{
    CoroutineTexture ct = new CoroutineTexture();
    yield return StartCoroutine(GetTextureAsync(ct));
    Texture a = ct.data;
    ...
}

Try with C# out keyword:

IEnumerator Start() {
    Texture2D myTexture;
    yield return StartCoroutine( loadAsset("http://sm.local/some_image.png", out myTexture) );
}

IEnumerator loadAsset( string url, out Texture2D texture ){
    // C# requires the out parameter to be at least null-initialized
    texture = null;
    WWW www = new WWW( url );

    float elapsedTime = 0.0f;

    while (!www.isDone) {
       elapsedTime += Time.deltaTime;

       if (elapsedTime >= 10.0f) break;

       yield return null;  
    }

    if (!www.isDone || !string.IsNullOrEmpty(www.error)) {
       Debug.LogError("Load Failed");
       yield break;
    }

    // load successful
    texture = www.texture;
}