Open angelhodar opened 3 years ago
This is because at the end of the Connect()
method, you have await Receive();
, so Connect
doesn't actually return until after you've disconnected.
Personally, I wasn't a big fan of this, so I always make the following changes in my projects:
I split Connect()
into separate Connect
and Listen
methods:
public async Task Connect()
{
try
{
m_TokenSource = new CancellationTokenSource();
m_CancellationToken = m_TokenSource.Token;
m_Socket = new ClientWebSocket();
foreach (var header in headers)
{
m_Socket.Options.SetRequestHeader(header.Key, header.Value);
}
await m_Socket.ConnectAsync(uri, m_CancellationToken);
OnOpen?.Invoke();
}
catch (Exception ex)
{
OnError?.Invoke(ex.Message);
OnClose?.Invoke(WebSocketCloseCode.Abnormal);
}
}
public async void Listen()
{
try
{
await Receive();
}
catch (Exception ex)
{
OnError?.Invoke(ex.Message);
OnClose?.Invoke(WebSocketCloseCode.Abnormal);
}
finally
{
if (m_Socket != null)
{
m_TokenSource.Cancel();
m_Socket.Dispose();
}
}
}
On my implementation layer, this means that my Connect()
method looks something like this:
public async Task Connect( string uri )
{
if( this.isOpen )
return; // we're already connected
this.m_socket = new WebSocket( uri );
// add our event listeners
this.m_socket.OnOpen += this._onWebSocketOpen;
...
// launch our connection
await this.m_socket.Connect();
// wait until the socket is actually open
await new WaitUntil( () => this.isOpen );
}
public void Update()
{
#if !UNITY_WEBGL || UNITY_EDITOR
if( this.isOpen )
this.m_socket.DispatchMessageQueue(); // get any messages
#endif
}
// called when our socket is open and ready for business
private void _onWebSocketOpen()
{
// start listening on the socket
this.m_socket.Listen();
}
This then lets me do something like the following any time I actually need to connect:
if( !this.m_networkService.isConnected )
await this.m_networkService.Connect( websocketServerURI );
this.m_networkService.DoSomeNetworkCall();
You'll also need the following to allow you to await
built-in Unity calls like WaitUntil
:
Extension methods to expose a GetAwaiter()
method:
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using UnityEngine;
public static class ExtensionMethods
{
// Allows the use of async/await (instead of yield) with any Unity AsyncOperation (e.g. UnityWebRequest)
// from https://gist.github.com/mattyellen/d63f1f557d08f7254345bff77bfdc8b3
public static TaskAwaiter GetAwaiter( this AsyncOperation asyncOp )
{
var tcs = new TaskCompletionSource<object>();
asyncOp.completed += obj => { tcs.SetResult( null ); };
return ( (Task)tcs.Task ).GetAwaiter();
}
// Allows the use of async/await for a WaitUntil coroutine
public static AsyncCoroutineAwaiter GetAwaiter( this WaitUntil wait )
{
AsyncCoroutineAwaiter awaiter = new AsyncCoroutineAwaiter();
AsyncCoroutineRunner.instance.StartCoroutine( wait, awaiter );
return awaiter;
}
}
The awaiter wrapper class:
using System.Runtime.CompilerServices;
/**
* A custom awaiter used when moving from coroutines to async/await
*/
public class AsyncCoroutineAwaiter : INotifyCompletion
{
private System.Action m_continuation = null; // an action invoked when the task is done
public bool IsCompleted { get; private set; }
public void GetResult() { }
public void Complete()
{
this.IsCompleted = true;
this.m_continuation?.Invoke();
}
void INotifyCompletion.OnCompleted( System.Action continuation )
{
this.m_continuation = continuation;
}
}
Class to actually run the coroutines on:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/**
* Helper class for running coroutines using the new async/await flow
*/
public class AsyncCoroutineRunner : MonoBehaviour
{
private static AsyncCoroutineRunner m_instance = null;
public static AsyncCoroutineRunner instance
{
get
{
if( AsyncCoroutineRunner.m_instance == null )
{
GameObject gObj = new GameObject( "AsyncCoroutineRunner" );
AsyncCoroutineRunner.m_instance = gObj.AddComponent<AsyncCoroutineRunner>();
gObj.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad( gObj );
}
return AsyncCoroutineRunner.m_instance;
}
}
void OnDestroy()
{
this.StopAllCoroutines();
}
public Coroutine StartCoroutine( object instruction, AsyncCoroutineAwaiter awaiter )
{
return this.StartCoroutine( this._wrap( instruction, awaiter ) );
}
private IEnumerator _wrap( object instruction, AsyncCoroutineAwaiter awaiter )
{
yield return instruction;
awaiter.Complete();
}
}
Then, you can add new extension methods as you see fit to the ExtensionMethods
static class. For example, if you wanted to await
a WaitForSeconds()
call:
// Allows the use of async/await for a WaitForSeconds coroutine
public static AsyncCoroutineAwaiter GetAwaiter( this WaitForSeconds wait )
{
AsyncCoroutineAwaiter awaiter = new AsyncCoroutineAwaiter();
AsyncCoroutineRunner.instance.StartCoroutine( wait, awaiter );
return awaiter;
}
// in your code elsewhere
await new WaitForSeconds( 1.5f );
Hey @endel, I just noticed that the Connect method is blocking because execution doesnt continue after calling method from another script, is this intentional? Also, can this be created outside a MonoBehaviour class? I say it because of the code inside the Update method for editor and webgl.
Thanks anyway for such a simple and amazing package!