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人がオンラインです。サーバーの負荷はほとんど目立たず、非常に小さなジャンプがありますが、負荷と呼ぶには本当に小さすぎます。



All Articles