colyseus / colyseus-unity-sdk

⚔ Colyseus Multiplayer SDK for Unity
https://docs.colyseus.io/getting-started/unity-sdk/
MIT License
371 stars 100 forks source link

JsonSerializationException when receiving a message from server in WebGL export #198

Closed sticmac closed 1 year ago

sticmac commented 2 years ago

When exporting my Unity project as a WebGL application, the following exception is thrown during runtime:

JsonSerializationException: Unable to deserialize instance of MyMessage because there is no parameterless constructor is defined on type.

This prevents any data sent by the server through messages to be deserialized and used on the client, which is a major problem.

After investigating on the issue with my local fork, I found that it originates from the TryGetConstructor method, at line 200:

if (AotRuntime || type.IsAbstract || type.IsInterface)
    return false;

It seems that AotRuntime is set to true for WebGL and iOS platforms, as shown in line 43 of the same file.

#if ((UNITY_WEBGL || UNITY_IOS || ENABLE_IL2CPP) && !UNITY_EDITOR)
            AotRuntime = true;
#else

I don't really know what this parameter corresponds to, but it seems to cause the issue I describe here. My quick and dirty fix was to comment the AotRuntime part of the condition at line 200, but I can't tell if it was there for a reason in the first place. What should be done to fix this?

endel commented 2 years ago

Interesting, thanks for the report @sticmac. If you can could you share the version of Unity you're using?

