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 (ofPongGame
) 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, becausethis
is the class scope at that point.Alright, so we mapped the
Player
andBall
as child ofPongGame
. 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 throughPongGame
'splayers
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 forroom.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:
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.