WebCall: A Class to Make Web Calls Cleaner and Easier
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!
#1 by Yohan Hadjedj on April 13th, 2015 ·
Hello!
Nice script (and awesome blog :D).
Just a question, why not make a singleton out of WebCall, so that we don’t need to pass the Monobehaviour as a parameter. The WebCall script would be on a gameObject in the scene, and never destroyed, you can even use DontDestroyOnLoad to keep it between changing scenes. And anyone can call it just by doing something like WebCall.Instance.Call().
Cheers!
#2 by jackson on April 14th, 2015 ·
Thanks for the compliments. :)
I’ve seen that sort of design commonly, and it’s one that fits many Unity projects well. I prefer to design the class without the requirement that it be used as a singleton. If you’d like to use it as a singleton, it’s easy to wrap in a class like you suggest. Here’s my stab at it:
The only part that’s really a singleton is the
MonoBehaviour
. You could write a more flexible script usable for otherWebCall
-like classes like so:There are lots of ways to organize it, but the point is that designing
WebCall
to not be a singleton allows you to wrap it with a singleton in whatever way makes most sense to your project. Since it’s open source, you could even modifyWebCall
itself. Or, if you prefer not to use singletons, you don’t have to make any changes at all.#3 by Yohan Hadjedj on April 15th, 2015 ·
Thanks for your answer!
For me (my projects), the best thing to do is to add the singleton code directly to the WebCall class, but as you say, that might just be my workflow ;)
#4 by Marcum on April 15th, 2015 ·
Excellent post. In the race to just get something to work I always made WWW calls the “Unity” way. Your WebCall class makes it so much cleaner. Thanks!!
#5 by Yohan Hadjedj on November 4th, 2015 ·
Hello again!
One nice thing you could add to this class is a timeout, which is not built in WWW :)