- Published on
Using UniRx To Perform Web Requests In Unity
- Authors
- Name
- Gökhan Doğramacı
Table of Contents
- Overview
- Web Requests in Play-Mode
- With Coroutines in Play-Mode
- With UniRx in Play-Mode
- Web Requests in Edit-Mode
- With Coroutines in Edit-Mode
- With UniRx in Edit-Mode
- Examples
- Waiting for list of requests:
- Making sequential requests
- Sending request when input box changes
- Comprehensive helper method to make requests
Overview
There are multiple ways to perform web requests in Unity, one of which is using UnityEngine.WWW
class. But this class is deprecated and the suggested way of making web requests is now UnityEngine.Networking.UnityWebRequest
class.
This is a very handy class to perform GET
, POST
, PUT
and DELETE
requests. When you send a request, it returns UnityWebRequestAsyncOperation
which you can use in multiple ways:
- In a coroutine where you can
yield
it - In an
async
method where you canawait
it - As an
IObservable
where you cansubscribe
to it
Remember, it always depends on the situation to decide which one of these solutions is suitable for you. You can employ the Coroutine
approach for simple use cases, even though they are a bit problematic. Or you can use the async
/ await
approach for the cases where you need to return a value easily, or catch errors. Using the IObservable
approach on the other hand may seem to be an overkill for some, but UniRx can be very powerful to perform the web requests considering its all benefits. Shortly, some of these are:
- Being able to publish multiple values in observable streams.
- Being able to easily catch errors, or format them to re-propagate.
- Being able to use all available extension methods such as
Where
,Select
,SelectMany
/ContinueWith
,Merge
,Concat
etc. to filter, format and combine multiple streams to create extremely easy-to-read flows.
In this article, we will learn how to perform web requests in play-mode and edit-mode with UniRx in Unity.
Disclaimer
This is not a post about how to import or how to use UniRx in Unity. If you would like to learn or get an idea about UniRx, please read this "What is UniRx?" article first. http://introtorx.com/ is also a great resource for learning Rx (.Net) as almost everything is applicable for UniRx.
Web Requests in Play-Mode
With Coroutines in Play-Mode
Normally, we perform the requests like this with the coroutines:
private void Start()
{
var coroutine = StartCoroutine(GetRequest("https://www.example.com"));
// When we need to cancel it
StopCoroutine(coroutine);
}
IEnumerator GetRequest(string uri)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
{
// Request and wait for the desired page.
yield return webRequest.SendWebRequest();
// ~~~ Process the result ~~~
}
}
Example is partially taken from the Unity documentation
This requires you to yield the request or listen to the operation and check IsDone
property to understand if it's completed. In both of the situations, you cannot surround the yield
statement with a try-catch
block, and you cannot return a value from the coroutine as the return type must be IEnumerator
. And don't forget, you need this coroutine to be started from one of the GameObjects which shouldn't be destroyed mistakenly for coroutine to be executed successfully.
With UniRx in Play-Mode
Luckily, with UniRx, we don't have the limitations above, and we don't need to manage a GameObject to execute the coroutine. Not only that, we will be able to access many useful extension methods. Here's an example:
private void Start()
{
var requestDisposable = UnityWebRequest
.Get("https://www.example.com")
.SendWebRequest()
.AsAsyncOperationObservable()
.Subscribe(result =>
{
// ~~~ Process the result ~~~
});
// When we need to cancel it
requestDisposable.Dispose();
}
We can easily convert a web request call (UnityWebRequestAsyncOperation
) to an observable stream with the AsAsyncOperationObservable
extension method. And whenever we need to cancel it, disposing of the return value of the subscription should do the trick.
Cancelling the subscription
Canceling the subscription does not abort the request but disposes of the request object to release its used resources. Most of the time, you do not need to Abort
the request, but if it is the intended behavior, it can be implemented in a helper or extension method with Observable.Create
method.
Web Requests in Edit-Mode
With Coroutines in Edit-Mode
You can still use the Coroutines to perform the web requests in Edit-Mode. For example, the EditorCoroutineUtility helper class can be used to start the coroutines whenever you need to call an endpoint in an Editor window.
Editor Coroutines package
To use EditorCoroutineUtility
helper class, com.unity.editorcoroutines
package must be imported to the project, and then the Unity.EditorCoroutines.Editor
assembly must be referenced.
public class ExampleWindow : EditorWindow
{
void OnEnable()
{
var url = "https://www.example.com";
var coroutine = EditorCoroutineUtility.StartCoroutine(GetRequest(url), owner: this);
// When we need to cancel it
EditorCoroutineUtility.StopCoroutine(coroutine);
}
IEnumerator GetRequest(string uri)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
{
// Request and wait for the desired page.
yield return webRequest.SendWebRequest();
// ~~~ Process the result ~~~
}
}
}
Example is partially taken from the Unity documentations
With this approach, coroutine execution is delegated to some other object (EditorCoroutine
class that listens to EditorApplication.Update
) but this doesn't remove the limitations of the coroutines.
With UniRx in Edit-Mode
Listening to the web requests with UniRx in Edit-Mode is very similar to doing it in Play-Mode. We can still use the all benefits of reactive extensions once we convert the async operation to an observable stream.
public class ExampleWindow : EditorWindow
{
void OnEnable()
{
var requestDisposable = UnityWebRequest
.Get("https://www.example.com")
.SendWebRequest()
.AsAsyncOperationObservable()
.Subscribe(result =>
{
// ~~~ Process the result ~~~
});
// When we need to cancel it
requestDisposable.Dispose();
}
}
However, by the time of writing this blog post, there's a nasty issue in UniRx code preventing listening to these requests in Edit-Mode as expected. You can see this issue I created in UniRx repository to see the details about this issue and how to solve it.
There is a couple of ways to listen to the request as expected until the issue is fixed.
IProgress
implementation as parameter
1. Providing an Even though you may not need the progress feedback, providing this parameter forces UniRx to check IsDone
property of the async operation, hence it works as expected.
var requestDisposable = UnityWebRequest
.Get("https://www.example.com")
.SendWebRequest()
.AsAsyncOperationObservable(new Progress<float>())
.Subscribe(result =>
{
// ~~~ Process the result ~~~
});
2. Using the fixed package
I already merged the fix in my forked repository. It contains a few improvements and fixes alongside this fix. You can import it to your project instead of the original UniRx
package for it to work as expected:
https://github.com/dogramacigokhan/UniRx.git?path=Assets/Plugins/UniRx/Scripts#gdfixes
Examples
From the codes above, it may look like there's no major difference between using coroutines and UniRx. But the difference becomes visible when we start using them in real-life scenarios. I won't be giving examples for the Coroutine counterparts, but only UniRx example. I think that would be enough to understand the point.
Waiting for list of requests:
private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();
private void Start()
{
// Inform when all of the requests are completed
var parallel = Observable.WhenAll(
Get("http://google.com/"),
Get("http://bing.com/"),
Get("http://unity3d.com/"));
parallel.Subscribe(xs =>
{
Debug.Log(xs[0]); // google
Debug.Log(xs[1]); // bing
Debug.Log(xs[2]); // unity
});
}
Making sequential requests
private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();
private void Start()
{
var disposable = Get("https://www.example.com")
.ContinueWith(operation =>
{
// ~~~ We can process the first response here and start the second call ~~~
var response = operation.webRequest.downloadHandler.text;
return Get("https://www.google.com");
})
.Subscribe(response => {
// ~~~ We can process the final response here ~~~
});
// Disposing this will cancel the full sequence
disposable.Dispose();
}
Sending request when input box changes
Let's see how can we perform a web request when an input box value changes by delaying the request until user stops typing for 200 milliseconds.
[SerializeField] private TMP_InputField searchBox;
private IObservable<UnityWebRequestAsyncOperation> Get(string url) =>
UnityWebRequest.Get(url).SendWebRequest().AsAsyncOperationObservable();
private void Start()
{
var disposable = this.searchBox
.OnValueChangedAsObservable()
.Throttle(TimeSpan.FromMilliseconds(200))
.SelectMany(searchTerm => Get($"https://www.example.com/q={searchTerm}"))
.Subscribe(response =>
{
// ~~~ We can process the response here ~~~
});
// Disposing this will cancel both search-box listening and the request listening
disposable.Dispose();
}
Comprehensive helper method to make requests
public static IObservable<UnityWebRequest> Download(
string url,
Func<string, UnityWebRequest> webRequestFunc,
Dictionary<string, string> headers = null)
{
return Observable.Create<UnityWebRequest>(observer =>
{
var request = webRequestFunc(url);
var requestDisposable = new SingleAssignmentDisposable();
// Set default header to accept json
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Accept", "application/json");
if (headers != null)
{
foreach (var header in headers)
{
request.SetRequestHeader(header.Key, header.Value);
}
}
IObservable<UnityWebRequest> HandleResult()
{
if (requestDisposable.IsDisposed)
{
return Observable.Throw<UnityWebRequest>(
new OperationCanceledException("Already disposed."));
}
if (request.result != UnityWebRequest.Result.Success)
{
return Observable.Throw<UnityWebRequest>(new WebException(request.error));
}
if (request.responseCode != (long)HttpStatusCode.OK)
{
return Observable.Throw<UnityWebRequest>(
new WebException($"{request.responseCode} - {request.downloadHandler.text}"));
}
return Observable.Return(request);
}
requestDisposable.Disposable = request
.SendWebRequest()
.AsAsyncOperationObservable()
.ContinueWith(_ => HandleResult())
.CatchIgnore((OperationCanceledException _) => observer.OnCompleted())
.Subscribe(result =>
{
observer.OnNext(result);
observer.OnCompleted();
}, observer.OnError);
return new CompositeDisposable(request, requestDisposable);
});
}
With this Download
method, we can:
- Make sure the lifetime of the request is handled properly
- Publish value(s) when the request completes successfully
- Publish error when the request fails for any reason
- Use it for a different type of
UnityWebRequest
s - Filter, reshape or combine its result with other observable streams to create complex but easy-to-follow flows.
For example, to perform a GET
request:
Download("https://www.example.com", UnityWebRequest.Get).Subscribe(result => { /* ~~~ Process the result ~~~ */ })
To perform a POST
request:
Download("https://www.example.com", uri => UnityWebRequest.Post(uri, "sample-post-data")).Subscribe()
There are many more extension methods that you can use with UniRx to reshape your data and combine multiple streams to simplify complex flows. Check out RxMarbles to see how some of the extension methods work in action.