DoubleDeez / MDFramework

A multiplayer C# game framework for Godot 3.4 Mono.
https://discord.gg/UH49eHK
MIT License
76 stars 13 forks source link

Player synchronization and examples (and tests kind of) #9

Closed Beider closed 4 years ago

Beider commented 4 years ago

So I plan on adding the following piece of code to my project because I need it for myself, I will write it to my branch and you can optionally pull it over once it is done. I am posting this here in case someone has better ideas on how to do this.

Player Synchronization

A common issue in network games is to ensure all players are synched, in particular if a new player joins the game in the middle of a game. I think a lot of this could be automated to this end I am going to add a few new features.

The first thing added to this end will be the player synchronizer class I outlined in another issue I posted here. In short this will allow the server to know the ping and approximate game time of the other players games.

Synchronizer

I intend to add a synchronizer class that will be instanced by the MDGameSession when the MDGameSession is created, it will be a root node just like MDGameSession and it's task will be to handle network synchronization on join / leave between players. Along with also tracking game ticks.

The first thing I will add is a GameTick tracker, this will work on the game delta time and be customizable with a default of 100 ticks per second. I think this will help a lot as rpc messages can then contain the tick the message was sent. This will make it easier for the other clients to display things properly. Say you recieve the following

[Tick100] Postion Vector2(10,10) [Tick110] Postion Vector2(10,20) [Tick116] Fire bullet + angle whatever [Tick120] Postion Vector2(10,30)

Without the tick it is very hard to know when the bullet should be fired, particularly since the position of this object will most likely be a bit behind in the first place. Another thing that could happen is something like this,

[Tick100] Postion Vector2(10,10) [Tick110] Postion Vector2(10,20) [Tick120] Postion Vector2(10,30) [Tick106] Fire bullet + angle whatever

If you have already moved past tick 106 you could always write code to figure out what the position was at tick 106 and spawn the bullet there, then add some additional velocity to make it catch up or simply calculate what position it would be at by the time you recieve the message

Of course you can send the position with your bullet but in general being able to know when in the other game simulation something happened would be incredibly useful.

Player join synchronization

The synchronizer will have a second feature, it will also track every networked node and member that is created through the framework and marked as important. To this extent I would add a new parameter to the SpawnNetworkNode called bool AddToNetworkSynchronizer which will be default true.

The synchronizer will have an option to pause all clients on player join, so if a player joins while the game is in progress. The synchronizer will use the ticks system to tell all existing players to pause on a given tick in the future. While this is happening the list of nodes and members that need to be synchronized will already have been sent to the new player.

Once the game is paused the synchronizer will ensure all players have the updated values for all networked objects and members. It will send progress messages back to the server which will distribute to all clients. I also plan to add a simple UI scene that is used for this (that can be overriden) that would show the status of synchronization.

Once all players are at the same tick and everything is synchronized the server will send a message to all clients to restart the game in a few seconds. For this the PlayerSynchronization class mentioned above will be used to ensure the players are as synched as possible.

I think adding this will let the framework pretty much keep all players mostly in synch automatically. Of course the synchronizer will also introduce a bunch of new events regarding synchronization and may even be extended to detect major desynchs and pause the game to allow everyone to synch up.

Examples (& kind of tests)

I wish to add an Example folder with a few examples.

Basic MP setup This will be the base scene used for all other examples. The idea here would be to create some simple UI that can host / join games. That way people who want to use the framework could simply inherit from this scene or copy this scene to get a quick start.

Synchronizer Predictive Example I will make a predictive actor that simply travels in a random direction and bounces off the edge of the screen. Then it will spawn in a lot of them and maybe also a bunch of inactive ones as well just to give the synchronizer more work. The idea here will be to test / demonstrate the synchronizer and how it can keep clients in synch without much actual work. If it all works out any joining client should be synched and see the exact same thing as the server.

Interactive Rejoin Example This demo will have a simple player character (godot sprite) where the player can set some properties on it (like scale, position, random color). When a player leaves I will save the data about the sprite and when another player join they will be asked if they want to join as one of the leavers or a new player. The idea is to show off how you can store data from disconnecting players and allow for rejoin.

2D Shooter Example Top down 2D shooter where players can join whenever they want and be synchronized into the game. The game will show off how to do a lobby, start the game, join in progress. Basic hit detection on both client / server side. And how to interpolate properties to keep players in synch.

Once a player wins a map a new random map will be loaded. It will show how you can have joining players spawn in on any map while the game is in progress.

I also plan to make this example into a youtube tutorial as I recently started a youtube channel to do devlogs and tutorials. here is one where I talk about the MDFramework a bit.

Open Questions / Problems

The main problem I am having is I want to introduce an easy way to say that you want ticks with some member. Say you have player position and you want it to be tracked with ticks. Maybe even keep a history of values that you can access from the code so you don't always have to write code to keep a history. It would be nice to make this as easy as possible.

I have not yet decided on the best way to do this. If there are any suggestions for this that would be great.

I am probably starting on this tomorrow, if anyone has suggestions or think my solution is utter garbage and have a better solution then please let me know.

DoubleDeez commented 4 years ago

I haven't fully read through this yet but I wanted to comment that I have a position synchronization class started for my own game that I'm planning to move to the framework. It supports prediction and interpolation based on the expected RPC rate but there's still a bunch of room for improvement.