EDIT: Also, are you possibly using "Managed Code Stripping"? If enabled your issue could be related to that (https://github.com/colyseus/colyseus-unity-sdk/issues/135)

sticmac commented 2 years ago

Thank you for your quick answer @endel. I'm using Unity 2021.3.2f1 LTS and the managed code stripping option is activated and set to minimal level.

endel commented 2 years ago

Hi @sticmac, would you mind providing the full stack trace for us? And perhaps a small reproduction scenario where we can check this error happening?

sticmac commented 2 years ago

The problem seems to happen while receiving any message containing a custom object in its payload while running the client as a WebGL application. In my case, I have this script for managing players connections (spawning characters for players already connected and for newly connected players).

public class PlayersConnection : MonoBehaviour
{
    [Header("Components")]
    [SerializeField] PlayerSpawner _playerSpawner;

    [Header("Models")]
    [SerializeField] IntObservableModel _playersCounter;

    private class ConnectedPlayersMessage {
        public Player[] players;
    }

    private void OnEnable() {
        StartCoroutine(OnEnableCoroutine());
    }

    private IEnumerator OnEnableCoroutine() {
        // Basically my method to wait until the client is connected to a room on the colyseus server
        yield return new WaitUntil(() => ColyseusManager.Instance.CurrentRoom != null);
        _playersCounter.Value = 1; // reset player counter (counting the player themselves)

        ColyseusRoom<GameRoomState> currentRoom = ColyseusManager.Instance.CurrentRoom;

        // Fetching already connected players to instantiate them
        currentRoom
            .OnMessage<ConnectedPlayersMessage>("connectedPlayers", (message) => {
                _playersCounter.Value = 1; // reset player counter (counting the player themselves)
                Player[] players = message.players;
                foreach (Player p in players) {
                    PlayerJoin(p);
                }
            });

        // Instantiates a newly joining player
        currentRoom.OnMessage<Player>("playerJoin", (p) => PlayerJoin(p));
        // and removes the ones leaving
        currentRoom.OnMessage<Player>("playerLeave", (p) => PlayerLeave(p));

        // We tell the server that we are ready to recieve already connected players
        currentRoom.Send("playerReady");
    }

    /// <summary>
    /// Increases player counter and make the player spawn if they're in the room
    /// </summary>
    /// <param name="p">The model of the player who joined</param>
    private void PlayerJoin(Player p) {
        if (p.position.room == SceneManager.GetActiveScene().buildIndex) {
            _playerSpawner.InstantiatePlayer(p);
        }
        _playersCounter.Value++; // increases player counter after adding joining player
    }

    /// <summary>
    /// Decreases player counter and dispawn the player if they're in the room
    /// </summary>
    /// <param name="p">The model of the player who joined</param>
    private void PlayerLeave(Player p) {
        if (p.position.room == SceneManager.GetActiveScene().buildIndex) {
            _playerSpawner.RemovePlayer(p);
        }
        _playersCounter.Value--; // decreases player counter after removing leaving player
    }
}

My schema classes are generated with schema-codegen:

public partial class Player : Schema {
    [Type(0, "string")]
    public string id = default(string);

    [Type(1, "string")]
    public string username = default(string);

    [Type(2, "ref", typeof(Position))]
    public Position position = new Position();
}

public partial class Position : Schema {
    [Type(0, "number")]
    public float room = default(float);

    [Type(1, "number")]
    public float x = default(float);

    [Type(2, "number")]
    public float y = default(float);

    [Type(3, "number")]
    public float z = default(float);
}

The above PlayersConnection scripts works perfectly fine when running from the editor. However, when building a WebGL application, exposing it through a local HTTP server on 8080 port and running it in my browser, I get this error in the browser console (development build):

JsonSerializationException: Unable to deserialize instance of 'PlayersConnection+ConnectedPlayersMessage' because there is no parameterless constructor is defined on type.
  at GameDevWare.Serialization.Metadata.TypeDescription.CreateInstance () [0x00000] in <00000000000000000000000000000000>:0 
--- End of stack trace from previous location where exception was thrown ---

WebGLClient.framework.js:1798:11
    _JS_Log_Dump http://127.0.0.1:8080/Build/WebGLClient.framework.js:1798
    WebGLPrintfConsolev(LogType, char const*, void*) http://127.0.0.1:8080/Build/WebGLClient.wasm:21852580
    InternalErrorConsole(char const*, ...) http://127.0.0.1:8080/Build/WebGLClient.wasm:21856477
    DebugStringToFilePostprocessedStacktrace(DebugStringToFileData const&) http://127.0.0.1:8080/Build/WebGLClient.wasm:21855962
    DebugStringToFile(DebugStringToFileData const&) http://127.0.0.1:8080/Build/WebGLClient.wasm:21853157
    Scripting::LogExceptionFromManaged(ScriptingExceptionPtr, int, char const*, bool, Scripting::LogExceptionFromMangedSettings const*) http://127.0.0.1:8080/Build/WebGLClient.wasm:21196712
    ScriptingInvocation::Invoke(ScriptingExceptionPtr*, bool) http://127.0.0.1:8080/Build/WebGLClient.wasm:21200125
    void ScriptingInvocation::Invoke<void>(ScriptingExceptionPtr*, bool) http://127.0.0.1:8080/Build/WebGLClient.wasm:21200627
    InitPlayerLoopCallbacks()::UpdateScriptRunDelayedTasksRegistrator::Forward() http://127.0.0.1:8080/Build/WebGLClient.wasm:21034722
    ExecutePlayerLoop(NativePlayerLoopSystem*) http://127.0.0.1:8080/Build/WebGLClient.wasm:11521830
    ExecutePlayerLoop(NativePlayerLoopSystem*) http://127.0.0.1:8080/Build/WebGLClient.wasm:11521986
    MainLoop() http://127.0.0.1:8080/Build/WebGLClient.wasm:19732962
    MainLoopUpdateFromBackground(void*) http://127.0.0.1:8080/Build/WebGLClient.wasm:19711104
    dynCall_vi http://127.0.0.1:8080/Build/WebGLClient.wasm:21972628
    createExportWrapper http://127.0.0.1:8080/Build/WebGLClient.framework.js:1036
    _emscripten_set_interval http://127.0.0.1:8080/Build/WebGLClient.framework.js:9974
    _emscripten_set_interval http://127.0.0.1:8080/Build/WebGLClient.framework.js:9975
    (Asynchrone : setInterval handler)
    _emscripten_set_interval http://127.0.0.1:8080/Build/WebGLClient.framework.js:9972
    main http://127.0.0.1:8080/Build/WebGLClient.wasm:19738445
    createExportWrapper http://127.0.0.1:8080/Build/WebGLClient.framework.js:1036
    callMain http://127.0.0.1:8080/Build/WebGLClient.framework.js:16994
    doRun http://127.0.0.1:8080/Build/WebGLClient.framework.js:17037
    run http://127.0.0.1:8080/Build/WebGLClient.framework.js:17049
    runCaller http://127.0.0.1:8080/Build/WebGLClient.framework.js:16977
    removeRunDependency http://127.0.0.1:8080/Build/WebGLClient.framework.js:991
    unityFileSystemInit http://127.0.0.1:8080/Build/WebGLClient.framework.js:37
    doCallback http://127.0.0.1:8080/Build/WebGLClient.framework.js:4865
    done http://127.0.0.1:8080/Build/WebGLClient.framework.js:4876
    oncomplete http://127.0.0.1:8080/Build/WebGLClient.framework.js:4232
    (Asynchrone : EventHandlerNonNull)
    reconcile http://127.0.0.1:8080/Build/WebGLClient.framework.js:4230
    syncfs http://127.0.0.1:8080/Build/WebGLClient.framework.js:4003
    <anonyme> http://127.0.0.1:8080/Build/WebGLClient.framework.js:4092
    (Asynchrone : EventHandlerNonNull)
    getRemoteSet http://127.0.0.1:8080/Build/WebGLClient.framework.js:4089
    onsuccess http://127.0.0.1:8080/Build/WebGLClient.framework.js:4039
    (Asynchrone : EventHandlerNonNull)
    getDB http://127.0.0.1:8080/Build/WebGLClient.framework.js:4036
    getRemoteSet http://127.0.0.1:8080/Build/WebGLClient.framework.js:4079
    syncfs http://127.0.0.1:8080/Build/WebGLClient.framework.js:3999
    getLocalSet http://127.0.0.1:8080/Build/WebGLClient.framework.js:4072
    syncfs http://127.0.0.1:8080/Build/WebGLClient.framework.js:3997
    syncfs http://127.0.0.1:8080/Build/WebGLClient.framework.js:4883
    syncfs http://127.0.0.1:8080/Build/WebGLClient.framework.js:4879
    unityFileSystemInit http://127.0.0.1:8080/Build/WebGLClient.framework.js:35
    unityFramework http://127.0.0.1:8080/Build/WebGLClient.framework.js:40
    callRuntimeCallbacks http://127.0.0.1:8080/Build/WebGLClient.framework.js:1186
    preRun http://127.0.0.1:8080/Build/WebGLClient.framework.js:867
    run http://127.0.0.1:8080/Build/WebGLClient.framework.js:17025
    runCaller http://127.0.0.1:8080/Build/WebGLClient.framework.js:16977
    removeRunDependency http://127.0.0.1:8080/Build/WebGLClient.framework.js:991
    receiveInstance http://127.0.0.1:8080/Build/WebGLClient.framework.js:1103
    receiveInstantiationResult http://127.0.0.1:8080/Build/WebGLClient.framework.js:1110
    (Asynchrone : promise callback)
    instantiateAsync http://127.0.0.1:8080/Build/WebGLClient.framework.js:1130
    (Asynchrone : promise callback)
    instantiateAsync http://127.0.0.1:8080/Build/WebGLClient.framework.js:1128
    createWasm http://127.0.0.1:8080/Build/WebGLClient.framework.js:1149
    unityFramework http://127.0.0.1:8080/Build/WebGLClient.framework.js:14006
    loadBuild http://127.0.0.1:8080/Build/WebGLClient.loader.js:1052
    (Asynchrone : promise callback)
    loadBuild http://127.0.0.1:8080/Build/WebGLClient.loader.js:1051
    createUnityInstance http://127.0.0.1:8080/Build/WebGLClient.loader.js:1095
    createUnityInstance http://127.0.0.1:8080/Build/WebGLClient.loader.js:1080
    onload http://127.0.0.1:8080/:71
    (Asynchrone : EventHandlerNonNull)
    <anonyme> http://127.0.0.1:8080/:70

So the problem seems to happen while deserializing my connectedPlayers message (which is, indeed, the first message I shall receive).

The quick and dirty fix I described in the original post was to change line 200 of Assets/Colyseus/Runtime/GameDevWare.Serialization/Metadata/MetadataReflection.cs file to this:

if (/*AotRuntime ||*/ type.IsAbstract || type.IsInterface)
    return false;

By doing so, the application now works on a WebGL platform.

Hope it helps! :) Ping me if you need any more details, I wrote this very quickly

