Proposal: automatic state synchronization in the client-side

I'm in the process of making the state synchronization easier to apply in the client-side, since the usage of delta-listener often causes confusion. (colyseus.js#12, colyseus-unity3d#28, ...)

The idea is to use decorators to annotate which properties should be synched. The delta-listener will be used under the hood by the auto-sync tool.

To initialize the synchronization, you would provide the room instance, and the root instance holding the annotations. The initializeSync method would register all listeners automatically, based on the annotations.

let client = new Client("ws://localhost:2657");
let pongRoom = client.join("pong");

let game = new PongGame();
initializeSync(pongRoom, game); // registers all listeners automatically

Alright, so game is our root instance (of PongGame) holding the annotations. The names of the properties annotated in the client-side should be exactly the same as it is in the room state, in the server-side.

let addToStage = (app, player) => app.stage.addChild(player);
let removeFromStage = (app, player) => app.stage.removeChild(player);

export class PongGame extends PIXI.Application {
    @syncMap(Player, addToStage, removeFromStage)
    players: EntityMap<Player> = {};

    @syncObject(Ball, addToStage, removeFromStage)
    ball: Ball;
}

The annotations used here are @syncMap and @syncObject. Both of them accept a callback for when the object is created and removed. Unfortunately, it's not possible to use an instance method as an argument in the decorator, because this is the class scope at that point.

Alright, so we mapped the Player and Ball as child of PongGame. They need to sync its properties as well. We'll use @sync for direct data mappings.

export class Player extends PIXI.Graphics {
    @sync() x: number;
    @sync() y: number;
}

Given that Player is mapped through PongGame's players property, the sync tool will register listeners for "players/:id/x" and "players/:id/y".

What's interesting is that you can provide name aliases, and even use setters when synching data directly:

export class Ball extends PIXI.Graphics {
    /* mapping "x" property to "nextX" */
    @sync('x') nextX: number;

    /* mapping "y" property to "nextY" setter */
    @sync('y') 
    set nextY (value) {
        this._nextY = value;
    }
}

That's it for now. I still need to figure out how to deal with deeper objects, like "players/:id/items/:id/x" - which still doesn't work at the moment.

The API is still work in progress, feedback is very welcome! :rocket:
Cheers!

Update: almost ready to use!

I've also added a @listen method annotation. It's a shorthand for room.listen():

class Something {
    @listen("players/:id/score")
    doSomethingWithScore (change: DataChange) {
        console.log(change.path.id, change.value);
    }
}

And a very simple Pong game is playable here: https://colyseus-pong.herokuapp.com/
The sources are available here: https://github.com/endel/colyseus-auto-sync/

Next step is combining synching with client-prediction. This Heroku instance is in the U.S. and the gameplay feels very laggy for me.

Cheers! :rocket:

@endel Thoughts on how you would implement this in Unity?

Also do you have a good example of a first state sync of a complex state structure in Unity?

Hi @codrobin33, I haven't researched much how to do it using C#. I think it's possible to achieve a pretty similar API.

The initial state has been fixed recently on the JavaScript client - the callback of room.listen() is being triggered by deep additions. This is currently missing in the C# client. (colyseus-unity3d#31)

@codrobin33 listening to the initial state has been fixed for Unity today (v0.8.2)

@endel what a quick turnaround! thank you!

I will try out my project and see if this fixes what i was struggling with.

@endel hey sir im still struggling with initial sync, here is my setup.

Error i'm receiving:

Colyseus.Room.ParseMessage (System.Byte[] recv) (at Assets/Plugins/Colyseus/Room.cs:140)
Colyseus.Room.Recv () (at Assets/Plugins/Colyseus/Room.cs:73)
Colyseus.Client.Recv () (at Assets/Plugins/Colyseus/Client.cs:90)
RoomHandler+<Start>c__Iterator0.MoveNext () (at Assets/RoomHandler.cs:37)
UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at /Users/builduser/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

Here is my data from server:
0_1513310230592_Screen Shot 2017-12-14 at 9.53.52 PM.png

Code section its dying on (Room.cs) in plugins:

} else if (code == Protocol.ROOM_STATE) {
				byte[] encodedState = (byte[]) message [2];

				// TODO:
				// https://github.com/deniszykov/msgpack-unity3d/issues/8

				// var remoteCurrentTime = (double) message [3];
				// var remoteElapsedTime = (int) message [4];

				// this.SetState (state, remoteCurrentTime, remoteElapsedTime);

				this.SetState (encodedState, 0, 0);

			}

And finally, my start function

IEnumerator Start () 
    {
        client = new Colyseus.Client ("ws://localhost:2657");
        client.OnOpen += OnOpenHandler;
        client.OnClose += (object sender, EventArgs e) => room.Leave();

        yield return StartCoroutine(client.Connect());

        room = client.Join("game");
        room.OnReadyToConnect += (sender, e) => StartCoroutine ( room.Connect() );
        room.Listen ("messages/:number", this.OnMessageAdded);
        room.Listen ("players/:id", this.OnPlayerAdded);
        room.Listen ("shared/:map/:bases/:id", this.OnBaseAdded);
        room.OnJoin += OnRoomJoined;
        room.OnUpdate += OnUpdateHandler;

        room.OnData += (object sender, MessageEventArgs e) => Debug.Log(e.data);

        while (true)
        {
            client.Recv();

            // string reply = client.RecvString();
            if (client.error != null)
            {
                Debug.LogError ("Error: " + client.error);
                break;
            }

            yield return 0;
        }

        OnApplicationQuit();
    }

Have any initial thoughts?

Hey @codrobin33! Are you using the latest version of the server and client? Would be great if you could provide me an example project to reproduce the error. (btw it's recommended to report bugs on the issue tracker https://github.com/gamestdio/colyseus-unity3d/issues)

@endel I was looking at this in a tired state, but im pretty sure i had fully updated client and server. Ill double check what i was seeing and if i still have an error I'll open an issue on the github. Thanks for the response.