References: https://www.reddit.com/r/godot/comments/8ch20v/help_with_node_sync_over_a_network/ https://gafferongames.com/post/snapshot_interpolation/

DoubleDeez commented 4 years ago

As for Join Synchronization, MDReplicator/MDGameSession should already handle that for you, it will update new players with the networked nodes and any MDReplicated properties.

Beider commented 4 years ago

Aye they do synchronize on join, however currently as far as I know there is no way for the client to know when synchronization is complete. Nor for the server.

The synchronizer I proposed would allow for the client and server to know when synchronization is completed. Also the game ticks I think could be useful in a lot of situation, particlarly if you are making a network game based on ticks (such as a real time game like transport tycoon or factorio, both of which work on ticks I believe).

That being said, it might be that this is not useful for the framework itself. If that is the case I would just implement this seperately for my own game as I wish to have these features.

Also I would be very interested in your sychronization class once you completed it, I am just about to start on the same thing for my game so if it is added to the framework I would be happy to help test it.

DoubleDeez commented 4 years ago

Ah yeah, the synchronization state is a bit of a special case. The framework sort of assumes that the game will continue during synchronization so there's no "complete" moment, it just sends everything continuously.

That's great, I'll try to get the PositionReplicator up in the next few days, it would be good to try out some other algorithms. The current one is okay but there's some noticeable jitter/rubber-banding.

Beider commented 4 years ago

So I have completed the first stage of this now locally, the game now pauses when another client joins. Makes sure every client is synched, then resumes at the same time.

Here is a video

In this video I got a dumb actor that actually recieve no further network signals except for the initial synchronized state of position, speed and direction to move. Then it just bounces off the edge of the screen. All clients stay completely in synch.

All that is left is to add a default graphical interface to show the synch progress (currently only logged) and then a timer that shows the countdown to game unpausing.

Here is how the log looks on join,

[2020-06-10 00:29:24.249][548][SERVER] [LogBasicNetworkLobby::Info] Player joined UnkownPlayer with PeerID 1397867656
[2020-06-10 00:29:24.259][548][SERVER] [LogBasicNetworkLobby::Info] Player changed name to UnkownPlayer
[2020-06-10 00:29:24.525][565][SERVER] [LogGameSynchronizer::Info] Msec response number 1 from peer [1397867656] is 3866 local Msec is 62324
[2020-06-10 00:29:24.531][565][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] recorded a ping of 275
[2020-06-10 00:29:24.540][565][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] recorded a estimated msec of -58335
[2020-06-10 00:29:24.550][565][SERVER] [LogGameSynchronizer::Info] Estimated OS.GetTicksMsec offset for peer [1397867656] is -58335 based on 1 measurements
[2020-06-10 00:29:25.009][594][SERVER] [LogGameSynchronizer::Info] Peer [1018386675] completed synch
[2020-06-10 00:29:25.015][594][SERVER] [LogGameSynchronizer::Trace] All clients are not synched yet
[2020-06-10 00:29:25.025][594][SERVER] [LogGameSynchronizer::Info] Peer [1058880107] completed synch
[2020-06-10 00:29:25.031][594][SERVER] [LogGameSynchronizer::Trace] All clients are not synched yet
[2020-06-10 00:29:25.143][602][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] has synched 0 out of 120 nodes
[2020-06-10 00:29:26.611][690][SERVER] [LogGameSynchronizer::Info] Msec response number 2 from peer [1397867656] is 5153 local Msec is 64409
[2020-06-10 00:29:26.619][690][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] recorded a ping of 2063
[2020-06-10 00:29:26.631][690][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] recorded a estimated msec of -58245
[2020-06-10 00:29:26.636][690][SERVER] [LogGameSynchronizer::Info] Estimated OS.GetTicksMsec offset for peer [1397867656] is -58290 based on 2 measurements
[2020-06-10 00:29:26.641][690][SERVER] [LogGameSynchronizer::Info] Peer [1397867656] completed synch
[2020-06-10 00:29:26.652][690][SERVER] [LogGameSynchronizer::Trace] All clients synched, sending unpause signal
[2020-06-10 00:29:26.658][690][SERVER] [LogGameSynchronizer::Trace] Unpausing game in 2

If we look in the client log for the clients you can see the unpause signal reach them at different times,

[2020-06-10 00:29:26.777][267][PEER 1058880107] [LogGameSynchronizer::Trace] Unpausing game in 1.881
[2020-06-10 00:29:26.777][376][PEER 1018386675] [LogGameSynchronizer::Trace] Unpausing game in 1.891
[2020-06-10 00:29:27.094][275][PEER 1397867656] [LogGameSynchronizer::Trace] Unpausing game in 1.616

[PEER 1058880107] unpaused at 00:29:28.658 [PEER 1018386675] unpaused at 00:29:28.668 [PEER 1397867656] unpaused at 00:29:28.710

So even though there was over 700 ms difference in signal arrival all peers unpaused within 52 ms of each other. The only reason the last client is so far off is because it just joined and I still didn't make the server wait for a higher confidence on it's TickMsec value (if you see in the server log we only had 2 values, it is pretty accurate around 20). Which I probably will do in which case I assume all clients will unpause within 10 ms of eachother regardless of ping.