Skip to content

Commit b6805e0

Browse files
Daxoderekhoffjdetterkazimuth
authored
SpacetimeDB working in Unity WebGL Builds (#286)
## Description of Changes As it stands today, Unity WebGL doesn't work. Partially the reason for this is Multi-Threading, and the other reason is the use of `ClientWebSocket`. In order to fix this (specifically in the case of Unity), here's some changes that _can_ be made. Note that this is mostly a suggestion and does come with a few flaws, though arguably it might still be better than it not working at all? Up to you! The Tl;Dr of how it works, is to: - **MultiThreading Problem**: simply invoke the `Task.Run` functions on main thread instead, and use a coroutine in place of where the two simultaneous threads was expected. - **ClientWebSocket**: Use a `.jslib` to create the WebSocket directly within Javascript, and then have JS call the corresponding correct functions. DISCLAIMER: currently OnClose doesn't quite work correctly as `__allocate` isn't invoked correctly. ## API Not a breaking change to the API, should be internal implementation details ## Requires SpacetimeDB PRs None ## Testsuite ???? SpacetimeDB branch name: master ## Testing Open the Blackholeio project, try building it for WebGL - [X] Made a game using the feature: https://daxode.itch.io/eat-to-the-deep --------- Co-authored-by: rekhoff <[email protected]> Co-authored-by: John Detter <[email protected]> Co-authored-by: John Detter <[email protected]> Co-authored-by: James Gilles <[email protected]>
1 parent a9020db commit b6805e0

File tree

5 files changed

+312
-4
lines changed

5 files changed

+312
-4
lines changed

src/Plugins.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Plugins/WebSocket.jslib

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
mergeInto(LibraryManager.library, {
2+
WebSocket_Init: function(openCallback, messageCallback, closeCallback, errorCallback) {
3+
this._webSocketManager = {
4+
instances: {},
5+
nextId: 1,
6+
callbacks: {
7+
open: null,
8+
message: null,
9+
close: null,
10+
error: null
11+
}
12+
};
13+
14+
var manager = this._webSocketManager;
15+
manager.callbacks.open = openCallback;
16+
manager.callbacks.message = messageCallback;
17+
manager.callbacks.close = closeCallback;
18+
manager.callbacks.error = errorCallback;
19+
},
20+
21+
WebSocket_Connect: function(uriPtr, protocolPtr, authTokenPtr) {
22+
try {
23+
var manager = this._webSocketManager;
24+
var uri = UTF8ToString(uriPtr);
25+
var protocol = UTF8ToString(protocolPtr);
26+
var authToken = UTF8ToString(authTokenPtr);
27+
28+
var socket = new window.WebSocket(uri, protocol);
29+
socket.binaryType = "arraybuffer";
30+
31+
var socketId = manager.nextId++;
32+
manager.instances[socketId] = socket;
33+
34+
socket.onopen = function() {
35+
if (manager.callbacks.open) {
36+
dynCall('vi', manager.callbacks.open, [socketId]);
37+
}
38+
};
39+
40+
socket.onmessage = function(event) {
41+
if (manager.callbacks.message && event.data instanceof ArrayBuffer) {
42+
var buffer = _malloc(event.data.byteLength);
43+
HEAPU8.set(new Uint8Array(event.data), buffer);
44+
dynCall('viii', manager.callbacks.message, [socketId, buffer, event.data.byteLength]);
45+
_free(buffer);
46+
}
47+
};
48+
socket.onclose = function(event) {
49+
if (manager.callbacks.close) {
50+
var reasonStr = event.reason || "";
51+
var reasonArray = intArrayFromString(reasonStr);
52+
var reasonPtr = _malloc(reasonArray.length);
53+
HEAP8.set(reasonArray, reasonPtr);
54+
dynCall('viii', manager.callbacks.close, [socketId, event.code, reasonPtr]);
55+
_free(reasonPtr);
56+
}
57+
delete manager.instances[socketId];
58+
};
59+
60+
socket.onerror = function(error) {
61+
if (manager.callbacks.error) {
62+
dynCall('vi', manager.callbacks.error, [socketId]);
63+
}
64+
};
65+
66+
return socketId;
67+
} catch (e) {
68+
console.error("WebSocket connection error:", e);
69+
return -1;
70+
}
71+
},
72+
73+
WebSocket_Send: function(socketId, dataPtr, length) {
74+
var manager = this._webSocketManager;
75+
var socket = manager.instances[socketId];
76+
if (!socket || socket.readyState !== socket.OPEN) return -1;
77+
78+
try {
79+
var data = new Uint8Array(HEAPU8.buffer, dataPtr, length);
80+
socket.send(data);
81+
return 0;
82+
} catch (e) {
83+
console.error("WebSocket send error:", e);
84+
return -1;
85+
}
86+
},
87+
88+
WebSocket_Close: function(socketId, code, reasonPtr) {
89+
var manager = this._webSocketManager;
90+
var socket = manager.instances[socketId];
91+
if (!socket) return;
92+
93+
var reason = UTF8ToString(reasonPtr);
94+
socket.close(code, reason);
95+
delete manager.instances[socketId];
96+
}
97+
});

src/Plugins/WebSocket.jslib.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/SpacetimeDBClient.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Concurrent;
34
using System.Collections.Generic;
45
using System.IO;
@@ -188,10 +189,18 @@ protected DbConnectionBase()
188189
SpacetimeDBNetworkManager._instance.RemoveConnection(this);
189190
}
190191
};
192+
193+
#if UNITY_WEBGL && !UNITY_EDITOR
194+
if (SpacetimeDBNetworkManager._instance != null)
195+
SpacetimeDBNetworkManager._instance.StartCoroutine(PreProcessMessages());
196+
#endif
191197
#endif
192198

