doubledeez / mdframework Goto Github PK
View Code? Open in Web Editor NEWA multiplayer C# game framework for Godot 3.4 Mono.
Home Page: https://discord.gg/UH49eHK
License: MIT License
A multiplayer C# game framework for Godot 3.4 Mono.
Home Page: https://discord.gg/UH49eHK
License: MIT License
After my refactoring network master can't be swapped since we buffer this information instead of checking every update.
public virtual bool ShouldReplicate()
{
// Note this means we don't support nodes swapping network master.
// If that should be supported then CheckIfShouldReplicate has to be run every update.
return IsShouldReplicate;
}
It would be good to provide a solution that supports buffering but also allows for switching of network master. Perhaps we can add a node extension along the same line of SpawnNetworkNode() that is called something like ChangeNetworkMaster(int PeerId) which would simply change the network master of any node you call it on.
The readme lists what you need to add to your .csproj file, however this list is not complete. As such anyone simply copying this list will just get a bunch of errors.
I also had a problem with the MDNetMode enum in the MDTypes class, for some reason godot didn't pick it up even if I included this file. As such I simply moved this enum to MDNodeExtensions as that seemed to be where it fit the best.
Here is my list from my .csproj file, note that this does not contain MDTypes as I removed it.
<Compile Include="MDFramework\MDAttributes\MDAutoRegister.cs" />
<Compile Include="MDFramework\MDAttributes\MDBindNode.cs" />
<Compile Include="MDFramework\MDAttributes\MDCommand.cs" />
<Compile Include="MDFramework\MDAttributes\MDReplicated.cs" />
<Compile Include="MDFramework\MDExtensions\MDControlExtensions.cs" />
<Compile Include="MDFramework\MDExtensions\MDNodeExtensions.cs" />
<Compile Include="MDFramework\MDExtensions\MDVector2Extensions.cs" />
<Compile Include="MDFramework\MDExtensions\MDVector3Extensions.cs" />
<Compile Include="MDFramework\MDHelpers\MDArguments.cs" />
<Compile Include="MDFramework\MDHelpers\MDInput.cs" />
<Compile Include="MDFramework\MDHelpers\MDCommands.cs" />
<Compile Include="MDFramework\MDHelpers\MDLog.cs" />
<Compile Include="MDFramework\MDHelpers\MDProfiler.cs" />
<Compile Include="MDFramework\MDHelpers\MDStatics.cs" />
<Compile Include="MDFramework\MDInterface\MDConsole.cs" />
<Compile Include="MDFramework\MDInterface\MDInterfaceManager.cs" />
<Compile Include="MDFramework\MDNetworking\MDGameSession.cs" />
<Compile Include="MDFramework\MDNetworking\MDPlayerInfo.cs" />
<Compile Include="MDFramework\MDNetworking\MDReplicator.cs" />
<Compile Include="MDFramework\MDGameInstance.cs" />
I've played quite a bit with this framework and would like to share some thoughts and ideas which would be really helpful in that framework.
IsConnected
boolean in GameSession
, or GameInstance
(just somewhere where you can get it form anywhere like these)
This was a bit of a struggle since i didn't know that GameSession.IsSessionStarted
means that he successfully connected. This should be either renamed, or left like that but this bool doesnt mean that player is synched. Would be a great idea to combine GameSession.IsSessionStarted
and some boolean about synch to know if we were connected and synched first time.
Would be also nice to make event after started and synched session.
PlayerCount
This is needed sometimes too. What i've done, is inherited from MDGameInstance
and exposed Player.Count with a function. Really simple, but very helpful
Getting player info before it leaves
MDFramework/MDNetworking/MDGameSession.cs
Lines 291 to 295 in 3a72a95
MDFramework/Examples/BasicNetworkLobby/BasicNetworkLobby.cs
Lines 118 to 123 in 3a72a95
public class MyMDGameSession : MDGameSession
{
public event PlayerEventHandler OnPreparePlayerInfoForRemoval = delegate { };
protected override void PreparePlayerInfoForRemoval(MDPlayerInfo PlayerInfo)
{
OnPreparePlayerInfoForRemoval(PlayerInfo.PeerId);
}
}
(might be a good idea to send not just peerid, but a full info there, it was just for example)
And then used this MyMDGameSession
with cast from getting this.GetGameSession()
MDFramework/MDNetworking/MDGameSession.cs
Lines 86 to 88 in 3a72a95
MDFramework/MDNetworking/MDGameSession.cs
Lines 113 to 115 in 3a72a95
peer.ServerRelay = param;
, on 87 and 114 line above, where param
is bool passed into a function.We should add more tests (demos) to test everything added in #17 and all features that existed before #17.
Currently there are a three constants that are not configurable in the GameClock but they should be.
///<summary>If we are calculating offset from ping this is the minimum offset we can have</summary>
public static readonly int MINIMUM_OFFSET = 5;
///<summary>We add some extra buffer to the offset just in case</summary>
public static readonly int OFFSET_BUFFER = 5;
///<summary>Used to control how much we are allowing to be off by for the tick offset</summary>
public static readonly int MAX_TICK_DESYNCH = 5;
This could be fixed in the scope of #12
Instead of OneShot mode for [MDReplicated] attribute instead we will add a custom MDRpc that can use the game clock and ensure the message arrives at the correct time. This is a followup to #17
Now that we split the MDReplicatedMember into a class that we can subclass it would be possible to add a member that would allow for easy replication of ICollection. That way we could replicate generic lists across the network without much overhead.
I would suggest we introduce a new replicated member called MDReplicatedICollection, this replicated member handles all the logic regarding checking for added / removed stuff from the list. Then we add a MDReplicatedICollectionContentHandler that will handle the actual replication, insertion, etc..
The reason I want the handler is because if we introduce the parameters mention in this suggestion we could allow people to pass in their own handlers if they have a list of some custom type.
We could also introduce a handler that default works on KeyValuePair<object, object> which would then work for dictionaries.
In theory buffered values could now last for the duration of the connection if something ends in the buffer but the node never arrives. We should add a date stamp to each buffer for when the latest message arrived, if a certain amount of time expired (could be something like 2 minutes~) then clean up the buffer. This is unlikely to occur so maybe we should consider not adding this at all since it will result in extra processing time spent to check quite often. Is this extra processing time worth the potential memory leak for the duration of the session?
When a player first joins a server there's generally some initialization that needs to occur both from the player and from the server. (eg. authenticating with a platform, sending desired user name or character, etc).
To help facilitate this flow, MDPlayerInfo should provide hooks (virtual methods) for users to add this initialization logic and an Event on MDGameSession should be fired to notify once initialization has completed (or failed).
cc @Meister1593
Currently we only have MDCRMInterpolatedVector2 which is our Vector2, it would be good to also add an interpolated Vector3. If any functionality is duplicated between the two then it should probably be moved into another class that both the interpolated Vectors can inherit from.
Should include information on:
The MDReplicatorGroupManager currently has very simple logic for balancing the updates between frames. What could be needed is some more advanced balancing logic and possibly rebalance logic.
Also is it worth it to allow users to set custom group names? It seems like allowing this just makes balancing harder, if we don't have group names any form of balancing we introduce could be much more efficient. I know I was the one who introduced the concept but now after thinking about it I am questioning the value of it.
In MDGameSession.GetOrCreatePlayerObject(int PeerId) we have this line,
string PlayerName = String.Format(PlayerNameFormat, PeerId);
This variable is never used, simply assigned. This of course is not a big deal, however I would suggest that this should be added to the MDPlayerInfo by default as most multiplayer games require a name, this could also serve as a good example on how to replicate values.
Personally I added the following to my MDPlayerInfo:
private String _playerName = "UnkownPlayer";
[MDReplicated]
public string PlayerName
{
get { return _playerName; }
set {
_playerName = value;
this.GetGameSession().NotifyPlayerNameChanged(PeerId);
}
}
In addition I added a new event to the MDGameSession,
public event PlayerEventHandler OnPlayerNameChanged = delegate {};
public void NotifyPlayerNameChanged(int peerId)
{
OnPlayerNameChanged(peerId);
}
I think this is something that almost every multiplayer game needs and it also serves as a simple example on how you can replicate values and send out a notification.
Finish the replication group manager, this is a followup from #17
The MDInterfaceManager will introduce the concept of a Screen and ScreenLayers.
A screen is a fullscreen collection of Controls and exists on a ScreenLayer.
A Screen is itself a control, with the addition of a Close method that calls into the interface manager.
The Interface Manager has two (at this time) screen layers: Main and PopUp.
A screen layer is a stack of Screens. For the Main layer, only the top screen in the stack is visible, when that screen closes, the screen below it is revealed. If a new screen is opening on the Main layer, the previous top screen is hidden.
For the PopUp layer, all screens in the stack are visible, layered on screen in the same order they were added to the screen layer stack.
With PR #60 the wiki is now outdated and needs to be updated to inform users on how to configure settings
https://github.com/DoubleDeez/MDFramework/wiki/Game-Instance
To me it seems there is something wrong with the console currently, I haven't had time to look into it properly yet but I am guessing there is an infinite loop somewhere.
It appears to happen whenever I try to execute a command in the console, the entire game just freezes. I might spend some time on later myself if I find I need the console.
We should add an option to MDProfiler to write profiler results to a csv file for ingestion to other software.
To not interfere with profiling, file writing should be done at the end of the frame and adding the profile data to the buffer should be quick.
This is just a hunch, but I'm betting that because of #19, there will likely be a similar issue with spawning many networked nodes in a short period, so sending them over the network should be frame sliced
Godot version:
3.2.1 Mono release 64 bit
OS/device including version:
Linux 5.6.7.arch1-1, Arch rolling,
mono 6.4.0,
mono-msbuild 16.5.xamarinxplat.2020.01.10.05.36-1
Issue description:
When trying to launch server in _Ready method with autoloaded MDGameInstance as GameInstance, tree element in MDGameSession class on 87 line is null
Commented zone bellow is where i checked if this occurs when MDGameInstance attached to node and called after some time (it creates server just fine)
Test spatial node has attached test.cs script
Currently the GameClock sets the remote offset to be equal to the ping of the player with the highest ping. It would be nice to have a second mode where it calculates a remote offset per player.
The solution to this would be as follows:
Eg.
P1 => Ping : 100
P2 => Ping : 150
P3 => Ping : 200
In this scenario maximum ping would be
P1 => P1 + P3 = 300
P2 => P2 + P3 = 350
P3 => P3 + P2 = 350
I made a small example to test out my own code for my game clock with 10 moving objects that just rotated in a circle, for testing I used the Replicator on their position and scale. This caused each of the objects to pretty much spam one replicaiton call every frame. I also used clumsy to set 600 ping between the clients. Which I think hit some sort of limit because my clients started getting really bad ping to each other and packets were getting dropped.
I think a solution would either be to say that each value can only be replicated so often or perhaps a replication general cap where if you try to replicate more than X values in Y time you will get throttled and get errors in the log.
There seem to be a limit to how many calls you can do per second and we should probably make sure the framework respects this.
Right now, some logic is intended to be server-relayed (for example, synching player info)
If i disable server-relay, it would work work as intended (thought, one error would be thrown into log when new player connects on server with at least one host and 1 player)
Is there a plans possibly to implement that feature?
Server authoritative setups are quite often used for example, in competitive games, where cheating would ruin entire game, so relaying RPC over server to clients automatically is not thing that you want to do in that case.
The new MDList feature brought with it the IMDDataConverter interface, this interface is used so that you can have a list of any type you want including custom classes as long as you provide a data converter. A default converter is provided that supports the basic godot types that do not require conversion.
In addition the MDReplicatedCommandReplicator also has support for OnValueChangedEvent which was introduced when the MDClockedReplicatedMember was introduced.
So right now the support for these features look like this:
I think this is confusing and I think most of this functionality should be moved into the MDReplicatedMember if possible so all classes that inherit can support the same functionality.
However to do this we will need one big change, currently MDReplicatedMember does replication by calling Rset and RSetUnreliable directly on the node, all other replicators send their messages through the MDReplicator.
Code from MDReplicatedMember
///<summary>Replicate this value to all clients</summary>
protected virtual void ReplicateToAll(Node Node, object Value)
{
MDLog.Debug(LOG_CAT, $"Replicating {Member.Name} with value {Value} from {LastValue}");
if (IsReliable())
{
Node.Rset(Member.Name, Value);
}
else
{
Node.RsetUnreliable(Member.Name, Value);
}
LastValue = Value;
}
The MDClockedReplicatedMember on the other hand sends it through the replicator
///<summary>Replicate this value to all clients</summary>
protected override void ReplicateToAll(Node Node, object Value)
{
MDLog.Debug(LOG_CAT, $"Replicating {Member.Name} with value {Value} from {LastValue}");
if (IsReliable())
{
Replicator.Rpc(REPLICATE_METHOD_NAME, Replicator.GetReplicationIdForKey(GetUniqueKey()),
GameClock.GetTick(), Value);
}
else
{
Replicator.RpcUnreliable(REPLICATE_METHOD_NAME, Replicator.GetReplicationIdForKey(GetUniqueKey()),
GameClock.GetTick(), Value);
}
LastValue = Value;
}
The MDReplicatedCommandReplicator is slightly different
protected void ReplicateCommandToPeer(object[] Command, int PeerId)
{
Replicator.RpcId(PeerId, REPLICATE_METHOD_NAME, Replicator.GetReplicationIdForKey(GetUniqueKey()), GetGameTick(), Command);
}
To allow support for data converters and for the value change event we would need to modify the MDReplicatedMember to send its messages through the replicator just like MDClockedReplicatedMember does. In addition it should be noted that the MDReplicatedCommandReplicator supports both GameClock and no-GameClock modes in the same class. This is would also be possible for MDReplicatedMember if we send the commands through the MDReplicator.
I propose that we redo the replicated members so we end up with the following end result
MDReplicatedMember
MDRInterpolatedVector2
MDReplicatedCommandReplicator
MDList
Example of how a class could be it's own data converter and thus be replicated. You can note there is no need to set the data converter setting since the interface is implemented directly in the class. This change would need to be made for the MDList as well.
[MDReplicated]
protected MyObject CustomObject = new MyObject();
[MDReplicated]
protected MDList<MyObject> ObjectList;
public class MyObject : IMDDataConverter
{
public string Value1 { get; set; }
public int Value2 { get; set; }
public bool Value3 {get; set; }
public object[] ConvertToObjectArray(object item)
{
MyObject obj = item as MyObject;
return new object[] { obj.Value1, obj.Value2, obj.Value3 };
}
public object ConvertFromObjectArray(object[] Parameters)
{
MyObject obj = new MyObject();
obj.Value1 = Parameters[0] as String;
obj.Value2 = Convert.ToInt32(Parameters[1]);
obj.Value3 = Boolean.Parse(Parameters[2].ToString());
return obj;
}
public int GetParametersConsumedByLastConversion()
{
return 3;
}
}
So there are a few things that we want and maybe want to add to MDReplicated, and there will probably be more in the future.
My suggestion to solve all of these is that instead of adding more attributes to [MDReplicated] we instead just add a parameter array.
public MDReplicated(MDReliability InReliability = MDReliability.Reliable,
MDReplicatedType RepType = MDReplicatedType.OnChange, params object[] parameters)
Then we could add flags in the form of a parameter enum to MDReplicatedMember and all inheriting classes, which would look something like
public enum Parameters
{
OnChangeListener,
ProcessWhilePaused
}
Then people can just pass in stuff as parameters pairs,
[MDReplicated(MDReliability.Unreliable, MDReplicatedType.Interval,
MDReplicatedMember.Parameters.OnChangeListener, (Action<object>)OnPositionChanged,
MDCRMInterpolatedVector2.Parameters.Prediction, false)]
// Alternative version
[MDReplicatedSetting(MDReplicatedMember.Parameters.OnChangeListener, (Action<object>)OnPositionChanged)]
[MDReplicatedSetting(MDCRMInterpolatedVector2.Parameters.Prediction, false)]
We then just process the parameters we know about and pass the parameters up the inheritance tree after we processed our own. I think the good thing about something like this is that it would allow each custom implementation to pick up their own parameters. We could even introduce some in the MDReplicator itself if we need (maybe pause processing goes there).
The downside is that this is less explicit, upside would be very easy to extend with new options. Also I feel like most of the options here are very custom options that you don't need to know about just to use the framework.
Another thing we could in theory do if we introduce this on the MDReplicator as well is to allow people to pass their custom MDReplicatedMember as a parameter instead of having to overwrite the MDReplicator itself.
I have made another suggestion here that could need something like this.
Setup a repo that framework changes can be tested against. This will allow us to run CI against it.
This repo will also be a source for examples on how to use the framework.
PredictiveSynchronizationExample is currently not working. A quick initial investigation leads me to believe something is going wrong during replication of members for the actor class. I did check the replicator and we are sending out Rset calls to the PredictiveActor but the variables are not being updated.
I think this is because the client recieves updates for some future game tick but since the game is paused we never get there. The solution is probably to put the replicator into a special state during initial synchronization so that game ticks are ignored during initial synchronization
The PredictiveActor implements the IMDSynchronizedNode
interface. The implementation blocks synchronization until all of the networked member variables have been updated.
public bool IsSynchronizationComplete()
{
if (Speed == 0f || Direction == Vector2.Zero)
{
// Fake that we are taking variable time to synch
FinishSynchAt = OS.GetTicksMsec() + (uint)Random.RandiRange(500, 8000);
return false;
}
if (OS.GetTicksMsec() < FinishSynchAt)
{
return false;
}
return true;
}
Issue #62 introduced data converters to the MDReplicatedMember, it also introduced a generic data converter that can convert any type of class.
However currently recursion is not supported, so it is impossible to have a class with another replicated class inside. It is also impossible to have a MDList with another MDList inside it. Nor can you have a MDList inside a replicated class.
This will require some more refactoring and wider changes to the system.
Currently most network methods use a params object[] Parameters
to send data across the network. The MDList and the data converters send their data as index + value pairs.
[0] = <index>
[1] = <data>
[2] = <index>
etc...
The most logical way to fix this would be to modify the sending so the index part also contains length of the data. So something like,
[0] = <index>#<length>
[1] = <data>
[2] = Data
etc...
In addition we also need to make member registration recursive and more generic. MDList, class data converter should support further converters / MDList inside them. For this we may need to implement some kind of new interface or data structure. We should also take care so we only send the data that is changed.
Eg. If we got something like this MDList<MDList<CustomClass>>
, when a value in a custom class in the inner list is changed only the change for that value should be sent. That means the message may look something like this
[0] <index for outer MDList>#3
[1] <index for inner MDList>#2 // This would be the data from previous line
[2] <index for CustomClass member that changed>#1
[3] <value for the CustomClass member>
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.
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.
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.
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.
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.
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.
Would be a good idea to add option for non-automatic synchronization trigger.
For example: Host sits on server, another joins. When it joined, now it would automatically start synchronization.
With that feature, i can launch synchronization when i want.
This needed for example when you have to do local stuff before actual synching and you need to make sure that you are actually connected to server right now.
So for my game I want to try to figure out what game time each client is at relative to the server.
There is an OS.GetTicksMsec() which returns time since the game was started. Now my idea for this is to introduce a new subnode or class that I would add to MDPlayerInfo which has two primary features.
Optionally to ping the client at regular intervals so you can check what the ping to the client is easily. This may allow you to optimize your usage of the second feature.
When a client joins it would ping messages back and forth where the server would request the clients OS.GetTicksMsec() repeatedly and see how long it took to get it. Then it will try to figure out by using average (and removing outliers) what the offset between the client and server OS.GetTicksMsec() is.
The idea for this is that then it would be possible to send a message from the server in advance that something should be done on all clients at an exact time and it would be executed at roughly the same time on all clients.
Eg. Something like Rpc("do_action", playerinfo.GetClientTicksMsec()+500).
Using C# reflection, it should be possible to create a SaveGame system that requires near-zero boilerplate to use.
It should be built in a way so that it could be extended to support various platforms cloud save systems.
File writing should be able to be done async.
I went through and compiled a list of all virtual methods that are used for settings because I plan to add them to the readme when I add the MDGameSynchronizer stuff.
MDGameInstance
GetGameSessionType() -> Return the type for MDGameSession
GetGameSynchronizerType() -> Return the type for MDGameSynchronizer
GetPlayerInfoType() -> Return the type for MDPlayerInfo
* IsConsoleAvailable() -> Returns if console is available (Default: Only in debug mode)
* UseUPNP() -> Returns if UPNP should be used (Default: True)
* RequireAutoRegister() -> Returns if MDAutoRegister is required (Default: False)
* GetConsoleKey() -> Returns the console key (Default: QuoteLeft)
* UseGameSynchronizer() -> Decides if the network synchronizer is used or not (Default: True)
* GetLogDirectory() -> Get the log output directory (Default: user://logs/)
MDGameSession
! UseSceneBuffer() -> If true we will keep a reference to all loaded scenes around so we don't need to load the resource from disc every time
MDGameSynchronizer
* IsPauseOnJoin() -> Pauses the game for synching on player join (Default: True)
* GetUnpauseCountdownDurationMSec() -> Unpause countdown duration (Default: 2 seconds)
* GetPingInterval() -> How often do we ping each client (Default: Every second)
* GetPingsToKeepForAverage() -> Pings to keep for getting average (Default: 10)
* IsActivePingEnabled() -> If set to true we will ping every player continuously. (Default: true)
* GetInitialMeasurementCount() -> This decides how many times we go back and forth to establish the OS.GetTicksMsec offset for each client (Default: 20)
I looked at the different virtuals in there and a lot of them really should not change durting runtime (Marked with *). As such I wondered if it would be an idea to move such settings into a properties file instead. I think this would make it easier to do configurations and it would reduce amount of code as the amount of properties grow.
Optionally we could also load it from a properties file and give access to changing properties by name during runtime. That way you could toggle things from code, maybe something like
this.ModifyMDFrameworkProperty("UseUPNP", false);
An additional benefit here is that you can create different property files for your different builds. We could do a virtual GetPropertiesFilePath that would by default provide a different file for debug/release builds.
Scene Buffer
The first thing I noticed is that the MDGameSession.UseSceneBuffer() should probably move over to MDGameInstance to ensure you only need to override one class for configuration.
Secondly I am not sure if the solution for this one would be to provide the path to the scene in the virtual method. Then you can decide if scene buffer should be used on a scene by scene basis. The only thing I see as a problem here is that doing string compares during runtime is not exactly fast.
The new implementation of MDRset/MDRpc that uses the game clock has to resolve the method/member every time a call is made. This is slow and it would be good to avoid this or make it faster.
There are two potential solutions,
Look into using something that speeds this up such as FastMember
All methods and members should be attributed with one of godot's keywords [Master], [Puppet] etc.. We could scan for all of these when the networked node is added to the replicator and then add them to a hashmap. Key could be same as the key used for the existing lookup map which is <node path>#<member name>
.
We would then only have to look up members if they for some reason do not exist in this map and we can then buffer them for future calls.
Personally I am leaning towards option 2, even though combining it with option 1 might also be worth it since we do need to do this every time a networked node is added.
There are quite a few features in the framework that do a lot of memory allocations, looping, and other processing. It would be a good idea to get metrics on the runtimes of the various features to make sure the framework's performance is in a good state and don't break down in larger games.
Once a workflow for this has been established it should be applied to any other features we add in the future.
I would suggest setting up a discord for the MDFramework, Godot already has it's own discord server so it would be logical to be on the same platform.
The reason I think this will be a benefit is as more and more people are picking up the framework we don't end up with a bunch of bugs reports here that are just from user error. A discord server would allow people who has more experience with the framework to help new people in real time instead of getting a new bug report every time someone needs help.
That being said if we don't mind bug reports that may not be real bugs on here then a discord server may be redundant.
I noticed while making the synchronizer that the MDPlayerInfo is created, then other clients are notified that it is created followed immediately by asking the MDPlayerInfo to synch itself to other clients.
If for some reason the create player info packet drops or is delayed and arrive after the synchronization the game could end up in a desynchronized state. This can also happen for network nodes as replication starts immediately and if a value is set to OnChange and only changed one time it may never reach the clients. RSet just throws an error if the node doesn't exist and never retries as far as I know.
You can replicate this kind of behavior with Clumsy and a high packet drop rate and high out of order rate.
I think there are two solutions to this problem
For this we would supply our own secure versions of all the Rpc and Rset calls and also make the replicator use those. The calls would go through our own class that supports retry and will store any call that fails for a set amount of time. Then we hook into node created and if the node is created we would send all buffered calls.
Personally I think this would be the perfered solution as no callbacks are needed, as such the properties would be set as soon as possible.
Second option also includes custom Rpc and Rset except in this instance we implement a callback through the GameSynchronizer so the server is notified when an instance is created on the client. Then the server would be the one to buffer any commands and only send them once the node exists on the client.
The tricky thing here would be to handle things like nodes that exist in the game from the start and if some child node of an instanced network scene is registered. We can probably solve this by checking the hierarchy to see if a parent is a networked scene but it is something to consider.
So I am still not sure why this is happening and I am currently trying to figure it out.
I have the following piece of code to spawn a player character for each player when they join my game. (The first part just sets the name after joining)
private void OnPlayerJoined(int PeerId)
{
// Set our name
if (!MDStatics.IsNetworkActive() || _session.GetPlayerInfo(PeerId).IsNetworkMaster())
{
_session.GetPlayerInfo(PeerId).PlayerName = _txtPlayerName.Text;
}
if (_session.IsMaster())
{
PlayerSpawnPoint point = (PlayerSpawnPoint)GetTree().GetNodesInGroup(ManagerBase.GROUP_SPAWN_POINT)[0];
this.SpawnNetworkedNode("res://Scenes/Characters/PlayerCharacters/Elf/PlayerElf.tscn", "Player" + PeerId, PeerId, point.GetGlobalSpawnPosition().To3D());
}
}
As you can see in the screenshot below a second node appears on the client with a suffix of @38, this causes a bunch of errors to be thrown on the server as the MDReplicator tries to replicate the values of this node as well. Which of course does not exist on the server.
It should be enabled by default and be able to be disabled.
So I finished the second part of the Synchronizer now which is the game clock, it has a tick counter that ticks on all network clients at the same speed and it automatically keeps all clients in synch. I have added some questions at the bottom.
How it works
The game clock uses the maximum ping of any player to decide how far behind network objects should run. There is a video here, just pause at any time and you can see that the top value for the current tick is almost in synch for both clients.
You can also see that the remote offset automatically adjusts as ping increases / decrease. Ensuring that remote objects are not further behind than they need to be.
Now my idea is to add two more things that use this,
MDClockedNetworkDataNode
Which will be a script you can just attach to a node on your networked objects. By default it has an interval for how often it sends updates. It allows you to add / get MDClockedNetworkProperty.
MDClockedNetworkProperty
A property that can be either Interpolated or a one shot thing. You can also set if it is always sent along with the other properties in your MDClockedNetworkDataNode or if it is sent only when you trigger it. Can be overwritten for custom properties
The idea here is that you would add this to your player and maybe add some properties like,
Position (Vector)
Rotation (Vector)
Shoot (bool) (manually triggered, one shot)
Then the MDClockedNetworkDataNode will send updates for position and rotation along with game tick every time it updates. On the clients when you fetch the value it will automatically be interpolated for you based on the game ticks.
Then for the Shoot property you can check it every frame and it will only return true on the game tick where it happened (or first game tick that is processed after this tick).
The idea here at least for my own game is to use this to ensure all networked entities synch up properly and that events are triggered at the correct time / location. Since we are interpolating between ticks and events happen at any given tick it should in theory happen exactly the same on all clients (albeit a bit behind for networked clients, based on ping).
I expect to have everything done later this week sometime.
Questions
Is this something you think would fit in the framework?
Does it conflict with your interpolation changes?
If you think it fits, do you think the idea for MDClockedNetworkDataNode and MDClockedNetworkProperty is fine or would you rather implement this part differently?
I added an onscreen debugger to my own fork of MDFramework,
This is an example of how it works, it is very easy to use. By default I bound it to F12 in my fork.
AddOnScreenDebugInfo("FPS", () => Engine.GetFramesPerSecond().ToString());
AddOnScreenDebugInfo("Static Memory", () => MDStatics.HumanReadableSize(OS.GetStaticMemoryUsage()));
AddOnScreenDebugInfo("Network Active: ", () => MDStatics.IsNetworkActive().ToString());
AddOnScreenDebugInfo("PeerId: ", () => MDStatics.GetPeerId().ToString());
Is this something that would fit into the framework? I personally think it is very useful for printing information to the screen during runtime.
The GameClock interpolated Vector2 does not currently support prediciton.
Adding prediction would be fairly simple, simply calculate the velocity based on the last two updates every time we update the vector and if we stop recieving updates we can simply apply the same velocity every tick.
One thing that may be needed is a state for the MDCRMInterpolatedVector2 so we know if we are in initial update mode, interpolation mode or prediction mode.
Question is how long should prediction apply? I would suggest not allowing prediction to last more than 1-2 seconds as after this it is likely that the player is disconnected. It is probably best to stop the player so they don't activate triggers far off in the distance if they keep sliding in the same direction.
Right now there's a mix of comment styles on Classes and Members
A standard should be determined and the project should be changed to adhere to the standard.
Examples from codebase:
///<summary>A synchronization class with three primary features
///<para>It tracks the ping to each client</para><para></para>
///<para>It tracks what the local OS.GetTicksMsec() is on each connected client relative to the server (server only)</para><para></para>
///<para>Finally it takes care of making sure all players are fully synchronized on request.
/// Usually when a player joins or level is changed</para>
///</summary>
///<Summary>This contains what we think is the GetTicksMsec offset for each player</summary>
/*
* MDGameSession
*
* Class that manages the current multiplayer state of the game.
*/
// Registers a new node to MDFramework systems
In the scope of #49 it is intended to implement the ability to disable server relay. Currently the GameSynchronizer has clients sending ping requests to each other, of course these bounce through the server anyway since there is no P2P communication.
Since these packets go through the server anyway we might as well not have the clients ping eachother. The reason they do ping each other is because we use the highest ping in the game to decide what the Remote Offset should be for the GameClock.
In short the remote offset decides how many game ticks behind remote signals are executed, 1 remote offset is 1 physics frame, ie. roughly 16 milliseconds. So if remote offset is 10 then all remote nodes are running 160 milliseconds behind for that client, this is to ensure we get a smooth behavior from all remote nodes so the data will be there before it is needed. Currently the remote offset is actually adjusted extra high because ping is for an entire roundtrip, half of ping time would be the minimum time you need to get remote signals. In addition we have an additional buffer added on top of the ping (20 % I believe).
So what do we do to solve this in the GameSynchronizer?
private void OnPingTimerTimeout(Timer timer, int PeerId)
{
// Check if peer is still active
if (!InternalPingList.ContainsKey(PeerId) || !MDStatics.IsNetworkActive())
{
MDLog.Trace(LOG_CAT, "Peer {0} has disconnected, stopping ping", PeerId);
timer.Stop();
timer.RemoveAndFree();
return;
}
// Send ping request
if (MDStatics.IsClient() || GameClock == null)
{
RpcId(PeerId, nameof(RequestPing), OS.GetTicksMsec());
}
else
{
uint ping = (uint)GetPlayerPing(PeerId);
uint estimate = GetPlayerTicksMsec(PeerId) + ping;
RpcId(PeerId, nameof(RequestPing), OS.GetTicksMsec(), estimate, GameClock.GetTickAtTimeOffset(ping));
}
}
If you look at this method you can see that clients send other peers ping requests, this should be removed (just disable the ping timer for clients and remove the MDStatics.IsClient()
). Then instead the server should calculate what the "highest ping" should be for each client and send it to the client. The highest ping would be <clients ping>+<highest ping of remaining peers>
.
Eg.
P1 => Ping : 100
P2 => Ping : 150
P3 => Ping : 200
In this scenario maximum ping would be
P1 => P1 + P3 = 300
P2 => P2 + P3 = 350
P3 => P3 + P2 = 350
This information could be sent along with normal ping packets and then we just need to ensure if you are a client when you are asked for max ping in GetMaxPlayerPing()
we return this value.
Godot version:
3.2.1 Mono release 64 bit
OS/device including version:
Linux 5.6.7.arch1-1, Arch rolling,
mono 6.4.0,
mono-msbuild 16.5.xamarinxplat.2020.01.10.05.36-1
Issue description:
When loading game with autoloaded or attached MDGameInstance it fails to load because AutoRegAtr is null and it crashes game.
Line 123 in MDGameInstance:
With simple null check it passes and launches game afterwards fine:
The README should be trimmed down to a short explanation on what the framework is, the features it has, and how to add it to your project. It should link out to the wiki for detailed info on how to use the various features
With PRs coming in it would be good to have CI to validate builds to prevent breaks from being merged in
This is another thing I plan on adding eventually but will mention here in case someone else want to do it or have a better idea.
As it currently stands the [MDReplicated] property is great when you want to just synchronize properties that are not changed often. Or a single property that is changed often.
However in cases for things like a player where a lot of data might be sent at regular intervals (say 10 times a second or so). Having one MDReplicated member for each value is highly inefficient as such I would like to suggest a new class that allows for compression of multiple values easily.
My initial idea for this was to simpl add something like MDNetworkCompressor then have functions like:
AddString(int key, MemberInfo member)
AddVector2(int key, MemberInfo member, int precision)
AddFloat(int key, MemberInfo member, int precision)
GetNetworkString()
SetNetworkString(String value)
GetFloatValue(int key)
etc..
Then the user would simply instance this class, add all their values to it with the compression level they want and finally introduce a MDReplicated string property where they just update with the GetNetworkString and then do SetNetworkString in the setter for this property.
However after looking at the code I think it might be possible to do this automatically, but I am not so sure. Adding a class that can do something like this would probably offer more control as it could be hard for the Replicator to know how often each replicated property is changed. The precision could be easily extended into the [MDReplicated] attribute.
Anyway this was just a passing idea I had that I know I will have to do at some point in the future. This is in no way urgent for my game so I probably won't put any work into this for quite a while. I just wanted to suggest this here to start a discussion on how this could be done.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.