The blog has moved!
If you are interested in reading new posts, the new URL to bookmark is http://blog.valeriogheri.com/
PongR is a Pong like HTML5 multiplayer game.
Hint: it turns out that adding multiplayer is a HUGE effort, if you want to do it right or even just not completely wrong. 😀
What I still needed to start was a game with an easy model, that required not much graphics skill and that had multiplayer “built-in”… and that’s how I chose Pong!
It is important to note that this is a project I did to learn, and at least for this very first release, the game is really boring(!) and awfully looking 😦
Furthermore this article just describes my experience and my approach: I will point out all the difficulties I came across, all the things I understood, all the points that are still lacking (an awful lot, unfortunately), better technologies that could be used etc… Therefore it’s not a true tutorial about how a HTML5 multiplayer game should be done (I will post some interesting links, though), it’s more about how I put the pieces of the puzzle together and my considerations about HTML5 game development!
This post will be split in two sub-posts, just to avoid having a single, huge, unreadable, wall of text.
- The first half is about about the fundamentals: the (simple) math behind the physics model, the architecture of the system, the technologies used and finally the game client/server model, with an insight of how and why I failed hard at my first attempt.
The “mathematical” model
First off I started with the mathematical model that drives the game’s physics: player movements, ball movements, collisions with players and the field, goal event.
Players can only move up/down, as per the original Pong game; about the ball, in order to keep things simple, I adopted a basic model: initially it is moving horizontally, right or left .
During the game, the ball can collide either with a player (N.B. if the player is not moving, the ball will only change it’s horizontal direction) either with the field, and the new direction is computed based on the following schema:
For this first version I deliberately chose not to add more complexity, like a variable speed of the ball and players, that in turn can lead to different bouncing angles.
System architecture and technologies used
Because this is a multiplayer game, there will be an important part of server side code and an equally important frontend part (this point is crucial, I will come back to this).
The backend is implemented using Asp.Net MVC and C#, and the realtime communication is managed by SignalR.
Two pages: the landing page where the user enters a name and starts the game, and the second is really the game: the pitch and few info on the screen (players’ names and score).
This leads us to talk about the client connection flow:
- A client connects to http://pongr-1.appharbor.com, enters a name and submits the form
- If there is no match to join, then the client enters into a waiting state
- When there are two clients ready to play, the system
- Creates a match
- Assigns the two clients to the match
- Initialise the state of the game
- Adds the game it to the list of games that are currently being played
- Signals the clients the match can start, along with the initial state of the game
- Clients receive the start signal, setup the match and start their partial simulation of the game and draw the scene
Because we can have multiple games going on at the same time, the server needs to know which client belongs to which game (as per point 2b), so that whenever it has to send update about a game, the server will send the message to only the two clients that are playing that match, and not to everyone.
To do this, the server behind the scenes is storing games and players into an in-memory repository, using SignalR Groups feature to be able to send messages only to the right players.
SignalR Groups feature is a powerful, yet strange beast, as we will see in the second part of this post, posing some constraint on the way the system must be designed in order to leverage it.
Round 1, fight: a completely wrong approach!
As a first approach, because I didn’t know anything about the HTML5 Canvas object, I tried to bypass it entirely using HTML and CSS to draw each frame of the game: the field was a div, the players were divs, even the ball was a div rounded using CSS (border radius…)!! The UI was heavily automatized using Knockout.js.
Each client was simulating its own game, sending updates to the other client (through the SignalR Hub on the server) about its position on a fixed basis.
This means that each client is authoritative regarding its own state: position, score etc… and that the server is only a mere repeater for status updates.
As I quickly understood by playing the game, the first attempt had several fatal issues:
- DOM manipulation is really slow and limited when facing typical game requirements (not surprisingly)
- After a few seconds of playing, the players were not playing the same game anymore, they were completely out of synch! This is due to the fact that the network is not a perfect channel with no latency, and even with very few delays, the match is rapidly going out of synch.
By googling, I quickly realized that while addressing the first part would turn out to be easy, addressing the second point would be a totally different task.
Round 2 – A new approach: the server authoritative model and its consequences
As a total newbie to game development, I should have probably started following a tutorial from the start, but… it’s good to fail as well, you learn by understanding first hand what works and what does not.
At the end I found a very nice article (which links to other very interesting articles, that I strongly suggest you read, at least for background culture) about multiplayer HTML5 games: http://buildnewgames.com/real-time-multiplayer/
Because the networking architecture of my project is mostly taken from there, and is explained quite well (it’s better to look at the source code though, it will clarify some concepts), I won’t go too much into detail of each step. Once again the goal here is to provide a general idea and to explain my experience.
So what does it mean that the server is authoritative versus the clients in terms of the simulation of the game?
It means that the one and only authority regarding everything that happens in the world is the server: the server is running a simulation of the game, and it updates it based on the inputs that arrive from the clients (key presses, commands etc…).
On a regular basis, the server also notifies the clients of the new game state: all the connected clients will receive the same update and they will have to update their representation of the game based on what the server computed.
This simple idea will assure that all the connected clients will share almost (we will see later why “almost”) exactly the same representation of the game at any time.
This model is also used to avoid client from cheating: it’s always the server who decides what happens and what doesn’t!
This new approach leads to a new scenario: if before each client was running its own game, now it is the server that is running the game and the clients are left with nothing but to redraw the scene based on current game state.
You can try this approach quite easily but what you get is very poor performance in terms of reactivity: the client presses left, then the client sends a message to the server to notify it about the move, then the server updates the game state and sends the clients a new update, which in turn will redraw the scene, finally moving the player.
This means that after a loooooooooooooooooooooooooooooong wait, the client who moved will see its position change. This is what causes LAG. Not nice.
To avoid these kind of problems, one of the most common techniques is called “Client-side prediction”, and to describe it I can use the words from this very nice article http://www.gabrielgambetta.com/?p=22
“If the game world is deterministic enough (that is, given a game state and a set of inputs, the result is completely predictable),[…], we can assume the inputs we send to the server will be executed successfully. Under this assumption, the client can predict the state of the game world after the inputs are processed, and most of the time this will be correct.
Instead of sending the inputs and waiting for the new game state to start rendering it, we can send the input and start rendering the outcome of that inputs as if they had succeded, while we wait for the server to send the “true” game state – which more often than not, will match the state calculated locally”
This means that now the client is not a dumb spectator anymore, but actually runs some part of the game simulation too!
In the case of PongR, each client is predicting its own position, but because we also have a ball moving and potentially colliding with other objects, then the client needs also to predict every collision with the ball (and this is especially true if you consider sound effects)!
Unfortunately this implies having to duplicate some part of your code, and this can be painful if the languages used in the frontend and in the backend are different, like in this case!
There are also other techniques involved, like delta-time to make objects movement independent from the local framerate and client inteporalation to smooth movements of the opponent player. You can find them all detailed in the links I already pointed out.
One final, important, comment: no game state change event is simulated on the client.
Imagine a FPS game, the clients shoots another player and in its own representation of the game, the opponent dies: you see the animation and you get the points… All is good except that as soon as you receive the update from the server, you find out that it’s wrong, you didn’t really kill the opponent as he was 1 meter more on the left that you had known. So… how can you rollback such a situation? Simply you cannot, and that’s why game state change events are only simulated on the server.
In the case of PongR, the only game state change event is when one of the two players score.
We will see how I managed this.
That’s it, we are done for part 1. Next we will see what’s behind the scene.