オンラインゲームパート2を作成するか、バグに取り組みます

ご挨拶%habrname%!







昨日の記事で 、私にとっては、ほとんどの人がnodejsに興味がなく、多くの人がデモを見ているだけだと一般に予想されていました。 しかし、残念ながら、この記事でゲームが評価されることを考慮していませんでした! 記事を書くのに多くの時間を費やすのは恥ずかしいことでした(そして最も重要なことですが、ゲームは記事のために書かれ、その逆ではありません)が、今日は続編を書くことにしました。



じゃあ! バグに取り組み、求めたすべての機能を備えた実用的なゲームを作成しますが、ブログのトピックから逸​​脱せず、テスト中に発生したすべての技術的な問題を考慮しません。



このトピックの最初のメッセージは、habravchanin Assarginからの最初のバグレポートです

これは素晴らしいです-ブラウザやインターネットが遅くなり、最初の動きの後に自動的に勝つ間、5〜6回連続で押します! 相手にチャンスはありません! :)

そして、これはFF 3.6.24とクロム15.0.874.106の両方にあります


そして、 モルフィからの彼の後:

どうして?





ここで、私はダブルムーブでその瞬間を見逃しました。それは理解できます。ゲームをテストする目的はすべてを機能させることでした。 一方、検証は歩いているクライアントに対してのみ行われました。 サーバーに転送します。



誰がサーバーに行くかを確認する



ゲームモジュール/ tictactoe.jsの中心でファイルを開き、ゲームのオブジェクトであるmove関数を探します。

