Today’s article is about WebCall, a class to make Unity’s WWW cleaner and easier to use. How could it be cleaner or easier than it already is? By adding C# events! Normally your web calls have lots of clutter around them, your logic gets split across functions, and handling the call is hard when the GameObject or MonoBehaviour get destroyed. WebCall solves all these problems so your web calls are clean, easy, and robust. Read on for the source code and examples!

Let’s look at the problems with WWW a bit more closely. Here’s one way to use WWW.

class MyScript : MonoBehaviour
{
    void NormalCode()
    {
        // ... pre-download logic
 
        StartCoroutine(DownloadFile("http://files.com/myfile"));
 
        // ... logic continued at end of DownloadFile()
    }
 
    IEnumerator DownloadFile(string url)
    {
        // Download the file
        var www = new WWW(url);
        yield return www;
 
        // ... logic continues here
    }
}

Three problems crop up with this code. First, you have to split your logic into two functions. Second, your code is cluttered up with odd helpers like the call to StartCoroutine(), the yield return www, and a very awkward IEnumerator that makes you using System.Collections. Third, if your GameObject or MonoBehaviour are destroyed during the download you’ll never know that the download finished.

Here’s the other common approach with raw WWW:

class MyScript : MonoBehaviour
{
    // The file download
    WWW www;
 
    void NormalCode()
    {
        // ... pre-download logic
 
        // Download the file
        www = new WWW("http://files.com/myfile");
 
        // ... logic continued in Update()
    }
 
    void Update()
    {
        if (www != null && www.isDone)
        {
            // ... logic continues here
 
            // Remember to null the WWW
            www = null;
        }
    }
}

This approach also has three problems. You still have to split your logic into two functions. Your code is cluttered up with different (but still odd) helpers like the Update() function and a WWW field for what should be a detail of just one function in the class. And you still might not know if the download ever finished because the post-download logic is still tied a single GameObject and MonoBehaviour.

The solution to all of these problems is to wrap WWW in a class dispatches an event when the web call is done. Here’s how you use the WebCall class:

class MyScript : MonoBehaviour
{
    void NormalCode()
    {
        var webCall = new WebCall(this, "http://files.com/myfile");
        webCall.OnDone += w => Debug.Log("WebCall returned:\n" + w.Text);
    }
}

Voilà! All the logic is contained in one function and there’s no code clutter. But what if you want to solve the third problem and still get the OnDone event when the MonoBehaviour or GameObject are destroyed? Easy! Just call UseDifferentMonoBehaviour():

class MyScript : MonoBehaviour
{
    void NormalCode()
    {
        var webCall = new WebCall(this, "http://files.com/myfile");
        webCall.OnDone += w => Debug.Log("WebCall returned:\n" + w.Text);
        webCall.UseDifferentMonoBehaviour(otherScript);
    }
}

The API for WebCall is almost exactly the same as the API for WWW, as of Unity 5.0. The main difference is that you pass a MonoBehaviour to the constructors. That MonoBehaviour is used to run a coroutine just like in the first example, only WebCall hides all the ugliness behind the scenes.

Here’s the source code for WebCall:

using System;
using System.Collections;
using System.Collections.Generic;
 
using UnityEngine;
using System.Text;
 
/// <summary>
/// Unity3D WWW class wrapper that dispatches an event when it's done so you
/// don't have to keep checking the isDone field or use it from a coroutine.
/// </summary>
/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3021</author>
/// <license>MIT</license>
public class WebCall : IDisposable
{
	private static readonly Encoding UTF8Encoding = Encoding.UTF8;
 
	private MonoBehaviour monoBehaviour;
	private MonoBehaviour nextMonoBehaviour;
	private WWW www;
	private bool isDisposed;
 
	public delegate void OnDoneHandler(WebCall webCall);
 
	private OnDoneHandler onDoneInvoker = webCall => {};
 
	private WebCall(
		MonoBehaviour monoBehaviour,
		WWW www
	)
	{
		this.monoBehaviour = monoBehaviour;
		this.www = www;
		StartCoroutine();
	}
 
	public WebCall(
		MonoBehaviour monoBehaviour,
		string url
	)
	{
		this.monoBehaviour = monoBehaviour;
		www = new WWW(url);
		StartCoroutine();
	}
 
	public WebCall(
		MonoBehaviour monoBehaviour,
		string url,
		WWWForm form
	)
	{
		this.monoBehaviour = monoBehaviour;
		www = new WWW(url, form);
		StartCoroutine();
	}
 
	public WebCall(
		MonoBehaviour monoBehaviour,
		string url,
		byte[] postData
	)
	{
		this.monoBehaviour = monoBehaviour;
		www = new WWW(url, postData);
		StartCoroutine();
	}
 
