In this blog, I present a high-level overview about how we at Third Kind Games developed our NetCode using Amazon’s Lumberyard engine, to facilitate responsive and secure multiplayer experiences. Whilst there are several elements that go into the development of NetCode, today, I’ll be focusing on two components of Lumberyard that help us achieve this, namely, DataSets and RPCs.
From the start of development, we set out some technology goals that would define the direction we would take in order for us to be able to develop games that are responsive for players and resistant against cheating.
At its core, our technology is built around the Client-Server model. There is a central Server running somewhere and Client(s) connect and communicate with that Server for the Session of the game.
A key to this is that our entire code base is developed around the concept that the Server is authoritative over everything that happens in the game. That is, it is the Server that decides what happens in any particular session and for all intents and purposes, it is the real game that is playing out. The Server will accept the Client’s intent as to what action they wish to perform; the Server will then run the simulation itself and send back the results to the Client(s).
Of course, we still need to maintain a responsive game, so waiting for the round trip time of Server updates to come back before the Client does anything would be counter to this. With that in mind, our technology implements Client-side prediction whereby the Client is free to run its own simulation based on the user inputs. If there is a discrepancy between what the Server has simulated and what the Client has simulated then the Server data will override the data that is on the Client.
How then do we use Lumberyard to help us?
The Lumberyard engine ships with an amazing networking system called GridMate. It offers all the tools we need to implement the various pieces of NetCode that make up our technology.
Two (lower level) components of GridMate that we use in our NetCode are DataSets and Remote Procedure Calls (RPCs). Briefly, DataSets are used to synchronise state across a network automatically when the underlying value of the DataSet changes (one useful property of DataSets being that they guarantee eventual consistency). RPCs are used to send requests or messages to an authoritative node which can then be propagated back to all Peers if required.
Simple example use of DataSets and RPCs
The above example shows a movement request being instigated by a Client via the RPC. The Server receives this request and runs the simulation, after which the results are updated in the DataSets – this data is the authoritative view of the game. These DataSets become marked ‘dirty’ and are automatically propagated back to all connected Clients (including the source Client).
Two important things to note from this simplified example:
(1) Client #1 is making a request to the Server to move – This is important because the Client is not sending results as to what it thinks happened (aka a Trusted Client model) based on the user input.
(2) The results of the movement are being sent back to all Client(s) including the source Client – This is key to Client-side prediction. Whilst the movement request has been sent to the Server, the Client has (ahead of receiving the results back) performed its own simulation locally. On the source Client, when it receives these results, it will reconcile them. If the results match, all is well. If there is divergence, the Client will accept the Server results overriding its own. The other Client(s) receive the results for their peers and accept them by default. A hacked Client that decides not to accept Server results will do nothing – the Server continues to propagate its own, authoritative view of the game to everyone else.
Using DataSets and RPCs are extremely simple:
///////////////////////////////////////////// // DataSet and RPC Example GridMate::DataSet<float>::BindInterface < MyComponent, &MyComponent::OnNewDataCallback > m_myDataSet; GridMate::Rpc<>::BindInterface < MyComponent, &MyComponent::MyRpcCallback > MyRPC; /////////////////////////////////////////////
Example for DataSets and RPCs
On the DataSet line, we’re saying that the DataSet is a float type and when ‘m_myDataSet’ changes, the OnNewDataCallBack will automatically be called on all remote objects with the new data. For the RPC, it’s simply saying that when MyRPC is called on any client, the ‘MyRpcCallback’ will be called on the Server, and then can return true if the ‘MyRpcCallback’ should also be called on the Client(s).
I appreciate I’ve not gone into a tremendous amount of detail on this blog as to specifics of how things work or details around other elements of our NetCode (lag compensation/interpolation/ etc.). Please do leave a comment below if this is of interest.
Below though, are a few links to some excellent material on NetCode – please do check them out.