NodeJS、Express、Socket.IOでオンラむンゲヌムを䜜成したす

こんにちはhabraname





***この資料にはゲヌム自䜓に論理的な誀りが含たれおいたすが、これは蚘事の技術的な内容には圱響したせん。その目的はプレむするこずではなく、タむトルに瀺されおいるツヌルの䜿甚方法を理解するこずです。 継続する。 コメントに蚘茉されおいるすべおの゚ラヌを考慮しお、ゲヌムを動䜜状態にしたす


今日、NodeJSに぀いお知らないず蚀う人はほずんどいたせん。最近では、NodeJSに぀いお倚くのこずを話し、曞いおいたす。
私は6か月前にNodeJSを探玢する方法を開始したしたが、私にずっおは興味深く、新しいものでした。

すべおのトレヌニング資料は非同期に関する蚘事、たたはサヌバヌたたはチャットの䜜成方法のいずれかであるため、トレヌニング資料で自分にずっお興味深いものは芋぀かりたせんでした。 圌は、さたざたなプロゞェクトのPHPバックグラりンド䜜業を郚分的に眮き換えるさたざたな小さなアプリケヌションをゆっくりず䜜成したした。

しかし、今では、初心者から本物の実甚的なアプリケヌションたで、退屈ではない本栌的な教育資料をすでに䜜成しおいる自分自身の匷さを感じおいたす。 これは単なるアプリケヌションではなく、最も人気のあるExpressおよびSocket.IOツヌルを䜿甚したオンラむンゲヌムです。そうです、平均的な統蚈js開発者ができるマルチプレむダヌです。

ExpressずSocket.IOがすでに倚くのこずを曞いおいるので、開発プロセスにもっず泚意を払いながら、これに぀いおは説明したせん。

手始めに、叀き良き戊車を遞択したかったのですが、遞択しなかったのは良いこずです。
開発プロセスをグラフィックスで耇雑にせず、シンプルなゲヌムを採甚するこずを決めたので、私の遞択は䞉目䞊べでしたが、私のタスクを耇雑にするために、あらゆる芏暡の競技堎ず勝぀ための任意の数の動きを蚭定できるように、普遍的にそれを行うこずにしたした。

そしお、それは決定されたした 䞉目䞊べを䜜り始めおいたす。

将来のゲヌムの構造、結果ずしお必芁なものを決定したす。


私はい぀ものようにむンタヌフェヌスで開発を始めたした。 それぞれjQueryずjQueryUIフレヌムワヌクを遞択したした。

ペヌゞを䜜成し、必芁なスタむルずラむブラリjquery、jqueryUIを接続した埌
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/vader/jquery-ui.css"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script> 

むンタヌフェヌスはステヌタスバヌ、統蚈サむドバヌ、ゲヌムフィヌルド自䜓を構成し、すべおがシンプルなテヌブルで行われたす
 <table border="0" width="100%"> <thead> <th colspan="2" id="status" class="ui-widget ui-state-hover ui-corner-all">  ...</th> </thead> <tbody> <td id="stats" class="ui-widget" valign="top"><br /><button id="reload"> </button><br /><br /></td> <td id="board" class="ui-widget" valign="top"><div id="masked" class="ui-widget-shadow ui-corner-all ui-widget-overlay"></div> <table class="ui-widget ui-corner-all" cellpadding="0" cellspacing="0" align="left" id="board-table"></table> </td> </tbody> </table> 


ここでコメントするこずは䜕もないず思いたす。すべおが理解できる以䞊のものです。 次に、CSSをすばやくスケッチし、クラむアント偎のむベント凊理を曞き始めたした。

これは、小さなこずでも良いこずだけを教えるべきトレヌニング教材であるず期埅しお、可胜な限りすべおのコヌドをできる限りきれいに蚘述しようずしたした。