	public WebCall(
		MonoBehaviour monoBehaviour,
		string url,
		byte[] postData,
		Dictionary<string, string> headers
	)
	{
		this.monoBehaviour = monoBehaviour;
		www = new WWW(url, postData, headers);
		StartCoroutine();
	}
 
	public event OnDoneHandler OnDone
	{
		add { onDoneInvoker += value; }
		remove { onDoneInvoker -= value; }
	}
 
	public MonoBehaviour MonoBehaviour
	{
		get { return monoBehaviour; }
	}
 
	public void UseDifferentMonoBehaviour(
		MonoBehaviour monoBehaviour
	)
	{
		nextMonoBehaviour = monoBehaviour;
	}
 
	public AssetBundle AssetBundle
	{
		get { return www.assetBundle; }
	}
 
	public AudioClip AudioClip
	{
		get { return www.audioClip; }
	}
 
	public byte[] Bytes
	{
		get { return www.bytes; }
	}
 
	public int BytesDownloaded
	{
		get { return www.bytesDownloaded; }
	}
 
	public string Error
	{
		get { return www.error; }
	}
 
	public bool IsDone
	{
		get { return www.isDone; }
	}
 
	public MovieTexture Movie
	{
		get { return www.movie; }
	}
 
	public float Progress
	{
		get { return www.progress; }
	}
 
	public Dictionary<string, string> ResponseHeaders
	{
		get { return www.responseHeaders; }
	}
 
	public string Text
	{
		get { return www.text; }
	}
 
	public Texture2D Texture
	{
		get { return www.texture; }
	}
 
	public Texture2D TextureNonReadable
	{
		get { return www.textureNonReadable; }
	}
 
	public ThreadPriority ThreadPriority
	{
		get { return www.threadPriority; }
		set { www.threadPriority = value; }
	}
 
	public float UploadProgress
	{
		get { return www.uploadProgress; }
	}
 
	public string URL
	{
		get { return www.url; }
	}
 
	public void Dispose()
	{
		isDisposed = true;
		www.Dispose();
	}
 
	public AudioClip GetAudioClip(
		bool threeD
	)
	{
		return www.GetAudioClip(threeD);
	}
 
	public AudioClip GetAudioClip(
		bool threeD,
		bool stream
	)
	{
		return www.GetAudioClip(threeD, stream);
	}
 
	public AudioClip GetAudioClip(
		bool threeD,
		bool stream,
		AudioType audioType
	)
	{
		return www.GetAudioClip(threeD, stream, audioType);
	}
 
	public AudioClip GetAudioClipCompressed()
	{
		return www.GetAudioClipCompressed();
	}
 
	public AudioClip GetAudioClipCompressed(
		bool threeD
	)
	{
		return www.GetAudioClipCompressed(threeD);
	}
 
	public AudioClip GetAudioClipCompressed(
		bool threeD,
		AudioType audioType
	)
	{
		return www.GetAudioClipCompressed(threeD, audioType);
	}
 
	public void LoadImageIntoTexture(Texture2D tex)
	{
		www.LoadImageIntoTexture(tex);
	}
 
	public static string EscapeURL(
		string s,
		Encoding e = null
	)
	{
		return WWW.EscapeURL(s, e ?? UTF8Encoding);
	}
 
	public static WebCall LoadFromCacheOrDownload(
		MonoBehaviour monoBehaviour,
		string url,
		int version,
		uint crc = 0
	)
	{
		var www = WWW.LoadFromCacheOrDownload(url, version, crc);
		return new WebCall(monoBehaviour, www);
	}
 
	public static string UnEscapeURL(
		string s,
		Encoding e = null
	)
	{
		return WWW.UnEscapeURL(s, e ?? UTF8Encoding);
	}
 
	private void StartCoroutine()
	{
		if (monoBehaviour)
		{
			monoBehaviour.StartCoroutine(Coroutine());
		}
		else
		{
			var msg = "Web call to URL (" + URL + ") does not have a valid "
				+ "MonoBehaviour to run a coroutine on. "
				+ "The OnDone() event will never fire.";
			Debug.LogError(msg);
		}
	}
 
	private IEnumerator Coroutine()
	{
		while (true)
		{
			// Requested to switch MonoBehaviour the coroutine is on
			if (nextMonoBehaviour)
			{
				monoBehaviour = nextMonoBehaviour;
				nextMonoBehaviour = null;
				StartCoroutine();
				yield break;
			}
 
			// WWW has been disposed
			if (isDisposed)
			{
				yield break;
			}
 
			// WWW is done
			if (www.isDone)
			{
				onDoneInvoker(this);
				yield break;
			}
 
			// Check again next frame
			yield return null;
		}
	}
}

I hope you find this useful! If you’ve got another way of handling web calls from Unity that you prefer instead or you start using WebCall in your own projects, post about it in the comments!