The blog has moved!
If you are interested in reading new posts, the new URL to bookmark is http://blog.valeriogheri.com/
This is part 2 of “PongR – my first experience with HTML5 multiplayer gaming” (https://thewayofcode.wordpress.com/2012/11/05/pongr-my-first-experience-with-html5-multiplayer-gaming/)
Code and Demo
As always, the code is fully published on github [https://github.com/vgheri/PongR] and a (boring!) demo is available at http://pongr-1.apphb.com . If you don’t have anyone to play with, just open two tabs and enjoy!
Following a screenshot of the game:
Project structure and technologies used
PongR is a multiplayer HTML5 game, and this means that the client itself is doing a good amount of work.
The engine is replicated both on the server and on the client, as we have seen in the first part of this article when I talked about the server authoritative model + client side prediction that force us to add logic onto the client.
In the server this logic is built into a class named Engine and later on we will go into more details.
What we’ll see
While writing this second part of the post, I realised that tackling both the server and the client side in one single post will result in a wall of text.
Therefore in part 2 I will start covering the server side code, that is the authoritative source for our game.
Finally in part 3 I will cover the client side code, and this will allow me to dive deeper into HTML5 canvas, qUnit and so on.
So, let’s start!
Connecting to the server
I think that an intuitive way to dive into the code and understand it, is to to navigate the flow of the system: let’s pretend there are no matches currently played on the server, and that we are a client that lands on PongR page.
The Joined() method is inside the PongRHub class, on the server, and will accomplish the following tasks:
- Add user to list of connected users
- If waiting list is empty add user to waiting list
- Else find an opponent (first in the waiting list) and remove him from the waiting list
- Create room and assign both users
- Create a group for this room
- Setup match (playRoom Id, initial ball direction, player on the left and right etc…)
- Notify the group the match can start
- Add the game to the list of games that the Engine must simulate
Now, the first time I implemented this method I had a problem in between point 5 & 7: sometimes the clients didn’t receive the signal from the server to setup the match.
So I went back to the project’s wiki and I browsed the source code and I understood where the problem lies: when the server invokes Groups.Add(), it sends a signal to the client and notify it about its appartenance to a certain group.
Therefore sometimes I had race conditions where the second signal arrived before the first one, causing the client to drop the message.
I quickly and roughly fixed the issue adding a Thread.Sleep() call after the Groups.Add calls.
Here is the code for Joined():
I could have found a more elegant solution adding one more call after the joined to split the work, but the focus of this project is not SignalR per se, so I guess it’s ok anyway…
Next I signal clients that the match can start along with the game options, like initial direction of the ball, players info etc., and finally enqueue the game on the server so that its state will start to be processed.
// sends a message only to the clients belonging to the group, invoking the function setupMatch() that is part of the PongR module. Clients[playRoom.Id].setupMatch(matchOptions); // Adds the freshly created game to the list of games to be simulated by the server Engine.AddGame(game);
This is where our story splits in two parts: we now simultaneously have the clients AND the server playing the game.
How does it work? Again the theory behind what follows is taken from http://buildnewgames.com/real-time-multiplayer/ , which in turn comes from Valve’ Source Engine, I just adjusted it to my needs.
Basically both the server and the clients run two loops, and we call them:
- The physics update loop
- The update loop
Now these two loops do different things and run at different speeds depending if we are on the server or on the client.
Before moving on talking about the loops, it might be useful to describe what kind of messages the client is sending over to the server.
In PongR a client can only move up or down, and a sequence number is associated to each command to uniquely identify it.
So our payload will contain the following info:
- the id of the game being played
- the client id that just sent the command
- the command
- a sequence number
- a string identifying the type of movement: “up” or ”down”
The meaning of the sequence identifier is related to the Client-side prediction mechanism and the input buffer that both the server and the client keep. Here are the steps:
- The client moves and stores into its buffer the input along with its id
- The client sends the input to the server
- The server enqueues the input into a buffer
- When the server processes the input, it removes it from the buffer
- When the server update loop runs (will see it later in more detail), it notifies the client of the latest input that has been processed
- The client can remove from its buffer all the input with ID lower than or equal to the one just ack’ed
The payload size could be slimmed down by a lot, but that is again another story (for some ideas, take a look here: [www.buildnewgames.com/optimizing-websockets-bandwidth ] )
So let’s start with the server, the authoritative source of our game.
The server loops
To run the loops at fixed intervals, I used the System.Timers.Timer object, and I set them up like this in the Global.asax:
In the second gist we also see the handlers that will run when the Elapsed event is raised.
The server physics update loop
The server physics update loop runs around 60 times per second, that is every 15 msecs.
Its job is to update, for every tick of the simulation, players and ball position based on the inputs received by the clients and on the law of the physics of the game.
Let’s see what a loop round looks like:
MovePlayer() and UpdateBallPosition() are relatively simple so I won’t show them here.
Shortly, the former modifies the Y coordinate of the player and its direction (up/down), based on the inputs that it has received, while acknowledging every processed input sequence number, and the latter simple modifies the (X,Y) coordinates of the ball based only on its angle (that implictitly gives us the direction). For every step of the simulation, both the ball and the players (if any input arrived) will move by a fixed amount of pixels.
Browsing the code you will notice that both the player and the ball movement are based on something called DELTA_TIME. This is related to the concept of framerate independence and I invite you to read more about that here http://www.scirra.com/tutorials/67/delta-time-and-framerate-independence/page-1.
Briefly, quoting the link above:
“Framerate independent games are games that run at the same speed, no matter the framerate.”
Moving on to
var goal = CheckGoalConditionAndUpdateStatus(game);
we see that it’s the server who is checking if we have a goal situation, and if it’s the case, we update the score of each player (important, and later we’ll see why), add the timestamp of this event to a collection of goal timestamps and then we proceed to restart the game after a goal, that is really resetting positions of players and ball to initial state and choosing randomly the new initial ball direction.
I’m storing the goal timestamp because on the client we provide visual feedback of a goal, running a countdown after each so that the user has the time to understand what is going on and get ready for the next ball to play. During the countdown the ball won’t obviously move, therefore the server must not continue to simulate as well during this time.
We can see this check implemented in the outer physics loop, that is the method actually invoked by the Time elapsed event handler:
That’s it for the server physics update loop.
The server update loop
The update loop runs every 45 msecs, around 22 times per second, so at a much slower rate then the physics loop.
It’s all about finding a good trade-off between accuracy of the simulation (faster update rates) and network usage and performance (slower update rates).
The update loop job is to send the updated state of the game to the right clients.
The following line will send the message to the two clients that are part of the SignalR group
That’s it for part 2 of this article. We have covered the connection process and the server update loops. We have seen all the workarounds used to cope with SignalR Groups and to skip simulation when one of the player scored in a match, so that the clients can display a countdown before restarting the match.
Part 3 will be the last and we’ll take a look at the client side code, even though you can start by yourself cloning the repository on github.