GameItem.prototype.step = function(x, y, user, cb) { if(this.board[x + 'x' + y] !== undefined) return; //    "      ",   <hh user="theshock"></hh>     ! this.board[x + 'x' + y] = this.getTurn(user); this.steps++; cb(this.checkWinner(x, y, this.getTurn(user)), this.getTurn(user)); }
      
      





移動を受け取ったときにわかるように、フィールドの占有についてのみチェックしました。次に、その移動にチェックを追加する必要があります。まず、ゲームオブジェクトにturnという新しいプロパティを導入します。

 var GameItem = function(user, opponent, x, y, stepsToWin) { ... //   this.turn = 'X'; //      , ..         . }
      
      





ステップにチェックを追加し、移動を対戦相手に切り替えます。

 GameItem.prototype.step = function(x, y, user, cb) { if(this.board[x + 'x' + y] !== undefined || this.getTurn(user) != this.turn) return; //      turn       this.board[x + 'x' + y] = this.getTurn(user); this.turn = (user != this.user ? 'X' : 'O'); //       this.steps++; cb(this.checkWinner(x, y, this.getTurn(user)), this.getTurn(user)); }
      
      





これが最初のバグ修正です。 しかし、 Morfi画面を見たとき、UIのバグが目に付き、インターフェイスが更新されませんでした。ここで3回「You play:Toe」と表示されます。 ゲームのデバッグは、ゲームの終わりに、ユーザーをsocket.ioの個人用ゲームルームから切断し、それによってユーザーを新しいゲームの共有ルームに連れて行くことを推測しました。 それにより、ユーザーを新しいゲームに接続しますが、「新しいゲーム」ボタンを押す時間はまだありません。 これは簡単に修正できます;ボタンをクリックするだけでゲームにエントリを追加しますが、今は自動的に追加しません。



新しいゲームを自動的に再起動せずにゲームを終了する



パブリックフォルダーにあるクライアントファイルtictactoe.jsを開き、ボタンでゲームの開始に関するイベントへの呼び出しを追加します。その後、クリックイベントを変更してページをリロードします。

 $('#reload').hide().button({icons:{primary:'ui-icon-refresh'}}).click(function() { $('#reload').off('click').click(function(){window.location.reload();}); socket.emit('start'); });
      
      





また、index.jsのサーバー側では、別のイベントでゲームを起動する関数を使用します。

 socket.on('start', function () { if(Game.users[socket.id] !== undefined) return; //    ,         Game.start(socket.id.toString(), function(start, gameId, opponent, x, y){ ... }); });
      
      





ユーザーが新しいゲームを開始するためにクリックするまで、ユーザーの知識がなくても問題なく再起動できます。



進行タイムアウト



多くの人が誰も歩いていないと文句を言い始めました。これは最大の悪でした。おそらくゲームで多くの人が抱えていた問題のために、彼らは単に歩くことができず、ゲームは愚かに開かれました。 正しく指摘されているように、 Evengardはタイムアウトの移動が必要でした。 クライアントファイルに戻り、少し前のpublic / titactoe.jsを編集し、新しい変数を追加して、マスク関数を変更します。

 var TicTacToe = { gameId: null, turn: null, i: false, socket: null, interval: null, //  ... mask: function(state) { var mask = $('#masked'), board = $('#board-table'); clearInterval(this.interval); $('#timer').html(15); this.interval = setInterval(function(){ var i = parseInt($('#timer').html()); i--; $('#timer').html(i); }, 1000); if(state) { mask.show(); var p = board.position(); mask.css({ width: board.width(), height: board.height(), left: p.left, top: p.top }); } else { mask.hide(); } },
      
      





ゲームの開始時とムーブの受信時にタイマーをオンにし、ゲームの終了時とターンの受信時にオフにしてから再びオンにします。 コードについてはコメントしません。githubでソースを確認する方が簡単です。すべてが明確であるため、サーバーの実装に興味があります。



サーバーのタイムアウトとEventEmitterの調査



次に、サーバー側に移りましょう。 ここでは、目的のゲームでのみ機能するようにタイマーを正しく設定する必要があります。そのため、ゲームのオブジェクトをさらに増やします。 しかし、私はこの記事が教材になると約束したので、単純な機能を実行するのではなく、NodeJSのイベントを調査するか、EventEmitterをよりよく知るようにします。

作業するには、nodejsの別のイベントモジュールであるため、接続する必要があります。

models / tictactoe.jsファイルの最初に、EventEmitterとUtil接続を追加します。これには後者が必要です。これについては後で説明します。

 var util = require('util'), EventEmitter = require("events").EventEmitter;
      
      





この呼び出しでは、モジュールを接続するだけでなく、エクスポートされた関数の新しいオブジェクトも作成します。



理論のビット


既にイベントに基づいているsocket.ioを使用しているため、EventEmitterは既にそこに接続されていますが、コンテキストでは宣言されていません。 それを使用するために、nodejsは再接続せず、すべてのrequireがキャッシュされ、繰り返される呼び出しはそれぞれ既に接続されたモジュールへの呼び出しにすぎませんが、新しい方法で接続を登録します。 EventEmitterとは何ですか?これはイベントマシンのオブジェクトであり、簡単に言えば、通常のjavascriptの場合とまったく同じように、イベント呼び出しの一種の監視ですが、それを使用するには少し焦点を合わせる必要があります。



練習に戻る


まず、ゲームオブジェクトにイベントハンドラーを追加する必要があります。そのため、さまざまな関数のセットを含む別のutilモジュールを接続しました。それぞれの詳細な説明は公式ドキュメントで確認できます。 :

 var TicTacToe = module.exports = function() { //   EventEmitter.call(this); ... } util.inherits(TicTacToe, EventEmitter); var GameItem = function(user, opponent, x, y, stepsToWin) { //   EventEmitter.call(this); ... } util.inherits(GameItem, EventEmitter);
      
      





覚えておいて、私は特定の焦点に言及し、それだけです。 したがって、オブジェクトはイベントで機能するようになりました。 ゲームタイマーを制御するのは彼らと一緒です。



IDタイマーを保存するためのプロパティを追加し、GameItemに最初のイベントハンドラーを追加します。

 var GameItem = function(user, opponent, x, y, stepsToWin) { ... //   this.timeout = null; //   this.on('timer', function(state, user) { if(state == 'stop') { //   clearTimeout(this.timeout); this.timeout = null; } else { //   var game = this; this.timeout = setTimeout(function() { //  ,    game.emit('timeout', user); }, 15000); } });
      
      





1つの重要な点に注意してください。GameItemにはまだタイムアウトイベントがありませんが、それを追加し、index.jsファイルに追加します。そこでコードを説明します。

 socket.on('start', function () { if(Game.users[socket.id] !== undefined) return; Game.start(socket.id.toString(), function(start, gameId, opponent, x, y){ if(start) { //      timeout Game.games[gameId].on('timeout', function(user) { Game.end(user, function(gameId, opponent, turn) { io.sockets.in(gameId).emit('timeout', turn); closeRoom(gameId, opponent); //    }); }); ... }); });
      
      





イベントハンドラーを2層上に処理し、Webソケットで作業します。 これはすべて、イベントがトリガーされた後にWebソケットとGameオブジェクト(TicTacToe)の制御を有効にするために必要です。



オペラのちらつきのバグ、または統計に関する愚かな間違い


ユーザーseriyPSの書き込み:

これが何に関係しているのかわかりませんが、あなたの「ゲーム」が毎秒100個の統計イベントを送信します。 このような各イベントでは、UIの大部分が再描画され、すべてが遅くなります。 プレイすることは不可能です(誰とでもできません)。 ホラー全般。


そして、非常に専門的に問題に取り組み、完全に研究しました。 私はすぐにそれが本当に私の間違いであるとコメントで書きました、そして今、私はそれが何であるかを説明します、コードを見ることは気難しいです:

 io.sockets.on('connection', function (socket) { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); setInterval(function() { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); }, 5000); ... });
      
      





Webソケットの接続を確立した後、5秒ごとの間隔で統計の送信を開始します。異なる時間に接続された50人のユーザー5 * 50 = 250の統計要求がサーバーによって5秒ごとに送信されました。 そのようなバギー。

そして、Webソケット接続イベントの境界から統計コードを取り出すだけで解決できます。

 setInterval(function() { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); }, 5000); io.sockets.on('connection', function (socket) { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); ... });
      
      







リファクタリング


これでエラー修正が完了し、リファクタリングを行うことにしました。

index.jsファイルで最初にしたことは、余分なコードを削除し、ユーザーを部屋から切断して、関数に追加することでした。

 io.sockets.on('connection', function (socket) { ... function closeRoom(gameId, opponent) { socket.leave(gameId); io.sockets.socket(opponent).leave(gameId); } ... });
      
      







オンラインプレーヤーとゲームのカウンターは既にオブジェクトにあるため、完全に削除しました。

 var countGames = 0, countPlayers = [], Game = new TicTacToe(); ... io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + Object.keys(Game.games).length, ' : ' + Object.keys(Game.users).length ]);
      
      







さまざまな小さな変更がまだたくさんありましたが、記事はすでに素晴らしいことが判明しました:)

最初の記事からこの行までのこの叙事詩を読んでくれたすべての人に感謝します!



また、socket.ioライブラリの問題に関連するゲームに関する多くの問題、つまり通信を追加したいので、すべてが見た目ほど完璧ではないため、Googleアナリティクスの統計によると、頻繁に「クライアントがハンドシェイクされない」というエラーが確認されます。



PS:15日という早い時期に記事へのアクティブなリンクがあったことをおizeびしますが、その記事はUFOによって隠され、「投稿でのポケットピッキング」で7日間禁止されました。 政権の行動について議論することは不可能であるため、私は単に沈黙します。



プレイする



ゲームを開始するには、「新しいゲーム」をクリックする必要があります。

ivan.zhuravlev.name/game-勝つために4つの動きがある6x6フィールド

ivan.zhuravlev.name/game3-3勝3ムーブのフィールド



ソースを表示: github.com/intech/TicTacToe

すべての変更を1つにコミットしました: github.com/intech/TicTacToe/commit/ff3c1d6e9e298a84ab660b52385c378f69f24f01



All Articles