├── img
├── game_over.jpg
├── gameplay.jpg
├── architecture.png
├── start_game.jpg
├── architecture_rus.png
├── socketio_header.jpg
└── chrome_network-640x360.jpg
├── README.md
├── eng.md
└── rus.md
/img/game_over.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/game_over.jpg
--------------------------------------------------------------------------------
/img/gameplay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/gameplay.jpg
--------------------------------------------------------------------------------
/img/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/architecture.png
--------------------------------------------------------------------------------
/img/start_game.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/start_game.jpg
--------------------------------------------------------------------------------
/img/architecture_rus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/architecture_rus.png
--------------------------------------------------------------------------------
/img/socketio_header.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/socketio_header.jpg
--------------------------------------------------------------------------------
/img/chrome_network-640x360.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontenderMagazine/building-multiplayer-games-with-node-js-and-socket-io/HEAD/img/chrome_network-640x360.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Современные веб-технологиии предоставляют достаточно широкие возможности для
2 | разработки различных приложений, в том числе многопользовательских игр,
3 | реализующих взаимодействие пользователей в режиме реального времени.
4 | В этой статье описывается опыт создания подобной игры и объясняются
5 | основные принципы взаимодействия клиента и сервера посредством передачи
6 | информации через протокол веб-сокетов.
7 |
--------------------------------------------------------------------------------
/eng.md:
--------------------------------------------------------------------------------
1 | Building Multiplayer Games with Node.js and Socket.IO
2 | -----------------------------------------------------------
3 |
4 | *By [Eric Terpstra][1]*
5 |
6 | With the release of the Ouya, Xbox One and PS4 this year, couch-based console
7 | gaming appears to be as popular as ever. Despite the proliferation of multi-
8 | player games available on mobile devices, and even the advent of multi-player
9 | experiences available on the web, sitting next to the person you’re playing a
10 | game with is still an irreplaceable experience. While experimenting with Node.js
11 | and the Socket.IO library, I found a perfect opportunity to not only learn some
12 | interesting new technologies, but also experiment with using the web and common
13 | devices (computers and mobile phones) to replicate a console-like gaming
14 | experience.
15 |
16 | This article will give a brief overview of the fundamental concepts of the
17 | Socket.IO library in the context of building a multi-player, multi-screen word
18 | game. A web browser on a large screen device, such as a TV or computer will be
19 | the ‘console’ and mobile browsers will act as the ‘controllers’. Socket.IO and
20 | Node.js will provide the necessary wiring for all the browsers to share data and
21 | provide a cohesive, real-time game experience.
22 |
23 | ## The Game – Anagrammatix
24 |
25 | The game itself is a made up challenge involving anagrams – words that can
26 | have their letters re-arranged to form new words. The gameplay is quite simple
27 | – on a large ‘Host’ screen, a word is displayed. On each player’s controller (
28 | phone) is a list of similar words. One of the words is an anagram of the word on
29 | the Host screen. The player to tap the anagram first gets points for the round.
30 | However, tapping an incorrect word will lose points for the player. After 10
31 | rounds, the player with the most points wins!
32 |
33 | To download the code and play locally, first ensure that Node.js is installed.
34 | Visit the [Anagrammatix GitHub repo][2], clone the repository with
35 | `git clone https://github.com/ericterpstra/anagrammatix.git` and then run
36 | `npm install` to download and install all the dependencies (Socket.IO and
37 | Express). Once that is complete, run`node index.js` to start the game server.
38 | Visit http://localhost:8080 in your browser to start a game. In order to test
39 | out the host as well as multiple “player” clients you’ll need to open multiple
40 | windows.
41 |
42 | If you want to play with mobile devices, you will need to know the I.P. address
43 | of your computer on your local network. Your mobile devices will also need to be
44 | on the same network as the computer running the application. To begin, simply
45 | replace ‘localhost’ in the URL with the I.P. address (e.g. http://192.168.0.5:
46 | 8080). If you are on Windows, you may need to disable Windows firewall, or
47 | ensure port 8080 is open.
48 |
49 | ## The Underlying Technology
50 |
51 | Anagrammatix is implemented using only HTML, CSS and JavaScript. To keep things
52 | as simple as possible, the use of libraries and frameworks was kept to a minimum.
53 | The major technologies used in the game are listed below:
54 |
55 | * **Node.js** - Node provides the foundation for the back-end portion of the
56 | game, and allows the use of the Express framework, and the Socket.IO library.
57 | Also, some of the game logic for Anagrammatix takes place on the server. This
58 | logic is contained in a custom *Node Module* which will be discussed later.
59 |
60 | * **Express** - On its own, Node.js does not provide many of the necessary
61 | requirements for creating a web application. Adding Express makes it much
62 | simpler to serve static files such as HTML, CSS and JavaScript. Express is also
63 | used to adjust logging output, and create an easy-to-use environment for Socket.
64 | IO
65 |
66 | * **Socket.IO** - Socket.IO makes it dead simple to open a real-time
67 | communication channel between a web browser and a server (in this case, a server
68 | running Node.js and Express). By default, Socket.IO will use the
69 | *websockets* protocol if it is supported by the browser. Older browsers
70 | such as IE9 do not support websockets. In that case, Socket.IO will fall back to
71 | other technologies, such as using Flash sockets or an Ajax technique called
72 | *long-polling*.
73 |
74 | * Client side JavaScript – On the client side, a few libraries are used to
75 | make things a little easier:
76 |
77 | * jQuery – Mainly used in this case for DOM manipulation and event
78 | handling.
79 |
80 | * [TextFit ][3]- A small library that will expand text to fit the size of
81 | its container. This is what allows the ‘Anagrammatix’ title screen to fill the
82 | entire width of the browser no matter the screen size.
83 |
84 | * [FastClick.js][4] – A small library that helps remove the 300ms delay
85 | on mobile devices when ‘tapping’ the screen. This makes the game controller
86 | slightly more responsive when playing.
87 |
88 | * CSS – Some standard CSS is used to add a little color and style to the
89 | game.
90 |
91 | ## The Architecture
92 |
93 | ![architecture][5]
94 |
95 | The basic setup for the game follows the recommended configuration for using
96 | Socket.IO with Express on the[Socket.IO website][6]. In this configuration,
97 | Express is used to handle HTTP requests coming into the Node application. The
98 | Socket.IO module attaches itself to Express and listens on the same port for
99 | incoming websocket connections.
100 |
101 | When a browser connects to the application, it is served index.html and all the
102 | required JavaScript and CSS to begin. The JavaScript in the browser will
103 | initiate a connection to Socket.IO and create a websocket connection. Each
104 | websocket connection has a unique identifier (uid) so Node and Socket.IO can
105 | keep track of which communications to send to which browser.
106 |
107 | ## The Code – Server Side Setup
108 |
109 | The Express and Socket.IO modules do not come packaged with Node.js. they are
110 | external dependencies that must be downloaded and installed separately. Node
111 | Package Manager (npm) will do that for us with the`npm install` command as long
112 | as all the dependencies are listed in the package.json file. You can find
113 | package.json in the root of the project folder, and it contains the following
114 | JSON snippet to define the project dependencies:
115 |
116 | "dependencies": {
117 | "express": "3.x",
118 | "socket.io":"0.9"
119 | }
120 |
121 | The [index.js][7] file in the root of the project is the main entry point for
122 | the entire application. The first few lines instantiate all the required modules.
123 | Express is configured to serve static files, and Socket.IO is set up to listen
124 | on the same port as Express. The following lines show the bare minimum to get
125 | the server going.
126 |
127 | // Create a new Express application
128 | var app = express();
129 |
130 | // Create an http server with Node's HTTP module.
131 | // Pass it the Express application, and listen on port 8080.
132 | var server = require('http').createServer(app).listen(8080);
133 |
134 | // Instantiate Socket.IO hand have it listen on the Express/HTTP server
135 | var io = require('socket.io').listen(server);
136 |
137 | All of ther server-side code specific to the game is in [agxgame.js][8]. This
138 | file is pulled into the application as a Node module with
139 | `var agx = require('./agxgame')`. When a client connects to the application via
140 | Socket.IO, the`initGame` function of the agxgame module should run. The
141 | following code will make that happen.
142 |
143 | io.sockets.on('connection', function (socket) {
144 | //console.log('client connected');
145 | agx.initGame(io, socket);
146 | });
147 |
148 | The `initGame` function in the agxgame module will set up the *event listeners*
149 |
150 | gameSocket.on('hostCreateNewGame', hostCreateNewGame);
151 |
152 | In the code above, the `gameSocket` is a reference to an object created by
153 | Socket.IO to encapsulate functionality regarding the unique socket connection
154 | between the server and the connected browser. The `on` function creates a
155 | listener for a specific event and binds it to a function. So when the browser*
156 | emits* an event named `hostCreateNewGame` through the websocket, Socket.IO will
157 | then execute the`hostCreateNewGame` function. Names of events and functions don
158 | ’t have to be the same, it is just a nice convenience.
159 |
160 | ## The Code – Client Side Setup
161 |
162 | When a web browser connects to the game, it is served index.html from contents
163 | of the “public” folder, which consists of an empty div with an id of*gameArea*.
164 | Also within index.html are several HTML snippets contained within
165 | `
174 |
175 | The majority of the game logic, and all the client-side code is stored within
176 | [app.js][9]. The code is enclosed within a self executing function, and is
177 | organized using an*object literal namespacing* pattern. This means that all the
178 | variables and functions used in the application are properties of the`IO` and
179 | `App` objects. The basic outline for the client-side code is as follows:
180 |
181 | // Enclosing function
182 | function() {
183 |
184 | IO {
185 | All code related to Socket.IO connections goes here.
186 | }
187 |
188 | App {
189 | Generic game logic code.
190 |
191 | Host {
192 | Game logic for the 'Host' (big) screen.
193 | }
194 |
195 | Player {
196 | Game logic specific to 'Player' screens.
197 | }
198 | }
199 | }
200 |
201 | When the application first loads, there are two functions that execute when the
202 | document is ready
203 | – `IO.init()` and `App.init()`. The first will set up the Socket.IO connection
204 | , and the latter will display the title screen in the browser.
205 |
206 | The following line of code in `IO.init()` will initiate a Socket.IO connection
207 | between the browser and the server:
208 |
209 | IO.socket = io.connect();
210 |
211 | Following that, a call is made to `IO.bind()`, which sets up all the event
212 | listeners for Socket.IO on the client. These event listeners work just like
213 | those that we set up on the server, but work in the opposite direction. Take the
214 | following example:
215 |
216 | IO.socket.on('playerJoinedRoom', IO.playerJoinedRoom );
217 |
218 | In the above line of code, `IO` is a container object used for organizing code
219 | , the`socket` object is created by Socket.IO and has properties and methods
220 | related to the websocket connection, and the`on` function creates an event
221 | listener. In this case, when the server emits an event labeled
222 | `playerJoinedRoom`, then the `IO.playerJoinedRoom` function is executed on the
223 | client.
224 |
225 | ## Client-Server Communication
226 |
227 | The gameplay of Anagrammatix is quite simple. There are not many rules, no
228 | graphics or animation, and there are never more than a few words on any screen
229 | at a time. What makes this application a cohesive game and (arguably) fun is the
230 | communication between the three browser windows. It’s important to re-iterate
231 | that browsers do not communicate directly with each other, but rather emit data
232 | to the server, which is then processed and re-emitted back to the appropriate
233 | browsers. Each of these events carries data with it, so information from the
234 | client browser such as the player name or selected answer can travel to the
235 | server and make its way into other clients.
236 |
237 | All of this data flowing from client to server and back again can be a bit
238 | difficult to follow, especially with three clients connected at once. Luckily
239 | there is a great tool available in Google’s Chrome browser to help out. If you
240 | open the Chrome developer tools window and go to the*Network* tab, you will be
241 | able to monitor all websocket traffic for that particular window by clicking the
242 | *WebSockets* button along the bottom toolbar. Within the WebSockets panel, a
243 | list of connections appear in the left-hand column. Clicking on the connection,
244 | and then clicking the*Frames* tab will display a list of communications that
245 | have occurred through that particular websocket connection. See the screenshot
246 | below for an example
247 |
248 | ![chrome_network][10]
249 |
250 | The first column of the *Frames* tab is the most interesting. It shows the data
251 | that is passed through the websocket connection. In the screenshot above, each
252 | object in the data column represents and event that was emitted. Each object has
253 | a “name” and an “args” property. The value for the*name* is the name of the
254 | event being emitted, such as`playerJoinedRoom` or `hostCheckAnswer`. The *args*
255 |
256 | The Frames tab in Chrome developer tools, along with liberal use of
257 | `console.log()` makes it a bit easier to debug the application and keep track
258 | of events and data passing between clients and the server.
259 |
260 | ## The Gameplay
261 |
262 | Now that we have a brief overview of how the game is set up and how the
263 | underlying architecture works, let’s step through the actual gameplay and trace
264 | the flow of a single playthrough of a game of Anagrammatix.
265 |
266 | ### Open a Browser and Visit the Application URL
267 |
268 | **Node.js and Express serve the files from the public folder.** The index.html
269 | file is loaded, along with[app.js][9] and the required client-side JavaScript
270 | libraries.
271 |
272 | **`IO.init()` and `App.init()` are called in [public/app.js][11].** The client
273 | side game code is initialized. Event handlers are set up for Socket.IO, and code
274 | is initiated to load the title screen. The FastClick lib is loaded to speed up
275 | touch events. The following code in the`App.showInitScreen` function will load
276 | the`intro-screen-template` from index.html into the *gameArea* div so it is
277 | visible to the user.
278 |
279 | App.$gameArea.html(App.$templateIntroScreen);
280 |
281 | Also, the `App.bindEvents` function will create a number of click handlers for
282 | buttons that appear on-screen. The following lines of code create click handlers
283 | for the CREATE and JOIN buttons that appear on the title screen.
284 |
285 | App.$doc.on('click', '#btnCreateGame', App.Host.onCreateClick);
286 | App.$doc.on('click', '#btnJoinGame', App.Player.onJoinClick);
287 |
288 | ### The Create Button is Clicked
289 |
290 | **The `hostCreateNewGame` event is emitted by the client.** The function
291 | `App.Host.onCreateClick` does one thing – emit an event to the server to
292 | indicate that a new game must be created:`IO.socket.emit('hostCreateNewGame')`
293 |
294 | **The server creates a new *room* for the game, and emits the room ID back to
295 | the client.** The concept of a *room* is built into Socket.IO. A room will
296 | collect specific client connections and allow events to be emitted only to the
297 | clients within the room. Rooms are perfect for creating individual games using
298 | one server. Without rooms, it would be much more difficult to figure out which
299 | players and hosts should be connected with each other if multiple people are
300 | trying to initiate lots of games on the same server.
301 |
302 | The following function in [agxgame.js][8] will create a new game room.
303 |
304 | function hostCreateNewGame() {
305 | // Create a unique Socket.IO Room
306 | var thisGameId = ( Math.random() * 100000 ) | 0;
307 |
308 | // Return the Room ID (gameId) and the socket ID (mySocketId) to the browser client
309 | this.emit('newGameCreated', {gameId: thisGameId, mySocketId: this.id});
310 |
311 | // Join the Room and wait for the players
312 | this.join(thisGameId.toString());
313 | };
314 |
315 | In the code above, `this` refers to a unique Socket.IO object storing
316 | connection information for the client. The`join` function will place the client
317 | connection into the specified room. In this case the room ID is a randomly
318 | generated number between 1 and 100000. The room ID is sent back to the client
319 | and re-used as the unique game ID.
320 |
321 | **The client (the Host) detects the `newGameCreated` event from server,
322 | displays the room id as the Game ID on screen.** On the client (in
323 | [public/app.js][11]), the `App.Host.gameInit` and
324 | `App.Host.displayNewGameScreen` functions are called in order to display the
325 | randomly generated game ID on screen. The game ID and the Socket.IO room ID are
326 | the same.
327 |
328 | ![start_game][12]
329 |
330 | ### A Player Connects and Clicks Join
331 |
332 | **Display the `join-game-template` HTML template.** When a new window is opened
333 | , the client connects via Socket.IO just as before. If the JOIN button is
334 | clicked, a screen is displayed to collect the player’s name and the desired game
335 | ID.
336 |
337 | ### The First Player Clicks the Start Button
338 |
339 | **The player’s name and game ID get sent to the server.** When a player clicks
340 | ‘Start’ after entering his or her name and the appropriate game ID, that
341 | information is collected into a single*data* object and emitted to the server.
342 | The following code in the`App.Player.onPlayerStartClick` function does this.
343 |
344 | var data = {
345 | gameId : +($('#inputGameId').val()),
346 | playerName : $('#inputPlayerName').val() || 'anon'
347 | };
348 | IO.socket.emit('playerJoinGame', data);
349 |
350 | **On the server, the player is placed into the game room.** When server hears
351 | the`playerJoinGame` event, it calls the `playerJoinGame` function (in
352 | [agxgame.js][13]). This function places the player’s socket connection into
353 | the room specified by the player. It then re-emits the data (the player’s name
354 | and socket ID) to the clients in the room so the Host client knows about the
355 | connected player.
356 |
357 | In the code below (from the `playerJoinGame` function), the `gameSocket` refers
358 | to the global Socket.IO object. It allows us to look up all of the created rooms
359 | on the server. If the room is found, the game continues, otherwise an error is
360 | displayed for the connected player.
361 |
362 | var sock = this;
363 |
364 | // Look up the room ID in the Socket.IO manager object.
365 | var room = gameSocket.manager.rooms["/" + data.gameId];
366 |
367 | // If the room exists...
368 | if( room != undefined ){
369 | // attach the socket id to the data object.
370 | data.mySocketId = sock.id;
371 |
372 | // Join the room
373 | sock.join(data.gameId);
374 |
375 | // Emit an event notifying the clients that the player has joined the room.
376 | io.sockets.in(data.gameId).emit('playerJoinedRoom', data);
377 |
378 | } else {
379 | // Otherwise, send an error message back to the player.
380 | this.emit('error',{message: "This room does not exist."} );
381 | }
382 |
383 | **The player is shown the waiting screen.** When the `playerJoinedRoom` event
384 | is detected by the clients, a message is displayed on screen indicating that the
385 | player joined the game (See screenshot above
386 | ).
387 |
388 | ### Another Player Connects and Clicks Join
389 |
390 | **The same sequence occurs until the `playerJoinedRoom` event is emitted from
391 | the server.** When the `playerJoinedRoom` event is detected by the client, and
392 | the client happens to be a Host screen, then`App.Host.updateWaitingScreen` is
393 | called. This function keeps track of the number of players in the room. If there
394 | are two players, then the Host will emit the`hostRoomFull` event.
395 |
396 | ### The Countdown Begins
397 |
398 | **The server detects the `hostRoomFull` event and emits `beginNewGame`.** The
399 | `beginNewGame` event is emitted from the server to the Host and two connected
400 | players in a specified room with the following code:
401 |
402 | function hostPrepareGame(gameId) {
403 | var sock = this;
404 | var data = {
405 | mySocketId : sock.id,
406 | gameId : gameId
407 | };
408 | // Emit the event ONLY to participants in the room
409 | io.sockets.in(data.gameId).emit('beginNewGame', data);
410 | }
411 |
412 | Remember, the game ID shown to the players and the Socket.IO room ID where the
413 | client connections are stored*are the same thing*. The `in` function will
414 | target a specified room, and`in(...).emit()` will emit an event only to
415 | participants in that room.
416 |
417 | **The Host begins the countdown, and players are informed to “Get Ready”.** On
418 | the client, the`beginNewGame` event is the signal to start the game. The Host
419 | will execute`App.Host.gameCountdown` which loads the `host-game-template` HTML
420 | and displays a five second countdown. On the players’ screen,
421 | `App.Player.gameCountdown` simply displays ‘Get Ready!’.
422 |
423 | ![gameplay][14]
424 |
425 | ### The Game Begins!
426 |
427 | **The countdown ends, and the Host notifies the server to start the game.**
428 | Because the Host client cannot directly communicate with the players it must
429 | signal to the server that the countdown has ended. When the countdown reaches 0,
430 | the Host emits a`hostCountdownFinished` event. The server detects it, and
431 | starts the game by calling the`sendWord` function.
432 |
433 | **The `sendWord` function prepares the data for the next round in the game.**
434 | In this case, it is the first round. The`sendWord` function has two parts,
435 | gathering the data for the round, and emitting that data to the Host and players
436 | in the room.
437 |
438 | The *getWordData* function actually does the heavy lifting of selecting words
439 | for the round and preparing them to be sent to the clients. Word data for each
440 | round is stored in the following format:
441 |
442 | {
443 | "words" : [ "sale","seal","ales","leas" ],
444 | "decoys" : [ "lead","lamp","seed","eels","lean","cels","lyse","sloe","tels","self" ]
445 | }
446 |
447 | Before each round, the `words` array is randomly shuffled. The first element is
448 | selected as the word displayed on the Host screen. The second element is
449 | selected as the correct answer. The`decoys` array is also randomly shuffled,
450 | and the first five elements selected as incorrect answers.
451 |
452 | The correct answer is then inserted into a random spot in the list of incorrect
453 | answers. This data is emitted back to the client in the following format (see
454 | the`getWordData` function in [agxgame.js][15]):
455 |
456 | wordData = {
457 | round: i, // The current round in the game.
458 | word : words[0], // Displayed Word
459 | answer : words[1], // Correct Answer
460 | list : decoys // Word list for player (decoys and answer)
461 | };
462 |
463 | **The Host displays the word on screen.** The `App.Host.newWord` function will
464 | display the word in big letters on the screen, and store a reference to the
465 | correct answer and the number indicating the current round.
466 |
467 | **The player is shown a list of words.** The `App.Player.newWord` function will
468 | parse the list of decoys (which contains the correct answer, as well) and
469 | construct an unordered list of button elements to display on the players’
470 | screens.
471 |
472 | ### A Player Taps a Word
473 |
474 | **The tapped word is sent to the server, and re-emitted to the Host.** Each of
475 | the word buttons displayed to the players has a click handler that will call the
476 | `onPlayerAnswerClick` function when clicked or tapped. The following code will
477 | collect the necessary data, and emit it to the server:
478 |
479 | var $btn = $(this); // the tapped button
480 | var answer = $btn.val(); // The tapped word
481 |
482 | // Send the player info and tapped word to the server so
483 | // the Host can check the answer.
484 | var data = {
485 | gameId: App.gameId, // Also the room ID
486 | playerId: App.mySocketId,
487 | answer: answer,
488 | round: App.currentRound
489 | }
490 | IO.socket.emit('playerAnswer',data);
491 |
492 | **The `playerAnswer` event is detected by the server, and the answer is emitted
493 | to the Host.** The server emits the `hostCheckAnswer` event and attaches the
494 | player’s answer.
495 |
496 | ### The Host Verifies the Answer
497 |
498 | **The Host checks the player’s answer for the current round.** The
499 | `App.Host.checkAnswer` function first checks to see if the answer came from the
500 | current round. This helps avoid timing problems where one player taps a correct
501 | answer, and the second player taps the same answer immediately afterwards.
502 | Because the first player tapped the correct answer, the round should advance,
503 | and the second player should not get credit (or a penalty) for the tapped answer.
504 |
505 | If the correct answer is tapped, the player’s score is incremented and the
506 | `hostNextRound` event is emitted. If the incorrect answer is tapped, the player
507 | ’s score is decremented, and play continues for the current round.
508 |
509 | ### The Game Ends When the Wordpool is Exhausted
510 |
511 | **The `hostNextRound` function on the server will determine if the game is over
512 | .** If the number of the current round is less than the number of elements in
513 | the`wordPool` array, then the game continues with the next round. If the
514 | current round exceeds the number of words available, then the server will emit
515 | the`gameOver` event to the clients connected in the room.
516 |
517 | **The Host displays the winner.** The `App.Host.endGame` function is called on
518 | the client when the server emits the`gameOver` event. In that function, the
519 | Host checks to see which player has the highest score. The winning player’s name
520 | is then displayed on the screen.
521 |
522 | Also, the `App.Host.numPlayersInRoom` variable is reset to 0 (even though
523 | technically the players’ socket connections are still in the actual Socket.IO
524 | room). This is done to allow players a chance to begin a new game. The
525 | `App.Host.isNewGame` flag is set to true as well to indicate that a new game is
526 | about to begin.
527 |
528 | ![game_over][16]
529 |
530 | ### The Players Start Again
531 |
532 | **The player taps the *Start Again* button and the client emits the
533 | `playerRestart` event.** The server will notify the Host that a player wishes
534 | to play again by re-emitting the`playerJoinedRoom` event. The function
535 | `App.Host.updateWaitingScreen` will check the `App.Host.isNewGame` flag, see
536 | that is is true, and display the`create-game-template` on screen until the next
537 | player joins the game.
538 |
539 | When the second player clicks the *Start Again* button, the same sequence
540 | occurs and the game begins anew.
541 |
542 | ## In Conclusion
543 |
544 | This application only touches on a couple of the important concepts within the
545 | Socket.IO library and in building real-time web applications with Node.js. If
546 | you have never worked with real time web applications, hopefully this has been a
547 | good introduction to get you excited about learning more. The
548 | [Socket.IO website][17] provides a decent amount of documentation, as does the
549 | [Socket.IO wiki][18] on GitHub.
550 |
551 | [1]: http://flippinawesome.org/authors/eric-terpstra
552 | [2]: https://github.com/ericterpstra/anagrammatix
553 | [3]: https://github.com/STRML/textFit
554 | [4]: https://github.com/ftlabs/fastclick
555 | [5]: img/architecture.png
556 | [6]: http://socket.io/#how-to-use
557 | [7]: https://github.com/ericterpstra/anagrammatix/blob/master/index.js
558 | [8]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js
559 | [9]: https://github.com/ericterpstra/anagrammatix/blob/master/public/app.js
560 | [10]: img/chrome_network-640x360.jpg
561 | [11]: https://github.com/ericterpstra/anagrammatix/blob/master/public/app.js#L16
562 | [12]: img/start_game.jpg
563 | [13]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js#L95
564 | [14]: img/gameplay.jpg
565 | [15]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js#L171
566 | [16]: img/game_over.jpg
567 | [17]: http://socket.io/
568 | [18]: https://github.com/learnboost/socket.io/wiki
569 |
--------------------------------------------------------------------------------
/rus.md:
--------------------------------------------------------------------------------
1 | # Разработка многопользовательских игр с помощью Node.js и Socket.IO
2 |
3 | ![Шапка][То, как выглядит игра]
4 |
5 | С выходом Ouya, Xbox One и PS4 в этом году диванно-консольный гейминг
6 | становится популярным как никогда. Несмотря на распространение
7 | многопользовательских игр, доступных на мобильных устройствах, и даже на
8 | распространение опыта многопользовательского взаимодействия в вебе, все это не
9 | заменит удовольствия от игры бок о бок с другом. Работая с Node.js и
10 | библиотекой Socket.IO, я обнаружил прекрасную возможность не только изучить
11 | что-то новое, но также поэкспериментировать с использованием веб-технологий и
12 | различных устройств (мобильные гаджеты и ноутбуки) для воссоздания опыта
13 | игр на приставках.
14 |
15 | Эта статья даст вам краткий обзор фундаментальных основ работы библиотеки
16 | Socket.IO в контексте разработки многопользовательской игры в слова,
17 | использующей несколько экранов. Браузер на устройстве с большим экраном, таком
18 | как телевизор или компьютер, будет «приставкой», а мобильный браузер будет
19 | выступать в роли контроллера. Socket.IO и Node.js обеспечат необходимую связь
20 | между браузерами для передачи данных и обеспечения игрового взаимодействия в
21 | реальном времени.
22 |
23 | ## Игра «Анаграмматикс»
24 |
25 | Игра представляет из себя соревнование по разгадыванию анаграмм — слов, при
26 | перестановке букв в которых образуются новые слова. Геймплей достаточно
27 | прост — на основном экране появляется слово, а на каждом из
28 | контроллеров-смартфонов наших игроков появляется список похожих по написанию
29 | слов. Одно из появившихся слов — анаграмма к слову на основном экране. Игрок,
30 | который первым правильно выбрал анаграмму, получает очки за раунд. Выбор
31 | неправильного слова уменьшает суммарное количество очков игрока.
32 | После завершения 10 раундов побеждает игрок, набравший большее количество очков.
33 |
34 | Перед тем, как скачать исходный код, чтобы поиграть самостоятельно, убедитесь,
35 | что у вас установлен Node.js. Откройте [репозиторий игры на GitHub][2],
36 | клонируйте его с помощью команды
37 | `git clone https://github.com/ericterpstra/anagrammatix.git` и затем
38 | выполните команду `npm install`, чтобы скачать и установить все зависимости
39 | (Socket.IO и Express). После этого выполните `node index.js`,
40 | для запуска сервера игры. Чтобы начать непосредственно саму игру, откройте
41 | в браузере адрес `http://localhost:8080`. Для проверки возможности
42 | многопользовательской игры вам потребуется открыть несколько окон браузера.
43 |
44 | Если вы хотите поиграть, используя мобильные устройства, вам необходимо узнать
45 | IP-адрес вашего компьютера в локальной сети. Само собой, мобильные устройства
46 | должны находиться в той же сети, что и компьютер с запущенным приложением игры.
47 | Для начала игры просто замените ‘localhost’ в адресной строке на IP-адрес
48 | компьютера (например, `http://192.168.0.5:80801). Если вы используете
49 | Windows, то вам необходимо отключить фаерволл Windows или открыть порт 8080.
50 |
51 | ## Используемые технологии
52 |
53 | Для реализации игры «Анаграмматикс» используются только HTML, CSS и JavaScript.
54 | Чтобы сохранить код игры насколько это возможно простым, использование различных
55 | сторонних библиотек и фреймворков было сведено к минимуму. Основные
56 | технологии, используемые в игре, перечислены ниже:
57 |
58 | * **Node.js** - используется для создания серверной части игры и позволяет
59 | использовать фреймворк Express и библиотеку Socket.IO. Кроме того, часть
60 | логики игры реализуется на стороне сервера и оформлена в виде отдельного
61 | Node-модуля, о котором мы поговорим позже.
62 | * **Express** - сам по себе Node.js удовлетворяет многим требованиям,
63 | предъявляемым при разработке современных веб-приложений. Однако, добавление
64 | библиотеки Express позволит нам оптимизировать отдачу статичных файлов (HTML,
65 | CSS и JavaScript) пользователям. Мы также используем Express для логирования
66 | и реализации окружения, совместимого с Socket.IO.
67 | * **Socket.IO** - эта библиотека позволяет очень просто реализовать обмен
68 | данными между браузером и сервером (в данном случае представляющим собой
69 | связку Node.js и Express) в реальном времени. Socket.IO использует протокол
70 | *веб-сокетов*, если он поддерживается браузером по умолчанию. Старые браузеры,
71 | такие как IE9, не поддерживают этот протокол — в этом случае Socket.IO будет
72 | использовать сокеты через Flash или AJAX-технологию *long-polling*.
73 | * Клиентский JavaScript – для простоты, на фронтенде используется несколько
74 | JS-библиотек:
75 | * jQuery – в основном используется для обработки событий и манипуляций с
76 | DOM.
77 | * [TextFit][3] - небольшая библиотека, изменяющая размер текста таким
78 | образом, чтобы он заполнял свой контейнер. Использование этой библиотеки
79 | позволяет заполнять браузер главным экраном «Анаграмматикс» целиком, вне
80 | зависимости от ширины экрана устройства.
81 | * [FastClick.js][4] – небольшая библиотека, убирающая задержку в 300
82 | миллисекунд между тапом по экрану и обработкой события на мобильных
83 | устройствах. Это позволит сделать наш игровой контроллер более отзывчивым.
84 | * CSS – обычный CSS для того, что бы добавить стиль и немного красок в нашу игру.
85 |
86 | ## Архитектура
87 |
88 | ![Архитектура игры][Рисунок 1]
89 |
90 | В целом, архитектура игры соответствует рекомендованной конфигурации по
91 | использованию Socket.IO вместе с Express, доступной на [сайте Socket.IO][6]. В
92 | этой конфигурации Express используется для обработки HTTP-запросов,
93 | отправляемых в Node-приложение. Модуль Socket.IO подключается к Express и
94 | начинает отслеживать подключения к тому же порту, ожидая входящих
95 | websocket-соединений.
96 |
97 | Когда браузер подключается к веб-приложению, оно возвращает ему index.html и
98 | все необходимые для начала работы JavaScript- и CSS-файлы. Клиентская часть
99 | приложения подключается к Socket.IO и создаёт websocket-соединение. Каждое
100 | websocket-соединение обладает уникальным идентификатором для того, чтобы Node
101 | и Socket.IO могли работать с несколькими соединениями и отправлять данные
102 | соответствующему клиенту.
103 |
104 | ## Разработка серверной части
105 |
106 | Модули Express и Socket.IO не входят в Node.js. Они являются внешними
107 | зависимостями, которые должны быть загружены и установлены отдельно. Менеджер
108 | пакетов Node.js (npm) сделает это за нас при запуске команды `npm install`. То
109 | же самое он сделает со всеми зависимостями, перечисленными в файле
110 | `package.json`. Файл `package.json` вы можете найти в корневой директории
111 | проекта. Он содержит определяющий зависимости проекта JSON-объект:
112 |
113 | "dependencies": {
114 | "express": "3.x",
115 | "socket.io":"0.9"
116 | }
117 |
118 | Файл [index.js][7] в корневой директории проекта — это точка входа для всего
119 | приложения. Первые несколько строк инициализируют все необходимые модули.
120 | Express настроен для обеспечения доступа к статическим файлам, а модуль
121 | Socket.IO настроен так, чтобы отслеживать подключения к тому же порту,
122 | что и Express. Следующие строки — основа приложения, необходимая
123 | для работы сервера.
124 |
125 | // Создаем приложение с помощью Express
126 | var app = express();
127 |
128 | // Создаем HTTP-сервер с помощью модуля HTTP, входящего в Node.js.
129 | // Связываем его с Express и отслеживаем подключения к порту 8080.
130 | var server = require('http').createServer(app).listen(8080);
131 |
132 | // Инициализируем Socket.IO так, чтобы им обрабатывались подключения
133 | // к серверу Express/HTTP
134 | var io = require('socket.io').listen(server);
135 |
136 | Весь код серверной части игры вынесен в отдельный файл [agxgame.js][8]. Этот
137 | файл подключается в приложение в качестве модуля Node с помощью следующего
138 | фрагмента кода: `var agx = require('./agxgame')`. Когда клиент подключается к
139 | приложению с помощью Socket.IO, модуль `agxgame` должен выполнять функцию
140 | `initGame`. За это отвечает следующий фрагмент кода:
141 |
142 | io.sockets.on('connection', function (socket) {
143 | //console.log('client connected');
144 | agx.initGame(io, socket);
145 | });
146 |
147 | Функция `initGame` в модуле `agxgame` также добавит слушатель событий:
148 |
149 | gameSocket.on('hostCreateNewGame', hostCreateNewGame);
150 |
151 | Здесь `gameSocket` — это объект, созданный с помощью Socket.IO, чтобы
152 | инкапсулировать взаимодействие относительно уникального подключения через
153 | сокет между сервером и браузером. Функция `on` добавляет слушатель для
154 | определенного события и привязывает к нему функцию. Когда браузер *передает*
155 | событие `hostCreateNewGame` через веб-сокет, библиотека Socket.IO
156 | вызывает функцию `hostCreateNewGame`. Имена событий и функций не
157 | обязательно должны быть такими, мы назвали их так для наглядности.
158 |
159 | ## Разработка клиентской части
160 |
161 | Когда браузер подключается к игре, сервер отдает ему файл `index.html` из
162 | директории `public`, который содержит пустой `div` с id *gameArea*. В
163 | `index.html` также содержится несколько HTML сниппетов внутри тегов
164 | `
172 |
173 | Большая часть логики игры и весь клиентский код расположен в файле [app.js][9].
174 | Код заключен в самовызывающуюся функцию и организован с применением паттерна
175 | *объектно-буквенного обозначения пространства имен*. Это означает, что все
176 | переменные и функции, используемые в приложении, являются свойствами объектов
177 | `IO` и `App`. Структура клиентского кода выглядит примерно так:
178 |
179 | // Функция-замыкание
180 | function() {
181 |
182 | IO {
183 | Здесь располагается весь код, относящийся
184 | к использованию Socket.IO
185 | }
186 |
187 | App {
188 | Здесь располагается основная логика приложения
189 |
190 | Host {
191 | Логика игры для основного экрана.
192 | }
193 |
194 | Player {
195 | Логика игры для экранов игроков.
196 | }
197 | }
198 | }
199 |
200 | При первом запуске приложения после окончания загрузки документа вызываются
201 | 2 функции: `IO.init()` и `App.init()`. Первая настраивает подключение через
202 | Socket.IO, вторая показывает стартовый экран игры в браузере.
203 |
204 | Следующая строка в `IO.init()` инициализирует подключение через Socket.IO
205 | между браузером и сервером:
206 |
207 | IO.socket = io.connect();
208 |
209 | Вслед за этим вызывается функция `IO.bind()`, которая добавляет слушатель
210 | событий Socket.IO на клиенте. Этот слушатель работает аналогично слушателю на
211 | стороне сервера, но в обратном направлении. Обратите внимание на следующий
212 | пример:
213 |
214 | IO.socket.on('playerJoinedRoom', IO.playerJoinedRoom );
215 |
216 | В приведенном фрагменте `IO` — это объект-контейнер, используемый для
217 | организации кода. Объект `socket` создается библиотекой Socket.IO и содержит
218 | свойства и методы для подключения с использованием вебсокетов, а функция `on`
219 | добавляет слушатель события. Когда сервер передает событие `playerJoinedRoom`,
220 | на клиенте выполняется функция `IO.playerJoinedRoom`.
221 |
222 | ## Коммуникация между клиентом и сервером
223 |
224 | Геймплей «Анаграмматикс» достаточно прост. Здесь нет большого количества
225 | файлов, нет графики или анимации, и нет ничего кроме нескольких слов на экране.
226 | То, что в действительности делает это приложение настоящей интерактивной игрой
227 | и (я надеюсь) добавляет веселья, так это взаимодействие между 3 окнами браузера.
228 | Важно отметить, что эти 3 браузера не связываются напрямую между
229 | собой, а отправляют данные на сервер, который их обрабатывает и возвращает
230 | ответ соответствующему браузеру. Каждое из событий подразумевает передачу
231 | данных, поэтому информация из клиентского браузера, такая, как имя игрока и
232 | выбранный ответ, должна передаваться на сервер, а затем другим клиентам.
233 |
234 | Возможно, проследить за данными, передаваемыми от клиента серверу и обратно,
235 | довольно сложно, особенно при одновременном подключении сразу 3 клиентов.
236 | К счастью, в Google Chrome есть отличный инструмент, который нам в этом
237 | поможет. Если вы откроете панель инструментов разработчика в Chrome и
238 | перейдете на вкладку «Network» (Сеть), вы сможете следить за всем трафиком,
239 | идущим через вебсокеты в этом отдельном окне, выбрав пункт *WebSockets* на
240 | панели инструментов внизу.
241 | На открывшейся панели вебсокетов, в левой колонке, появляется список
242 | соединений. Кликнув на соединении в списке, и затем кликнув на вкладку
243 | *Frames*, вы увидите список сеансов обмена данными, которые производились
244 | через это отдельное соединение. Обратите внимание на следующий пример:
245 |
246 | ![Панель Network в инструментах разработчика Google Chrome][Рисунок 2]
247 |
248 | Наибольший интерес для нас представляет первая колонка на вкладке *Frames*.
249 | Она показывает данные, которые прошли через вебсокет-соединение.
250 | На изображении выше каждый объект в колонке Data представляет передаваемые данные
251 | и названия событий, которые произошли. Каждый объект имеет свойства `name` и
252 | `args`. Значение свойства `name` — это название прозошедшего события,
253 | например, `playerJoinedRoom` или `hostCheckAnswer`. Значение свойства `args`
254 | содержит массив данных, передаваемых между клиентом и сервером.
255 |
256 | Вкладка «Frames» в инструментах разработчика Chrome вместе с использованием
257 | `console.log()` позволяют проще отлаживать приложение и следить за событиями и
258 | передачей данных между клиентом и сервером.
259 |
260 | ## Геймплей
261 |
262 | Теперь, когда мы понимаем принципы организации архитектуры игры и то, как она
263 | реализована, давайте перейдем к геймплею и разберем по частям процесс
264 | одной игровой сессии в «Анаграмматикс».
265 |
266 | ### Пользователь открывает браузер и переходит по адресу приложения
267 |
268 | **Node.js и Express отдают файлы из директории public.** Файл index.html
269 | загружается вместе с [app.js][9] и необходимыми клиентскими
270 | JavaScript-библиотеками.
271 |
272 | **В [public/app.js][11] вызываются функциии `IO.init()` и `App.init()`.**
273 | Инициализируется клиентская часть игры. Добавляются слушатели событий для
274 | работы с Socket.IO. Происходит инициализация кода для загрузки стартового
275 | экрана. Библиотека FastClick загружается для ускорения обработки тач-событий.
276 | Следующий фрагмент кода внутри функции `App.showInitScreen` переместит в div
277 | *gameArea* HTML-фрагмент `intro-screen-template`, расположенный в index.html,
278 | тем самым показав его пользователю.
279 |
280 | App.$gameArea.html(App.$templateIntroScreen);
281 |
282 | Функция `App.bindEvents` добавит несколько обработчиков события клика для
283 | кнопок, которые появляются на экране. Следующий фрагмент кода добавляет
284 | обработчики события клика для кнопок «Создать» и «Присоединиться», которые
285 | появляются на стартовом экране.
286 |
287 | App.$doc.on('click', '#btnCreateGame', App.Host.onCreateClick);
288 | App.$doc.on('click', '#btnJoinGame', App.Player.onJoinClick);
289 |
290 | ### Пользователь кликает по кнопке «Создать»
291 |
292 | **На клиенте срабатывает событие `hostCreateNewGame`.** Функция
293 | `App.Host.onCreateClick` выполняет всего одно действие — передает событие на
294 | сервер, чтобы сообщить, что необходимо создать новую игру:
295 | `IO.socket.emit('hostCreateNewGame')`.
296 |
297 | **Сервер создает новое *игровое лобби* и возвращает идентификатор лобби
298 | клиенту.** Концепция *игрового лобби* уже реализована в Socket.IO. Лобби
299 | объединяет определенные клиентские подключения и обеспечивает передачу событий
300 | только тем клиентам, которые в данный момент «находятся» в одном лобби.
301 | Данная технология отлично подходят для создания отдельных игровых партий при
302 | использовании одного сервера. Без неё будет гораздо сложнее определить, какие
303 | игроки и хосты (основные экраны) должны быть соединены друг с другом, когда
304 | некоторое количество людей пытаются запустить несколько игр на одном сервере.
305 |
306 | Следующая функция внутри [agxgame.js][8] создаст новое игровое лобби:
307 |
308 | function hostCreateNewGame() {
309 | // Создаем уникальное лобби Socket.IO
310 | var thisGameId = ( Math.random() * 100000 ) | 0;
311 |
312 | // Вернем идентификатор лобби (gameId) и идентификатор сокета (mySocketId)
313 | // в браузер клиента
314 | this.emit('newGameCreated', {gameId: thisGameId, mySocketId: this.id});
315 |
316 | // Присоединяемся к лобби и ожидаем подключения других пользователей
317 | this.join(thisGameId.toString());
318 | };
319 |
320 | В приведенном фрагменте кода `this` — это уникальный объект Socket.IO,
321 | хранящий информацию о подключении клиента. Функция `join` «переносит»
322 | подключение клиента в указанное лобби. В данном случае его идентификатор
323 | определяется путем генерации случайного числа в диапазоне от 1 до 100000.
324 | Идентификатор лобби отправляется обратно клиенту и используется в качестве
325 | уникального идентификатора для игровой сессии.
326 |
327 | **Клиент (основной экран) получает с сервера событие `newGameCreated` и
328 | отображает идентификатор лобби как идентификатор игры на экране.** Чтобы
329 | показать сгенерированный случайный идентификатор игры на экране, на клиенте
330 | (внутри [public/app.js][11]) вызываются функции `App.Host.gameInit` и
331 | `App.Host.displayNewGameScreen`. Идентификатор игры и идентификатор лобби
332 | Socket.IO совпадают.
333 |
334 | ![Начало игры][Рисунок 3]
335 |
336 | ### Игрок подключается к игре и нажимает «Присоединиться»
337 |
338 | **В этот момент отображается HTML-шаблон `join-game-template`.** Когда
339 | открывается новое окно, клиент, как и прежде, подключается через Socket.IO.
340 | Если игрок нажимает на кнопку «Присоединиться», появляется экран, на котором
341 | игроку необходимо ввести имя и идентификатор игры, к которой он хочет
342 | присоединиться.
343 |
344 | ### Первый игрок нажимает на кнопку «Старт»
345 |
346 | **Имя игрока и идентификатор игры отправляются на сервер.** Когда игрок
347 | нажимает на кнопку «Старт» после ввода его (или ее) имени и соответствующего
348 | идентификатора игры, эта информация собирается в один объект *data* и
349 | отправляется на сервер. За это отвечает следующий фрагмент кода в функции
350 | `App.Player.onPlayerStartClick`:
351 |
352 | var data = {
353 | gameId : +($('#inputGameId').val()),
354 | playerName : $('#inputPlayerName').val() || 'anon'
355 | };
356 | IO.socket.emit('playerJoinGame', data);
357 |
358 | **На сервере игрок переносится в игровое лобби.** Когда сервер получает
359 | событие `playerJoinGame`, он вызывает функцию `playerJoinGame`
360 | (см. [agxgame.js][13]). Эта функция переносит сокет-подключение пользователя в
361 | выбранное игроком лобби. Затем она отправляет в ответ данные (имя игрока и
362 | идентификатор сокета) клиентам в этом лобби, так основной экран получает
363 | информацию о подключенных игроках.
364 |
365 | В приведенном ниже фрагменте кода (этот код выполняется внутри функции
366 | `playerJoinGame`) `gameSocket` относится к глобальному объекту Socket.IO. Он
367 | позволяет осуществлять поиск по всем созданным на сервере лобби. Если
368 | указанное лобби найдено, игра продолжается, иначе подключенный игрок
369 | получает сообщение об ошибке.
370 |
371 | var sock = this;
372 |
373 | // Поиск идентификатора лобби в объекте-менеджере Socket.IO.
374 | var room = gameSocket.manager.rooms["/" + data.gameId];
375 |
376 | // Если комната существует...
377 | if( room != undefined ){
378 | // Добавим идентификатор сокета в объект данных.
379 | data.mySocketId = sock.id;
380 |
381 | // Переместим пользователя в лобби
382 | sock.join(data.gameId);
383 |
384 | // Вызовем событие, оповещающее клиентов о подключении игрока к лобби.
385 | io.sockets.in(data.gameId).emit('playerJoinedRoom', data);
386 |
387 | } else {
388 | // В противном случае отправим игроку сообщение об ошибке.
389 | this.emit('error',{message: "Указанного лобби не существует."} );
390 | }
391 |
392 | **Игрок видит экран ожидания.** Когда клиент получает событие
393 | `playerJoinedRoom`, на экране отображается сообщение о подключении
394 | пользователя к игре (см. скриншот выше).
395 |
396 | ### Другой игрок подключается и нажимает «Присоединиться»
397 |
398 | **Повторяются те же действия, что и в случае с первым игроком до момента
399 | получения с сервера события `playerJoinedRoom`.** При получении клиентом,
400 | который является основным экраном, события `playerJoinedRoom` вызывается
401 | функция `App.Host.updateWaitingScreen`. Эта функция обрабатывает информацию о
402 | количестве игроков в лобби. Если игроков двое, клиент основного экрана
403 | отправляет событие `hostRoomFull`.
404 |
405 | ### Начинается обратный отсчет
406 |
407 | **Сервер получает событие `hostRoomFull` и отправляет событие `beginNewGame`.**
408 | Событие `beginNewGame` отправляется сервером основному экрану и двум
409 | подключенным игрокам в определенном лобби с помощью следующего кода:
410 |
411 | function hostPrepareGame(gameId) {
412 | var sock = this;
413 | var data = {
414 | mySocketId : sock.id,
415 | gameId : gameId
416 | };
417 | // Событие отправляется только тем игрокам, которые находятся в указанном лобби
418 | io.sockets.in(data.gameId).emit('beginNewGame', data);
419 | }
420 |
421 | Обратите внимание на то, что идентификатор игры, который видят игроки,
422 | и идентификатор лобби Socket.IO, в которой хранятся подключения клиентов,
423 | *совпадают*. Функция `in` указывает на определенное лобби, а
424 | `in(...).emit()` отправляет событие только тем пользователям, которые находятся в этом лобби.
425 |
426 | **Хост начинает обратный отсчет, а игрокам отправляется сообщение
427 | «Приготовьтесь».** На клиенте событие `beginNewGame` является сигналом к
428 | началу новой игры. На клиенте, который является основным экраном, выполняется
429 | функция `App.Host.gameCountdown`, которая загружает шаблон `host-game-template`
430 | и показывает 5-секундный таймер обратного отсчета. На экранах игроков функция
431 | `App.Player.gameCountdown` просто показывает сообщение «Приготовьтесь».
432 |
433 | ![Геймплей][Рисунок 4]
434 |
435 | ### Игра начинается
436 |
437 | **Когда обратный отсчет заканчивается, хост (клиент основного экрана) сообщает
438 | серверу о том, чтоб необходимо начать игру.** Так как хост не может напрямую
439 | общаться с игроками, он должен сообщить серверу, что обратный отсчет завершен.
440 | Когда обратный отсчет достигает значения 0, хост передает событие
441 | `hostCountdownFinished`. Сервер обрабатывает это событие и начинает игру путем
442 | вызова функции `sendWord`.
443 |
444 | **Функция `sendWord` подготавливает данные для следующего раунда игры.** В
445 | данном случае это первый раунд. Функция `sendWord` состоит из двух частей:
446 | собирающей данные для раунда и передающей эти данные на основной экран и
447 | игрокам в лобби.
448 |
449 | Функция *getWordData* занимается тяжелой работой по выбору слов для раунда и
450 | подготовкой их к отправке клиентам. Данные для каждого раунда хранятся в
451 | следующем формате:
452 |
453 | {
454 | "words" : [ "sale","seal","ales","leas" ],
455 | "decoys" : [ "lead","lamp","seed","eels","lean","cels","lyse","sloe","tels","self" ]
456 | }
457 |
458 | Перед каждым раундом массив `words` перемешивается. Первый элемент выбирается
459 | в качестве слова для отображения на экране. Второй элемент выбирается в
460 | качестве правильного ответа. Массив `decoys` также перемешивается, и первые 5
461 | элементов добавляются в качестве неправильных ответов.
462 |
463 | Затем правильный ответ вставляется в случайное место в списке неправильных
464 | ответов. Эти данные отправляются обратно клиенту в следующем формате (см.
465 | функцию `getWordData` в [agxgame.js][15]):
466 |
467 | wordData = {
468 | round: i, // Текущий раунд игры
469 | word : words[0], // Отображаемое на основном экране слово
470 | answer : words[1], // Правильный ответ
471 | list : decoys // Список слов для игроков (массив, содержащий правильный и неправильные ответ)
472 | };
473 |
474 | **На основном экране появляется слово.** Функция `App.Host.newWord` выводит
475 | слово большими буквами на экране, а также хранит правильный ответ и номер
476 | текущего раунда.
477 |
478 | **Игрокам показывается список слов.** Функция `App.Player.newWord` анализирует
479 | список вариантов ответа (который содержит и правильный ответ) и формирует
480 | ненумерованный список элементов `button` для отображения на экранах игроков.
481 |
482 | ### Игрок выбирает вариант ответа
483 |
484 | **Слово, на кнопку с которым нажал пользователь, отправляется на сервер и
485 | передается на клиент основного экрана.** Каждая из отображаемых на экране
486 | игрока кнопок со словами имеет обработчик клика по ней, который вызывает
487 | функцию `onPlayerAnswerClick` при клике или касании. Следующий фрагмент кода
488 | собирает необходимые данные и передает их на сервер:
489 |
490 | var $btn = $(this); // Кнопка, на которую нажал пользователь
491 | var answer = $btn.val(); // Выбранное слово
492 |
493 | // Отправляем информацию об игроке и выбранное слово на сервер
494 | // так, чтобы клиент основного экрана смог проверить правильность ответа
495 | var data = {
496 | gameId: App.gameId, // Отправляем также идентификатор лобби
497 | playerId: App.mySocketId,
498 | answer: answer,
499 | round: App.currentRound
500 | }
501 | IO.socket.emit('playerAnswer',data);
502 |
503 | **Событие `playerAnswer` обрабатывается сервером, и ответ передается на клиент
504 | основного экрана.** Сервер передает событие `hostCheckAnswer`, а вместе с ним
505 | и ответ пользователя.
506 |
507 | ### Клиент основного экрана проверяет правильность ответа
508 |
509 | **Клиент основного экрана проверяет ответ пользователя в текущем раунде.**
510 | Функция `App.Host.checkAnswer` сначала проверяет, является ли отправленный
511 | ответ ответом на текущий раунд. Это помогает избежать проблем с отслеживанием
512 | времени, когда оба игрока выбирают правильный ответ сразу же друг за другом.
513 | Так как первый игрок выбрал правильный ответ, раунд должен завершиться, и
514 | второй игрок не должен получить очки (или штраф) за выбранный вариант.
515 |
516 | Если игрок выбрал правильное слово, сумма очков игрока увеличивается, и
517 | вызывается событие `hostNextRound`. Если выбран неправильный ответ, сумма
518 | очков игрока уменьшается, и игра продолжается в текущем раунде.
519 |
520 | ### Игра заканчивается, когда использован весь набор слов
521 |
522 | **Функция `hostNextRound` определяет, должна ли игра закончиться.** Если номер
523 | текущего раунда меньше, чем количество элементов в массиве `wordPool`, то игра
524 | продолжается в следующем раунде. Если номер текущего раунда превышает
525 | количество доступных слов, то сервер передаст клиентам в комнате событие
526 | `gameOver`.
527 |
528 | **На основном экране появляется имя победителя.** Когда сервер передает
529 | событие `gameOver`, на клиенте вызывается функция `App.Host.endGame`. Эта
530 | функция проверяет, у какого из игроков больше очков. Имя победившего игрока
531 | выводится на основной экран.
532 |
533 | Значение переменной `App.Host.numPlayersInRoom` устанавливается в 0 (даже если
534 | технически сокет-подключения игроков все еще «находятся» в текущем лобби
535 | Socket.IO). Это сделано для того, чтобы позволить игрокам начать новую игру.
536 | Флаг `App.Host.isNewGame` устанавливается в значение `true`, чтобы показать,
537 | что начинается новая игра.
538 |
539 | ![Игра окончена][Рисунок 5]
540 |
541 | ### Игроки начинают заново
542 |
543 | **Игрок нажимает на кнопку «Начать заново», и клиент передает событие
544 | `playerRestart`.** Сервер отправляет клиенту основного экрана уведомление о
545 | том, что игрок хочет сыграть снова, повторно передав событие `playerJoinedRoom`
546 | . Функция `App.Host.updateWaitingScreen` проверяет значение флага
547 | `App.Host.isNewGame`, видит, что оно равно `true`, и выводит на экран шаблон
548 | `create-game-template`, пока другой игрок не подключится к игре.
549 |
550 | Когда второй игрок нажимает на кнопку «Начать заново», повторяется та же
551 | последовательность действий, и игра начинается заново.
552 |
553 | ## В заключение
554 |
555 | Это приложение затрагивает только некоторые важные концепции работы с
556 | библиотекой Socket.IO и принципы разработки приложений с взаимодействием в
557 | реальном времени с помощью Node.js. Если вы никогда не работали с приложениями,
558 | реализующими взаимодействие в реальном времени, надеюсь,
559 | вы получили достаточное количество интересной информации для того, чтобы вам
560 | захотелось изучить эту тему подробнее. Вы можете найти дополнительную
561 | информацию в документации [на сайте Socket.IO][17] или в [Socket.IO wiki][18]
562 | на GitHub.
563 |
564 | [1]: http://flippinawesome.org/authors/eric-terpstra
565 | [2]: https://github.com/ericterpstra/anagrammatix
566 | [3]: https://github.com/STRML/textFit
567 | [4]: https://github.com/ftlabs/fastclick
568 | [6]: http://socket.io/#how-to-use
569 | [7]: https://github.com/ericterpstra/anagrammatix/blob/master/index.js
570 | [8]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js
571 | [9]: https://github.com/ericterpstra/anagrammatix/blob/master/public/app.js
572 | [11]: https://github.com/ericterpstra/anagrammatix/blob/master/public/app.js#L16
573 | [13]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js#L95
574 | [15]: https://github.com/ericterpstra/anagrammatix/blob/master/agxgame.js#L171
575 | [17]: http://socket.io/
576 | [18]: https://github.com/learnboost/socket.io/wiki
577 |
578 | [Рисунок 1]: img/architecture_rus.png
579 | [Рисунок 2]: img/chrome_network-640x360.jpg
580 | [Рисунок 3]: img/start_game.jpg
581 | [Рисунок 4]: img/gameplay.jpg
582 | [Рисунок 5]: img/game_over.jpg
583 | [То, как выглядит игра]: img/socketio_header.jpg
--------------------------------------------------------------------------------