199+
#if !(UNITY_WEBGL && !UNITY_EDITOR)
200+
// For targets other than webgl we start a thread to pre-process messages
193201
networkMessageProcessThread = new Thread(PreProcessMessages);
194202
networkMessageProcessThread.Start();
203+
#endif
195204
}
196205

197206
struct UnprocessedMessage
@@ -351,10 +360,19 @@ private static IEnumerable<byte[]> BsatnRowListIter(BsatnRowList list)
351360
};
352361
}
353362

363+
#if UNITY_WEBGL && !UNITY_EDITOR
364+
IEnumerator PreProcessMessages()
365+
#else
354366
void PreProcessMessages()
367+
#endif
355368
{
356369
while (!isClosing)
357370
{
371+
372+
#if UNITY_WEBGL && !UNITY_EDITOR
373+
yield return null;
374+
while (_messageQueue.Count > 0)
375+
#endif
358376
try
359377
{
360378
var message = _messageQueue.Take(_preProcessCancellationToken);
@@ -363,7 +381,11 @@ void PreProcessMessages()
363381
}
364382
catch (OperationCanceledException)
365383
{
384+
#if UNITY_WEBGL && !UNITY_EDITOR
385+
break;
386+
#else
366387
return; // Normal shutdown
388+
#endif
367389
}
368390
}
369391

@@ -586,7 +608,13 @@ public void Disconnect()
586608
{
587609
isClosing = true;
588610
connectionClosed = true;
589-
webSocket.Close();
611+
612+
// Only try to close if the connection is active
613+
if (webSocket.IsConnected)
614+
{
615+
webSocket.Close();
616+
}
617+
590618
_preProcessCancellationTokenSource.Cancel();
591619
}
592620

@@ -612,7 +640,11 @@ void IDbConnection.Connect(string? token, string uri, string addressOrName, Comp
612640
Log.Info($"SpacetimeDBClient: Connecting to {uri} {addressOrName}");
613641
if (!IsTesting)
614642
{
643+
#if UNITY_WEBGL && !UNITY_EDITOR
644+
async Task Function()
645+
#else
615646
Task.Run(async () =>
647+
#endif
616648
{
617649
try
618650
{
@@ -628,7 +660,12 @@ void IDbConnection.Connect(string? token, string uri, string addressOrName, Comp
628660

629661
Log.Exception(e);
630662
}
663+
#if UNITY_WEBGL && !UNITY_EDITOR
664+
}
665+
_ = Function();
666+
#else
631667
});
668+
#endif
632669
}
633670
}
634671

0 commit comments

Comments
 (0)