Multiplayer Game Development With HTML5 - Sample Chapter
Multiplayer Game Development With HTML5 - Sample Chapter
ee
P U B L I S H I N G
pl
C o m m u n i t y
D i s t i l l e d
E x p e r i e n c e
$ 29.99 US
19.99 UK
Sa
m
Rodrigo Silveira
Rodrigo Silveira
Chapter 6, Adding Security and Fair Play, covers common flaws and security
vulnerabilities that are associated with network gaming. Here, common techniques are
described and demonstrated, allowing you to develop games that provide a playing
experience that is free from cheating.
[1]
Peer-to-peer networking
A simple way to connect players into the same virtual game world is through the
peer-to-peer architecture. Although the name might suggest that only two peers
("nodes") are involved, by definition a peer-to-peer network system is one in which
two or more nodes are connected directly to each other without a centralized system
orchestrating the connection or information exchange.
[2]
Chapter 1
On a typical peer-to-peer setup, each peer serves the same function as every other
onethat is, they all consume the same data and share whatever data they produce
so that others can stay synchronized. In the case of a peer-to-peer game, we can
illustrate this architecture with a simple game of Tic-tac-toe.
Player 1
Player 2
0
(here's my move)
0
(cool. Here's mine)
0
0
0
(here's my next move)
(...)
0
0
Once both the players have established a connection between themselves, whoever
is starting the game makes a move by marking a cell on the game board. This
information is relayed across the wire to the other peer, who is now aware of the
decision made by his or her opponent, and can thus update their own game world.
Once the second player receives the game's latest state that results from the first
player's latest move, the second player is able to make a move of their own by
checking some available space on the board. This information is then copied over to
the first player who can update their own world and continue the process by making
the next desired move.
The process goes on until one of the peers disconnects or the game ends as some
condition that is based on the game's own business logic is met. In the case of the
game of Tic-tac-toe, the game would end once one of the players has marked three
spaces on the board forming a straight line or if all nine cells are filled, but neither
player managed to connect three cells in a straight path.
[3]
Fast data transmission: Here, the data goes directly to its intended target.
In other architectures, the data could go to some centralized node first, then
the central node (or the "server", as we'll see in the next section) contacts the
other peer, sending the necessary updates.
Simpler setup: You would only need to think about one instance of your
game that, generally speaking, handles its own input, sends its input to other
connected peers, and handles their output as input for its own system. This
can be especially handy in turn-based games, for example, most board games
such as Tic-tac-toe.
More reliability: Here one peer that goes offline typically won't affect any
of the other peers. However, in the simple case of a two-player game, if
one of the players is unable to continue, the game will likely cease to be
playable. Imagine, though, that the game in question has dozens or hundreds
of connected peers. If a handful of them suddenly lose their Internet
connection, the others can continue to play. However, if there is a server that
is connecting all the nodes and the server goes down, then none of the other
players will know how to talk to each other, and nobody will know what is
going on.
On the other hand, some of the more obvious drawbacks of peer-to-peer architecture
are as follows:
Incoming data cannot be trusted: Here, you don't know for sure whether or
not the sender modified the data. The data that is input into a game server
will also suffer from the same challenge, but once the data is validated and
broadcasted to all the other peers, you can be more confident that the data
received by each peer from the server will have at least been sanitized and
verified, and will be more credible.
Fault tolerance can be very low: The opposite argument was made in the
benefits' section of Peer-to-peer networking that we discussed previously; if
enough players share the game world, one or more crashes won't make the
game unplayable to the rest of the peers. Now, if we consider the many cases
where any of the players that suddenly crash out of the game negatively
affect the rest of the players, we can see how a server could easily recover
from the crash.
[4]
Chapter 1
Data duplication when broadcasting to other peers: Imagine that your game
is a simple 2D side scroller, and many other players are sharing that game
world with you. Every time one of the players moves to the right, you receive
the new (x, y) coordinates from that player, and you're able to update your
own game world. Now, imagine that you move your player to the right by
a very few pixels; you would have to send that data out to all of the other
nodes in the system.
Client-server networking
The idea behind the client-server networking architecture is very simple. If you
squint your eyes hard enough, you can almost see a peer-to-peer graph. The most
obvious difference between them, is that, instead of every node being an equal peer,
one of the nodes is special. That is, instead of every node connecting to every other
node, every node (client) connects to a main centralized node called the server.
While the concept of a client-server network seems clear enough, perhaps a simple
metaphor might make it easier for you to understand the role of each type of node
in this network format as well as differentiate it from peer-to-peer (McConnell, Steve,
(2004) Code Complete., Microsoft Press). In a peer-to-peer network, you can think of it
as a group of friends (peers) having a conversation at a party. They all have access
to all the other peers involved in the conversation and can talk to them directly. On
the other hand, a client-server network can be viewed as a group of friends having
dinner at a restaurant. If a client of the restaurant wishes to order a certain item from
the menu, he or she must talk to the waiter, who is the only person in that group of
people with access to the desired products and the ability to serve the products to
the clients.
[5]
In short, the server is in charge of providing data and services to one or more clients.
In the context of game development, the most common scenario is when two or more
clients connect to the same server; the server will keep track of the game as well as
the distributed players. Thus, if two players are to exchange information that is only
pertinent to the two of them, the communication will go from the first player to and
through the server and will end up at the other end with the second player.
(here's player
1, move 1)
(here's my
move #1)
Player 1
Player 2
Server
(player 2,
move 1)
(player 2,
move 1)
(player 1,
move 2)
(...)
Following the example of the two players involved in a game of Tic-tac-toe that we
looked at in the section about peer-to-peer, we can see how similar the flow of events
is on a client-server model. Again, the main difference is that players are unaware of
each other and only know what the server tells them.
While you can very easily mimic a peer-to-peer model by using a server to merely
connect the two players, most often the server is used much more actively than
that. There are two ways to engage the server in a networked game, namely in
an authoritative and a non-authoritative way. That is to say, you can have the
enforcement of the game's logic strictly in the server, or you can have the clients
handle the game logic, input validation, and so on. Today, most games using the
client-server architecture actually use a hybrid of the two (authoritative and nonauthoritative servers, which we'll discuss later in the book). For all intents and
purposes, however, the server's purpose in life is to receive input from each of the
clients and distribute that input throughout the pool of connected clients.
Now, regardless of whether you decide to go with an authoritative server instead of
a non-authoritative one, you will notice that one of challenges with a client-server
game is that you will need to program both ends of the stack. You will have to do
this even if your clients do nothing more than take input from the user, forward it
to the server, and render whatever data they receive from the server; if your game
server does nothing more than forward the input that it receives from each client to
every other client, you will still need to write a game client and a game server.
We will discuss game clients and servers later in the chapter. For now, all we really
need to know is that these two components are what set this networking model apart
from peer-to-peer.
[6]
Chapter 1
Less work for the client: Instead of having a client (peer) in charge of taking
input from the user as well as other peers, validating all the input, sharing
data among other peers, rendering the game, and so on, the client can focus
on only doing a few of these things, allowing the server to offload some of
this work. This is particularly handy when we talk about mobile gaming, and
how much subtle divisions of labor can impact the overall player experience.
For example, imagine a game where 10 players are engaged in the same
game world. In a peer-to-peer setup, every time one player takes an action,
he or she would need to send that action to nine other players (in other
words, there would need to be nine network calls, boiling down to more
mobile data usage). On the other hand, on a client-server configuration,
one player would only need to send his or her action to one of the peers,
that is, the server, who would then be responsible for sending that data to
the remaining nine players.
[7]
More complexity due to more moving parts: It doesn't really matter how
you slice the pizza; the more code you need to write (and trust me, when
you build two separate modules for a game, you will write more code),
the greater your mental model will have to be. While much of your code
can be reused between the client and the server (especially if you use wellestablished programming techniques, such as object-oriented programming),
at the end of the day, you need to manage a greater level of complexity.
Using the same example of the two-player game of Tic-tac-toe, imagine that there are
thousands of players facing each other in single games. In a peer-to-peer setup, once
a couple of players have directly paired off, it is as though there are no other players
enjoying that game. The only thing to keep these two players from continuing their
game is their own connection with each other.
On the other hand, if the same thousands of players are connected to each other
through a server sitting between the two, then two singled out players might notice
severe delays between messages because the server is so busy handling all of the
messages from and to all of the other people playing isolated games. Worse yet, these
two players now need to worry about maintaining their own connection with each
other through the server, but they also hope that the server's connection between
them and their opponent will remain active.
All in all, many of the challenges involved in client-server networking are well
studied and understood, and many of the problems you're likely to face during
your multiplayer game development will already have been solved by someone
else. Client-server is a very popular and powerful game networking model, and the
required technology for it, which is available to us through HTML5 and JavaScript, is
well developed and widely supported.
[8]
Chapter 1
While at first, UDP may seem like a reckless protocol, the use cases that make UDP
so desirable and effective includes the many situations when you care more about
speed than missing packets a few times, getting duplicate packets, or getting them
out of order. You may also want to choose UDP over TCP when you don't care
about the reply from the receiver. With TCP, whether or not you need some form of
confirmation or reply from the receiver of your message, it will still take the time to
reply back to you, at least acknowledging that the message was received. Sometimes,
you may not care whether or not the server received the data.
TCP
Client
UDP
VS
Client
Server
Message 1
Server
Message 1
Acknowledge Message 1
Message 2
Message 2
//
Message 3 got lost
Acknowledge Message 2
...
...
A more concrete example of a scenario where UDP is a far better choice over TCP
is when you need a heartbeat from the client letting the server know if the player is
still there. If you need to let your server know that the session is still active every so
often, and you don't care if one of the heartbeats get lost every now and again, then it
would be wise to use UDP. In short, for any data that is not mission-critical and you
can afford to lose, UDP might be the best option.
In closing, keep in mind that, just as peer-to-peer and client-server models can
be built side by side, and in the same way your game server can be a hybrid
of authoritative and non-authoritative, there is absolutely no reason why your
multiplayer games should only use TCP or UDP. Use whichever protocol a particular
situation calls for.
[ 10 ]
Chapter 1
Network sockets
There is one other protocol that we'll cover very briefly, but only so that you can see
the need for network sockets in game development. As a JavaScript programmer,
you are doubtlessly familiar with Hypertext Transfer Protocol (HTTP). This is the
protocol in the application layer that web browsers use to fetch your games from a
Web server.
While HTTP is a great protocol to reliably retrieve documents from web servers,
it was not designed to be used in real-time games; therefore, it is not ideal for this
purpose. The way HTTP works is very simple: a client sends a request to a server,
which then returns a response back to the client. The response includes a completion
status code, indicating to the client that the request is either in process, needs to be
forwarded to another address, or is finished successfully or erroneously (Hypertext
Transfer Protocol (HTTP/1.1): Authentication, (June 1999). https://tools.ietf.org/
html/rfc7235)
There are a handful of things to note about HTTP that will make it clear that a better
protocol is needed for real-time communication between the client and server.
Firstly, after each response is received by the requester, the connection is closed.
Thus, before making each and every request, a new connection must be established
with the server. Most of the time, an HTTP request will be sent through TCP, which,
as we've seen, can be slow, relatively speaking.
Secondly, HTTP is by design a stateless protocol. This means that, every time you
request a resource from a server, the server has no idea who you are and what is
the context of the request. (It doesn't know whether this is your first request ever
or if you're a frequent requester.) A common solution to this problem is to include
a unique string with every HTTP request that the server keeps track of, and can
thus provide information about each individual client on an ongoing basis. You
may recognize this as a standard session. The major downside with this solution, at
least with regard to real-time gaming, is that mapping a session cookie to the user's
session takes additional time.
Finally, the major factor that makes HTTP unsuitable for multiplayer game
programming is that the communication is one wayonly the client can connect to
the server, and the server replies back through the same connection. In other words,
the game client can tell the game server that a punch command has been entered
by the user, but the game server cannot pass that information along to other clients.
Think of it like a vending machine. As a client of the machine, we can request specific
items that we wish to buy. We formalize this request by inserting money into the
vending machine, and then we press the appropriate button.
[ 11 ]
Client
Server
output
Port 1080
input
Port 1080
Socket
Port 2667
output
Port 2667
input
[ 12 ]
Chapter 1
Let us take a quick look at the programming interface (API) that we'll use within our
JavaScript code to interact with a WebSocket server. Keep in mind that we'll need
to write both the JavaScript clients that use WebSockets to consume data as well as
the WebSocket server, which uses WebSockets but plays the role of the server. The
difference between the two will become apparent as we go over some examples.
[ 13 ]
Although this one line of code may seem simple and harmless enough, here are a
few things to keep in mind:
It may seem obvious to you, but a common pitfall that those getting started
with WebSockets fall into is that, before you can establish a connection with
the previous code, you need a WebSocket server running at that domain.
[ 14 ]
Chapter 1
Once the socket is ready to send and receive data, you can send messages to the
server by calling the socket object's send method, which takes a string as the message
to be sent.
// Assuming a connection was previously established
socket.send('Hello, WebSocket world!');
Most often, however, you will want to send more meaningful data to the server, such
as objects, arrays, and other data structures that have more meaning on their own. In
these cases, we can simply serialize our data as JSON strings.
var player = {
nickname: 'Juju',
team: 'Blue'
};
socket.send(JSON.stringify(player));
Now, the server can receive that message and work with it as the same object structure
that the client sent it, by running it through the parse method of the JSON object.
var player = JSON.parse(event.data);
player.name === 'Juju'; // true
player.team === 'Blue'; // true
player.id === undefined; // true
If you look at the previous example closely, you will notice that we extract the
message that is sent through the socket from the data attribute of some event object.
Where did that event object come from, you ask? Good question! The way we receive
messages from the socket is the same on both the client and server sides of the
socket. We must simply register a callback function on the socket's onmessage event,
and the callback will be invoked whenever a new message is received. The argument
passed into the callback function will contain an attribute named data, which will
contain the raw string object with the message that was sent.
socket.onmessage = function(event) {
event instanceof MessageEvent; // true
var msg = JSON.parse(event.data);
};
Other events on the socket object on which you can register callbacks include
onerror, which is triggered whenever an error related to the socket occurs, and
onclose, which is triggered whenever the state of the socket changes to CLOSED; in
other words, whenever the server closes the connection with the client for any reason
or the connected client closes its connection.
As mentioned previously, the socket object will also have a property called
readyState, which behaves in a similar manner to the equally-named attribute
in AJAX objects (or more appropriately, XMLHttpRequest objects). This attribute
represents the current state of the connection and can have one of four values at any
point in time. This value is an unsigned integer between 0 and 3, inclusive of both
the numbers. For clarity, there are four accompanying constants on the WebSocket
class that map to the four numerical values of the instance's readyState attribute.
The constants are as follows:
between the client and the server is open and ready for use. Whenever the
object's readyState attribute changes from CONNECTING to OPEN, which
will only happen once in the object's life cycle, the onopen callback will be
invoked.
being closed.
Once the readyState has changed to a new value, it will never return to a previous
state in the same instance of the socket object. Thus, if a socket object is CLOSING or
has already become CLOSED, it will never OPEN again. In this case, you would need
a new instance of WebSocket if you would like to continue to communicate with
the server.
To summarize, let us bring together the simple WebSocket API features that
we discussed previously and create a convenient function that simplifies data
serialization, error checking, and error handling when communicating with the
game server:
function sendMsg(socket, data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
return true;
}
[ 16 ]
Chapter 1
return false;
};
Game clients
Earlier in the chapter, we talked about the architecture of a multiplayer game that
was based on the client-server pattern. Since this is the approach we will take for the
games that we'll be developing throughout the book, let us define some of the main
roles that the game client will fulfill.
From a higher level, a game client will be the interface between the human player
and the rest of the game universe (which includes the game server and other
human players who are connected to it). Thus, the game client will be in charge of
taking input from the player, communicating this to the server, receive any further
instructions and information from the server, and then render the final output to
the human player again. Depending on the type of game server used (we'll discuss
this in the next section and in future chapters), the client can be more sophisticated
than just an input application that renders static data received from the server.
For example, the client could very well simulate what the game server will do
and present the result of this simulation to the user while the server performs the
real calculations and tells the results to the client. The biggest selling point of this
technique is that the game would seem a lot more dynamic and real-time to the
user since the client responds to input almost instantly.
Game servers
The game server is primarily responsible for connecting all the players to the same
game world and keeping the communication going between them. However as
you will soon realize, there may be cases where you will want the server to be
more sophisticated than a routing application. For example, just because one of the
players is telling the server to inform the other participants that the game is over,
and the player sending the message is the winner, we may still want to confirm the
information before deciding that the game is in fact over.
With this idea in mind, we can label the game server as being of one of the two kinds:
authoritative or non-authoritative. In an authoritative game server, the game's logic
is actually running in memory (although it normally doesn't render any graphical
output like the game clients certainly will) all the time. As each client reports the
information to the server by sending messages through its corresponding socket,
the server updates the current game state and sends the updates back to all of the
players, including the original sender. This way we can be more certain that any
data coming from the server has been verified and is accurate.
[ 17 ]
[ 18 ]
Chapter 1
Surely, you are familiar with Tic-tac-toe. Two players take turns marking a single
square on a 9x9 grid, and whoever marks three spaces on the board with the same
mark such that a straight line is formed either horizontally, vertically, or diagonally
wins. If all nine squares are marked and the previously mentioned rule is not
fulfilled, then the game ends in a draw.
There are six main files that we need to worry about for now. The rest of them are
automatically generated by Node.js and related tooling. As for our six scripts, this is
what each of them does.
[ 19 ]
The last line will be explained in more detail when we talk about the basics of Node.
js. For now, what you need to know it is that it makes the Player class available to
the server code as well as the client code that is sent to the browser.
In addition, we could very well just use an object literal throughout the game
in order to represent what we're abstracting away as a player object. We could
even use an array with those three values, where the order of each element would
represent what the element is. While we're at it, we could even use a commaseparated string to represent all the three values.
As you can see, the slight verbosity incurred here by creating a whole new class
just to store three simple values makes it easier to read the code, as we now know
the contract that is established by the game when it asks for a Player. It will expect
attributes named id, label, and name to be present there.
In this case, id can be considered a bit superfluous because its only purpose is to
identify and distinguish between the players. The important thing is that the two
players have a unique ID. The label attribute is what each player will print on the
board, which just happens to be a unique value as well between both the players.
Finally, the name attribute is used to print the name of each player in a humanreadable way.
[ 20 ]
Chapter 1
As this code is intended to run in the server only, it takes full advantage of Node.
js. The first part of the script imports two core Node.js modules that we'll leverage
instead of reinventing the wheel. The first, EventEmitter, will allow us to broadcast
events about our game as they take place. Second, we import a utility class that
lets us easily leverage object-oriented programming. Finally, we define some static
variables related to the Board class in order to simplify event registration and
propagation.
Board.prototype.mark = function(cellId) {
//
if (this.checkWinner()) {
this.emit(Board.events.WINNER, {player:
this.players[this.currentTurn]});
}
};
[ 21 ]
The Board class exposes several methods that a driver application can call in order
to input data into it, and it emits events when certain situations occur. As illustrated
in the method mentioned previously, whenever a player successfully marks an
available square on the board, the game broadcasts that event so that the driver
program knows what has happened in the game; it can then contact each client
through their corresponding sockets, and let them know what happened.
Chapter 1
function makeMessage(action, data) {
var resp = {
action: action,
data: data
};
return JSON.stringify(resp);
}
console.log('Listening on port %d', PORT);
The first part of this Node.js server script imports both our custom classes (Board
and Player) as well as a handy third-party library called ws that helps us implement
the WebSocket server. This library handles things such as the setup of the initial
connection, the protocol upgrade, and so on, since these steps are not included in the
JavaScript WebSocket object, which is only intended to be used as a client. After a
couple of convenience objects, we have a working server that waits for connections
on ws://localhost:2667.
wss.on('connection', function connection(ws) {
board.on(Board.events.PLAYER_CONNECTED, function(player) {
wss.clients.forEach(function(client) {
board.players.forEach(function(player) {
client.send(makeMessage(events.outgoing.JOIN_GAME,
player));
});
});
});
ws.on('message', function incoming(msg) {
try {
var msg = JSON.parse(msg);
} catch (error) {
ws.send(makeMessage(events.outgoing.ERROR, 'Invalid
action'));
return;
}
try {
switch (msg.action) {
case events.incoming.JOIN_GAME:
var player = new Player(board.players.length + 1,
board.players.length === 0 ? 'X' : 'O', msg.data);
board.addPlayer(player);
break;
[ 23 ]
The rest of the important stuff with this server happens in the middle. For brevity,
we've only included one example of each situation, which includes an event
handler registration for events emitted by the Board class as well as registration
of a callback function for events received by the socket. (Did you recognize the
ws.on('message', function(msg){}) function call? This is Node's equivalent
of the client-side JavaScript socket.onmessage = function(event){} that we
discussed earlier.)
Of major importance here is the way we handle incoming messages from the game
clients. Since the client can only send us a single string as the message, how are we
to know what the message is? Since there are many types of messages that the client
can send to the server, what we do here is create our own little protocol. That is, each
message will be a serialized JSON object (also known as an object literal) with two
attributes. The first will be keyed with the value of action and the second will have
a key of data, which can have a different value depending on the specified action.
From here, we can look at the value of msg.action and respond to it accordingly.
For example, whenever a client connects to the game server, it sends a message with
the following value:
{
action: events.outgoing.JOIN_GAME,
data: "<player nickname>"
};
Once the server receives that object as the payload of the onmessage event, it can
know what the message means and the expected value for the player's nickname.
Chapter 1
*/
var Board = function(scoreBoard) {
this.cells = [];
this.dom = document.createElement('table');
this.dom.addEventListener('click', this.mark.bind(this));
this.players = [];
this.currentTurn = 0;
this.ready = false;
this.scoreBoard = scoreBoard;
this.init();
};
Board.prototype.bindTo = function(container) {
container.appendChild(this.dom);
};
Board.prototype.doWinner = function(pos) {
this.disableAll();
this.highlightCells(pos);
};
Again, for brevity, we have chosen not to display much of the game's logic. The
important things to note here are that this version of the Board class is very much
DOM-aware, and it behaves very passively to game decisions and the enforcement of
the game's rules. Since we're using an authoritative server, this class does whatever
the server tells it to, such as marking itself in a way that indicates that a certain
participant has won the game.
board.onMark = function(cellId){
socket.send(makeMessage(events.outgoing.MARK, {playerId: hero.id,
cellId: cellId}));
};
socket.onmessage = function(event){
var msg = JSON.parse(event.data);
switch (msg.action) {
case events.incoming.GAME_OVER:
if (msg.data.player) {
board.doWinner(msg.data.pos);
} else {
board.doDraw();
}
socket.send(makeMessage(events.outgoing.QUIT, hero.id));
break;
case events.incoming.QUIT:
socket.close();
break;
}
};
socket.onopen = function(event) {
startBtn.removeAttribute('disabled');
nameInput.removeAttribute('disabled');
nameInput.removeAttribute('placeholder');
nameInput.focus();
};
Again, it is noteworthy how DOM-centric the client server is. Observe also how
obedient the client is to the messages received from the server. If the action specified
by the server in the message that it sends to the clients is GAME_OVER, the client
cleans things up, tells the player that the game is over either because someone won
the game or the game ended in a draw, then it tells the server that it is ready to
disconnect. Again, the client waits for the server to tell it what to do next. In this case,
it waits for the server to clean up, then tells the client to disconnect itself.
[ 26 ]
Chapter 1
Summary
In this chapter, we discussed the basics of networking and network programming
paradigms. We saw how WebSockets makes it possible to develop real-time,
multiplayer games in HTML5. Finally, we implemented a simple game client and
game server using widely supported web technologies and built a fun game of
Tic-tac-toe.
In the next chapter, we will take a look at the current state of the art in the JavaScript
development world, including JavaScript in the server through Node.js. The chapter
will teach you current techniques to manage the development cycle in JavaScript with
workflow and resource management tools such as NPM, Bower, Grunt, and so on.
[ 27 ]
www.PacktPub.com
Stay Connected: