CRDTs and Distributed Consistency - Building a P2P game
In part two of this series we improved the distributed counter built on the first part of the series to allow increments and decrements and an arbitrary number of clients to join the counter. In this part we will build a peer-to-peer game that uses the counter to keep track of key presses in distributed way.
This is the third part of a series of posts on CRDTs and Distributed Consistency. You can find previous parts on these links:
- CRDTs and Distributed Consistency - The simple distributed counter.
- CRDTs and Distributed Consistency - The complex distributed counter.
Building a peer-to-peer game using the PNCounter, WebRTC and React
The game will be simple, we will increment a distributed counter with the keyboard. The winner will be the client that has the greatest balanced count. I call a balanced count when the number of increments are equal to the number of decrements. So if User A increments 2 units and decrements 4, the user score will be 2. If User B increments 5 and decrements 4 the user score will be 4 and it will be the winner with a total score amount of 7, because 7 increments and 8 decrements happened in total if we count across clients.
The flow of the game is the following:
- User A joins, this generates a game Id.
- User A shares the game link with the rest of the participants.
- Each participants joins.
- User A starts the game.
- Each participant presses the left/right arrow keys as fast as possible to increase the score.
- Fifteen seconds later the games finishes.
- The winner is shown.
- You take note of the global score, reload the page and play a second round.
The user that accumulates the most amount of points in N=3 rounds is the winner. Pretty basic, but it will help us cover a lot of missing parts on the usage of CRDTs within a real context.
Extending the PNCounter to be used as an external React store
In order to use the counter with React we need to be able to notify React that changes to the counter happened so React can properly rerender when things change.
The first thing to do is to emit a
change event (and other events) when the PNCounter gets incremented, decremented or merged with the state from another agent. In the following example you can see that we need to extend the EventEmitter class and then emit the events:
The next step is to create a hook that allows a user to create a PNCounter and use it across the application.
Now that the counter is initialized and we have a stable reference to it we can subscribe to its changes thanks to it being an EventEmitter.
For this we will make use of the useSyncExternalStore hook that allows us to synchronize React renders with the external store. Moreover, we will use the Redux Selector pattern to fetch or calculate derived values from the counter. The combinations of these two things allows us to be very efficient in terms of renders.
Now that we have all the required boilerplate we can start building the actual game. For the communications between peers we are going to use PeerJS a peer-to-peer library built on top of WebRTC.
First let's clarify some things:
- The code in the article is just sample code, more like pseudo-code, the actual code is much more complex.
- The final result is not a mesh of clients where all clients are treated equal. For simplicity we are going to say that we have a host client.
- I will provide very little details on how PeerJS works, sorry!
Sharing state between clients
First we need to create the clients and link them, if you are using PeerJS or will be something like this:
Each client can independently increment or decrement the counter and that change will be sent to the the other clients. In order to do that we will just subscribe to the local counter changes, something like this:
With this ideas you should be able to iterate and build the game I described that the start of the article that uses the
Building a simple CRDT like a counter is not that hard, but if you are interested and keep digging into this topic you will see that other types of CRDTs have more complex challenges as I mentioned in the previous part of the series.
Also, using a CRDTs has its challenges, you need to figure out how to connect clients and share that information between them, but both problems can be thought in almost complete isolation from each other, this allows you to use them in many contexts, via the internet, HTTP, WebSocket, SMS, Letters, you name it.
These structures are really useful in distributed computing or realtime apps where network partitions will happen, so the next time you have to build a distributed system you can keep these ideas in mind (or you can send us an email 😉).