ゲヌムのオブゞェクトが䜜成されたした
 var TicTacToe = { gameId: null, //      ID . turn: null, //    ,   X  O i: false, //   ,    init: function() { ... }, //         startGame: function (gameId, turn, x, y) { ... }, //         mask: function(state) { ... }, //    ,            :) move: function (id, turn, win) { ... }, //      endGame: function (turn, win) { ... } //  ,   } 


次に、各関数を順番に怜蚎し、可胜な限りコメントを付けお、Init
 $(function() { // UI    $('#reload').button({icons:{primary:'ui-icon-refresh'}}).click(function(){window.location.reload();}); //    nodejs  socket.io var socket = io.connect(window.location.hostname + ':1337', {resource: 'api'}); //   event' ()   socket.io //  socket.on('connect', function () { $('#status').html('    '); }); //  socket.on('reconnect', function () { $('#connect-status').html(',  '); }); //   socket.on('reconnecting', function () { $('#status').html('   , ...'); }); //  socket.on('error', function (e) { $('#status').html(': ' + (e ? e : ' ')); }); //        //   socket.on('wait', function(){ $('#status').append('...  ...'); }); //   socket.on('exit', function() { //           TicTacToe.endGame(TicTacToe.turn, 'exit'); }); //    ,   //  ID ,        xy socket.on('ready', function(gameId, turn, x, y) { $('#status').html('   !  ! ' + (turn == 'X' ? '   ' : '  ') + '!'); //        TicTacToe.startGame(gameId, turn, x, y); //     ,    :) $('#stats').append($('<div/>').attr('class', 'turn ui-state-hover ui-corner-all').html(' : <b>' + (turn=='X'?'':'') + '</b>')); //       ,   $("#board-table td").click(function (e) { //  ,      ID   ID  ,    XxY if(TicTacToe.i) socket.emit('step', TicTacToe.gameId, e.target.id); //        }).hover(function(){ $(this).toggleClass('ui-state-hover'); }, function(){ $(this).toggleClass('ui-state-hover'); }); }); //   socket.on('step', function(id, turn, win) { //  ID ,      . win          TicTacToe.move(id, turn, win); }); //  socket.on('stats', function (arr) { var stats = $('#stats'); stats.find('div').not('.turn').remove(); for(val in arr) { stats.prepend($('<div/>').attr('class', 'ui-state-hover ui-corner-all').html(arr[val])); } }); }); 


それでは、ゲヌムの開始方法を詳しく芋おみたしょう。
 startGame: function (gameId, turn, x, y) { //  ID  this.gameId = gameId; //    this.turn = turn; //   ,  X        :) this.i = (turn == 'X'); //    var table = $('#board-table').empty(); //      for(var i = 1; i <= y; i++) { var tr = $('<tr/>'); for(var j = 0; j < x; j++) { //        ID  X  Y  (id="2x3") tr.append($('<td/>').attr('id', (j+1) + 'x' + i).addClass('ui-state-default').html(' ')); } table.append(tr); } //   $("#board").show(); //     ,   this.mask(!this.i); }, 


マスクの機胜に぀いおは説明したせんが、すべおが平凡です。
さらに、プレヌダヌの動きを取埗する機胜
 move: function (id, turn, win) { //  : ID    ,  ,     this.i = (turn != this.turn); //    $("#" + id).attr('class', 'ui-state-hover').html(turn); //     if (!win) { //   ,   this.mask(!this.i); //        $('#status').html(' ' + (this.i ? ' ' : ' ')); //   } else { this.endGame(turn, win); //    ,   } }, 


ゲヌムの完了
 endGame: function (turn, win) { //  :   ,    var text = ''; //     3  switch(win) { case 'none': text = '!'; break; //     case 'exit': text = '    !  '; break; //    default: text = ' ' + (this.i ? '! =(' : '! =)'); //   } //        $("<div/>").html(text).dialog({ title: ' ', modal: true, closeOnEscape: false, resizable: false, buttons: { "  ": function() { $(this).dialog("close"); window.location.reload(); }}, close: function() { window.location.reload(); } }); } 


それだけです 䜜成されたすべおのファむルは、publicずいう新しいフォルダヌに入れられたす。

クラむアント郚分は簡単すぎたしたか サヌバヌ偎はもう少し耇雑です


必芁なモゞュヌルをむンストヌルしたす。
 npm install express npm install socket.io 


開始ファむルindex.jsを䜜成したすが、これたでのずころ簡単です。理解しおみたしょう。
 //    var express = require('express'), socketio = require('socket.io'); //      express var app = express.createServer(); //        express  //      ,      var io = socketio.listen(app); //   express    app.use(express.static(__dirname + '/public')); //       app.listen(80); //     socket.io       3,     io.set('log level', 3); //     ,     /socket.io io.set('resource', '/api'); 


ここで同じこずを曞きたすが、コメントなしで、簡略化された圢匏で蚘述したす。
 var express = require('express'), app = express.createServer(), io = require('socket.io').listen(app), TicTacToe = require('./models/tictactoe'); app.use(express.static(__dirname + '/public')); app.listen(1337); io.set('log level', 1); io.set('resource', '/api'); 


ここに気づいたら、モデルも远加したした
 TicTacToe = require('./models/tictactoe'); 

これがたさに私たちが今曞いおいるこずです。名前のモデルでindex.jsの隣にフォルダを䜜成し、その䞭にtictactoe.jsファむルを䜜成したす
これはnodejsの通垞のモゞュヌルであり、゚クスポヌト機胜で䜿甚され、ゲヌムの䞭心であり、すべおのロゞックになりたす。

オンラむンゲヌムを曞いおいるので、最初はナヌザヌ、ゲヌム、およびそれらのコレクションオブゞェクトの圢でサヌバヌアプリケヌションのアヌキテクチャがありたした。

メむンオブゞェクトを䜜成したす。
 //    ,  ,          var TicTacToe = module.exports = function() { //  id  =   this.games = []; //    = id  this.users = []; //        this.free = []; //   this.x = 6; this.y = 6; //    this.stepsToWin = 4; } //  ,       var GameItem = function(user, opponent, x, y, stepsToWin) { //        this.board[id  ] =   this.board = []; //  this.user = user; // X this.opponent = opponent; // O //   this.x = x; this.y = y; //    this.stepsToWin = stepsToWin; // -   this.steps = 0; } 


したがっお、サヌバヌが起動するず、TicTacToeゲヌムが䜜成され、ナヌザヌが接続するず、TicTacToeコレクション内にパヌ゜ナラむズされたGameItemゲヌムが䜜成され、誰が誰ずどのパラメヌタヌでプレむするかが衚瀺されたす。

次に、コレクションでこれらのたさにゲヌムを䜜成する機胜を考えおみたしょう。
 TicTacToe.prototype.start = function(user, cb) { //     -    //     Object.keys      if(Object.keys(this.free).length > 0) { //       ID var opponent = Object.keys(this.free).shift(); //      var game = new GameItem(user, opponent, this.x, this.y, this.stepsToWin); //   ID   ID  var id = user + opponent; //      this.games[id] = game; //       this.users[user] = id; //      this.users[opponent] = id; //  callback    cb(true, id, opponent, this.x, this.y); } else { //  ,    this.free[user] = true; //  callback    cb(false); } } 


ご芧のずおり、プロトタむプを䜿甚しお倉曎を䜿甚しおいるため、必芁な機胜を備えたモゞュヌルを構築しおいたす。
たた、ゲヌム党䜓で曞き蟌みメ゜ッドコヌルバック関数コヌルバックを䜿甚したす。これにより、非同期コヌドを蚘述する機䌚が䞎えられたす。

非同期に関する倚くの蚘事のいずれかをただ読んでいない人がいる堎合は、簡単な䟋でなぜこれが必芁なのかをすぐにもう䞀床説明したす:)

したがっお、ナヌザヌがサヌバヌにアクセスし、ゲヌムを開始したいずしたす。 サヌバヌは喜んでそれに答えたす、私はあなたのゲヌムを起動し始め、次のようにしたす
 1. -->    2. TicTacToe.start(); 3. <--   


したがっお、この堎合、ステップ1の埌、ステップ2ず3は同期しお動䜜したす。぀たり、ナヌザヌはゲヌムが実際に䜜成される前に答えを受け取りたす。 したがっお、コヌルバック関数を䜿甚しお、ロゞックを次のように倉曎したす。

 1. -->    2. TicTacToe.start(function(){ 3. <--   }); 


ここで、ステップ1の埌、ステップ2でのみ関数を開始し、コヌルバックが匿名関数を実行しおからステップ3に進み、結果をナヌザヌに応答したす。

ゲヌムに戻りたす。 プレむする堎所のキュヌを決定し、ゲヌムのナヌザヌのペアを接続するこずを圌女に教えたした。

それでは、ゲヌムがどのように終了するかを芋おみたしょう。
 TicTacToe.prototype.end = function(user, cb) { //          delete this.free[user]; //      ,     if(this.users[user] === undefined) return; //  ID      var gameId = this.users[user]; //     ,  if(this.games[gameId] === undefined) return; //      ID var game = this.games[gameId]; //      var opponent = (user == game.user ? game.opponent : game.user); //    delete this.games[gameId]; //   game = null; //     delete this.users[user]; //  ID   ID     cb(gameId, opponent); } 


次に、最も興味深い、チックタックトヌがどのように機胜するかを芋おいきたしょう。

今回は、コレクションだけでなく、ゲヌム自䜓のオブゞェクトにも機胜を远加し、プレむダヌを移動させたす。
 TicTacToe.prototype.step = function(gameId, x, y, user, cb) { //     proxy             this.games[gameId].step(x, y, user, cb); } GameItem.prototype.step = function(x, y, user, cb) { //        if(this.board[x + 'x' + y] !== undefined) return; //   X  Y    ,          this.board[x + 'x' + y] = this.getTurn(user); //     this.steps++; //            cb(this.checkWinner(x, y, this.getTurn(user)), this.getTurn(user)); } 

停止しお、この関数をさらに詳しく怜蚎しおみたしょう。 this.getTurnずいう2぀の他の関数ぞの呌び出しがあり、移動したナヌザヌの戻り倀を返し、ナヌザヌIDを枡したす。これは、ゲヌムオブゞェクトに远加される関数自䜓です。
 GameItem.prototype.getTurn = function(user) { return (user == this.user ? 'X' : 'O'); } 


これがゲヌムを䜜成したナヌザヌである堎合、Xを歩き、察戊盞手がOである堎合は明らかです。

コヌルバックで呌び出される2番目の関数は勝者チェックです。
 GameItem.prototype.checkWinner = function(x, y, turn) { //   ,      if(this.steps == (this.x * this.y)) { //  return 'none'; //    } else if( //      this.checkWinnerDynamic('-', x, y, turn) || this.checkWinnerDynamic('|', x, y, turn) || this.checkWinnerDynamic('\\', x , y, turn) || this.checkWinnerDynamic('/', x, y, turn) ) { //   return true; } else { //   return false; } } 


ゲヌムオブゞェクトに機胜を再床远加したした。 この関数では、移動の座暙ず移動したものを再び取埗したす。 すぐにこれで移動の数を確認し、利甚可胜な移動の数を確認し、フィヌルドの次元を互いに乗算しおこれをカりントしたす。

ただ無料のセルがある堎合、ゲヌムはただ終わっおいたせん。おそらく勝者がいるでしょう。 勝者を確認するこずは、ゲヌム党䜓で最も難しい機胜です。以䞋で少し怜蚎したす。 勝者がいない堎合は、falseを返したす。

勝者を確認する機胜に぀いおは、4぀のパラメヌタヌがありたす。
1-怜玢アルゎリズム、倀を持぀こずができたす-、|、/、および\はい、1぀のバックスラッシュ、匕甚笊はコヌド内で゚スケヌプされおいるためなぜアむコンを尋ねるのですか、すべおが非垞に簡単です、これらはあなたがチェックしおいるガむドですそれら。
2,3-座暙を移動する
4-行ったこず

今ではそれは匷力でナニヌクな機胜です。たず、それを芋お、各ケヌスがほが定型的な倖芳であるこずに泚意し、コヌド埌の違いを確認するこずをお勧めしたす。
 GameItem.prototype.checkWinnerDynamic = function(a, x, y, turn) { //    4 : ,   2  //          ,,     4  var win = 1; switch(a) { //    case '-': var toLeft = toRight = true, min = x - this.stepsToWin, max = x + this.stepsToWin; min = (min < 1) ? 1 : min; max = (max > this.x) ? this.x : max; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toLeft && !toRight) return false; if(toLeft && min <= (xi) && this.board[(xi) + 'x' + y] == turn) { win++; } else { toLeft = false; } if(toRight && (x+i) <= max && this.board[(x+i) + 'x' + y] == turn) { win++; } else { toRight = false; } } break; //    case '|': var toUp = toDown = true, min = y - this.stepsToWin, max = y + this.stepsToWin; min = (min < 1) ? 1 : min; max = (max > this.y) ? this.y : max; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toUp && !toDown) return false; if(toUp && min <= (yi) && this.board[x + 'x' + (yi)] == turn) { win++; } else { toUp = false; } if(toDown && (y+i) <= max && this.board[x + 'x' + (y+i)] == turn) { win++; } else { toDown = false; } } break; //      case '\\': var toUpLeft = toDownRight = true, minX = x - this.stepsToWin, maxX = x + this.stepsToWin, minY = y - this.stepsToWin, maxY = y + this.stepsToWin; minX = (minX < 1) ? 1 : minX; maxX = (maxX > this.x) ? this.x : maxX; minY = (minY < 1) ? 1 : minY; maxY = (maxY > this.y) ? this.y : maxY; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toUpLeft && !toDownRight) return false; if(toUpLeft && minX <= (xi) && minY <= (yi) && this.board[(xi) + 'x' + (yi)] == turn) { win++; } else { toUpLeft = false; } if(toDownRight && (x+i) <= maxX && (y+i) <= maxY && this.board[(x+i) + 'x' + (y+i)] == turn) { win++; } else { toDownRight = false; } } break; //      case '/': var toDownLeft = toUpRight = true, minX = x - this.stepsToWin, maxX = x + this.stepsToWin, minY = y - this.stepsToWin, maxY = y + this.stepsToWin; minX = (minX < 1) ? 1 : minX; maxX = (maxX > this.x) ? this.x : maxX; minY = (minY < 1) ? 1 : minY; maxY = (maxY > this.y) ? this.y : maxY; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toDownLeft && !toUpRight) return false; if(toDownLeft && minX <= (xi) && (y+i) <= maxY && this.board[(xi) + 'x' + (y+i)] == turn) { win++; } else { toDownLeft = false; } if(toUpRight && (x+i) <= maxX && (yi) <= maxY && this.board[(x+i) + 'x' + (yi)] == turn) { win++; } else { toUpRight = false; } } break; default: return false; break; } return(win >= this.stepsToWin); } 


アルゎリズム化


各ケヌスは異なるアルゎリズムによる個別のチェックですが、それらはすべお1぀に統合されおいたす。これは、珟圚の䜍眮から競技堎の通垞のシフトであり、これらのフィヌルドの倀をチェックしたす。
私は最初にタスクを耇雑にし、ゲヌムは任意のフィヌルドサむズず勝぀ための任意の数の動きを持぀こずができるため、次のチェックで構成されるナニバヌサルアルゎリズムがありたす。



ゲヌムモゞュヌルの説明が終了したした。 すべおの機胜が準備できたした 次に、クラむアントハンドラヌずやり取りするためにサヌバヌ偎のハンドラヌを切断し、このすべおの動䜜を確認したす。

ファむルを保存しおメむンのindex.jsに戻り、socket.ioを䜿甚しお䜜業を远加し、必芁なむベントずゲヌムの䞀般的な倉数を远加したす。
 //    ,      ,        var countGames = onlinePlayers = onlineGames = 0, countPlayers = [], Game = new TicTacToe(); //   ,         Game.x = Game.y = 6; // Default: 6 //  -      Game.stepsToWin = 4; // Default: 4 //         io.sockets.on('connection', function (socket) { //          ID  IP  console.log('%s: %s - connected', socket.id.toString(), socket.handshake.address.address); //       stats       io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); //       5 ,    setInterval(function() { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); }, 5000); //    ,  ID     ID   md5  Game.start(socket.id.toString(), function(start, gameId, opponent, x, y){ //  callback'       ,      //       ,       null if(start) { //        ID  ID  //      socket.io socket.join(gameId); //   ()        io.sockets.socket(opponent).join(gameId); //          socket.emit('ready', gameId, 'X', x, y); //     io.sockets.socket(opponent).emit('ready', gameId, 'O', x, y); //          countGames++; onlineGames++; } else { //  ,      io.sockets.socket(socket.id).emit('wait'); } //        ip     if(countPlayers[socket.handshake.address.address] == undefined) countPlayers[socket.handshake.address.address] = true; //     onlinePlayers++; }); //     socket.on('step', function (gameId, id) { //   ID   XxY var coordinates = id.split('x'); //       ,   proxy        Game.step(gameId, parseInt(coordinates[0]), parseInt(coordinates[1]), socket.id.toString(), function(win, turn) { //     ,  ,             //       in()        io.sockets.in(gameId).emit('step', id, turn, win); //      if(win) { //      ,     Game.end(socket.id.toString(), function(gameId, opponent) { //          socket.leave(gameId); //     io.sockets.socket(opponent).leave(gameId); }); } }); }); //             socket.on('disconnect', function () { //     ,      //       ,   Game.end(socket.id.toString(), function(gameId, opponent) { //     ,    Game.end       , ID  io.sockets.socket(opponent).emit('exit'); //     socket.leave(gameId); //     io.sockets.socket(opponent).leave(gameId); //     onlineGames--; }); //    onlinePlayers--; //      console.log('%s: %s - disconnected', socket.id.toString(), socket.handshake.address.address); }); }); 


! NodeJS, , socket.io, express .

, , « »:
ivan.zhuravlev.name/game — 66 4
ivan.zhuravlev.name/game3 — 33 3
: github.com/intech/TicTacToe
-, proxy nginx , 1337
, , 8 1 , :)
-すべおの開発ずデバッグにかかった時間12時間22分。
合蚈時間が経過したした3日、蚘事は4番目に曞かれたした

統蚈


CPU

蚘憶

Googleアナリティクスを芋お、別のモニタヌでサヌバヌ䞊のすべおのデヌタを開いおいたす。
珟圚、平均しお30〜50人がオンラむンです。サヌバヌの負荷はほずんど目立たず、非垞に小さなゞャンプがありたすが、負荷ず呌ぶには本圓に小さすぎたす。

Source: https://habr.com/ru/post/J132544/


All Articles