├── 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 --------------------------------------------------------------------------------