xhacker5000 commented 1 year ago

I like dirty fix :) And yes , it's works for me thanks @sticmac

yty commented 1 year ago

Hi everyone, I'm having the same problem.

if (/*AotRuntime ||*/ type.IsAbstract || type.IsInterface)
    return false;

Doing so can fix the problem of WEBGL in Ios. But it still doesn't work in Mac system (safari).

etherny commented 1 year ago

if you can target sub .net version it can fix your issue too. I'm having the same issue, and i juste downgrade .net to 2.1 (but keep il2cpp enabled)

image

i'm using package manager to import colyseus, then i cannot change the MetadataReflection.cs file.

etherny commented 1 year ago

Same issue here, colyseus onMessage room method not supporting custom class with il2cpp. Workaround used : Send your message as string (use JSON.stringify) and deserialize it manually in the onMessage (with JsonUtility.FromJson). No modification needed in colyseus package.

workaround c# client side code

public static void OnStringMessage<MessageType, S>(this ColyseusRoom<S> room, string type, Action<MessageType> handler) where S : Schema
{
    room.OnMessage<string>(type, (messageString) =>
    {
        var message = JsonUtility.FromJson<MessageType>(messageString);
        handler(message);
    });
}
// Usage
room.OnStringMessage<MessageType, State>("message", callback);
etherny commented 1 year ago

Look like assembly issue because i can use Colyseus classes as custom message.

Here an exemple with lobby room message

room.OnMessage<List<ColyseusRoomAvailable>>("rooms", onRooms);

sticmac commented 1 year ago

Sorry for pinging @endel but this issue is still tagged as Missing Minimal Reproduction and hasn't seen real progress for months as far as I know. Do you need more information than what I've provided in my third message?

deniszykov commented 1 year ago

Hi @sticmac. Did you tried solutions from #135?

sticmac commented 1 year ago

Hi @deniszykov, @endel already mentioned them indeed. But as stated in a previous message, at the time I raised this issue, I was using Unity 2021.3.2f1 LTS and the managed code stripping option was activated and set to minimal level.

The only working fix I've been using since then was to comment AotRuntime as stated in my second message here.

deniszykov commented 1 year ago

Yep it is a bug, AotRuntime check should be few lines down. I will make PR.

endel commented 1 year ago

Thanks to @deniszykov's PR (https://github.com/colyseus/colyseus-unity-sdk/pull/210) this has been finally fixed on latest version (0.14.21)