Appearance
Appearance
Unity WebGL builds allow your game to run in a web browser, but they have some limitations compared to building and deploying traditionally. That being said, the Metaplay SDK offers workarounds to some of these issues.
Here are some considerations:
The Metaplay SDK comes with an analyzer that produces warnings when it detects usage of known poorly-supported features in your client code, if using a Unity version supporting Roslyn analyzers (>= Unity 2020.2). The analyzer will become active once you enable Metaplay's WebGL support (see section Enabling WebGL). Note that the analyzer only covers your client's own code (and the Metaplay SDK itself), but not dependencies.
You need to configure a few things to get the WebGL builds working:
METAPLAY_ENABLE_WEBGL
. csc.rsp
in your Assets
folder, with a line saying -define:METAPLAY_ENABLE_WEBGL
.WebSockets:Enabled
to true
in your Options.base.yaml
.experimental:websockets:enabled
to true
in your Helm values.Unity WebGL has very poor support for threading, and you should treat it as a single-threaded environment. Certain types don't work in WebGL or might cause random crashes or hang-ups, including most threading primitives under the System.Threading
namespace, such as ThreadPool
, Timer
, and Thread
. Some concurrent data structures might also cause problems. System.Threading.Task
and ValueTask
work fine, as long as work is never queued in the broken ThreadPool
. You must take care to avoid accidentally using methods that do so.
The SynchronizationContext
class provides the environment for tasks and asynchronous operations to run in. Each thread has a current synchronization context accessible by calling SynchronizationContext.Current
. You can override the default synchronization context with your own to change how asynchronous operations behave in the code.
Fortunately, you don't have to do that since Unity provides a custom UnitySynchronizationContext
that runs all tasks on the Unity main thread by default. So all we have to do is make sure we use it.
The biggest pitfalls while using tasks are using the following methods:
Task.Run()
Task.Delay()
Task.ContinueWith()
Task.ConfigureAwait(false)
Task.Run()
, Task.ContinueWith()
, and Task.ConfigureAwait(false)
will all schedule execution on the ThreadPool
if not told otherwise, which will break when running in WebGL. Task.Delay()
uses System.Threading.Timer
internally, which does not work.
The Metaplay SDK provides direct replacements for these to make things run smoothly:
Task.Run()
-> MetaTask.Run()
Task.Delay()
-> MetaTask.Delay()
Task.ContinueWith()
-> Task.ContinueWithCtx()
Task.ConfigureAwait(false)
-> Task.ConfigureAwaitFalse()
These work the same as their regular counterparts when not running in WebGL but will not try to schedule anything on the ThreadPool
in WebGL.
Any normal await
calls will work fine, but some libraries may use the ThreadPool
internally when calling their async methods, so make sure to check them beforehand.
Using Task
s to execute long-running operations in the background is a common use case, which works fine when using await
to wait for web requests or file operations. If the operation is a long-running calculation or otherwise blocks the frame loop while executing, it is better to implement it as a Unity coroutine. You can also use await Task.Yield()
between the calculation steps to pass the execution back to the game loop.
The WebGL runtime doesn't support blocking operations, e.g., waiting for web requests or file operations to complete. Instead, all operations must be performed asynchronously by await
ing on Task
s or other similar primitives.
The Metaplay SDK provides a MetaTimer
class that replaces System.Threading.Timer
in WebGL. MetaTask.Delay()
also uses this class internally.
The standard library's thread-safe collection classes are reported to cause random crashes and hangs in Unity WebGL builds. This applies to ConcurrentDictionary<>
, ConcurrentStack<>
, and so on.
The Metaplay SDK solves this by introducing a simple wrapper WebConcurrentDictionary<>
that derives from the non-threaded system Dictionary<>
and implements some missing APIs of the concurrent version, such as TryRemove()
and GetOrAdd()
. The code using the collections uses #if
s to decide which version to use:
#if UNITY_WEBGL && !UNITY_EDITOR
using IntStringConcurrentDictionary = System.Collections.Concurrent.WebConcurrentDictionary<int, string>;
#else
using IntStringConcurrentDictionary = System.Collections.Concurrent.ConcurrentDictionary<int, string>;
#endif
You'll need to do something similar if you'd like to use concurrent data structures in your game.
Unity implements its file APIs on top of IndexedDB via Emscripten's MEMFS by default. This has some severe limitations as it tries to emulate a block storage API on top of the IndexedDB blob storage. The main limitations are:
The Metaplay SDK implements a direct blob API WebBlobStore
with file operations that map directly onto IndexedDB operations. The WebBlobStore
API operates only with full payloads and does not allow streaming operations or partial reads.
Metaplay's FileUtil
APIs are mapped to WebBlobStore
in WebGL builds and can be used directly from the application. Note that you can use only the Async
-suffixed versions due to limitations in Unity!
📝 Note
Consider using MetaplaySDK.PersistentDataPath
instead of Unity's Application.persistentDataPath
as the Metaplay wrapper returns a simpler path when running under WebGL.
The Metaplay SDK includes another blob API, AtomicBlobStore
, which supports atomic read and write operations on top of a regular file system. On WebGL, this is implemented on top of the browser's localStorage.
The localStorage only provides a fully synchronous API, reflected in the AtomicBlobStore
. This is also required for the browser to robustly persist any state when the browser or tab is being closed, as asynchronous operations are not guaranteed to complete.
The System.Net.Http.HttpClient
doesn't work in WebGL as it uses unsupported Task
methods internally. However, you can use UnityWebRequest
, which also works in WebGL builds.
Alternatively, the Metaplay SDK provides a thin wrapper MetaHttpClient
with a similar API to the system HttpClient
that is implemented using Unity's UnityWebRequest
in WebGL builds and system HttpClient
elsewhere. It only provides the operations needed for the SDK itself to work, but it should be trivial to extend.