こんにちは%habraname%!
***この資料にはゲーム自体に論理的な誤りが含まれていますが、これは記事の技術的な内容には影響しません。その目的はプレイすることではなく、タイトルに示されているツールの使用方法を理解することです。 継続する。 コメントに記載されているすべてのエラーを考慮して、ゲームを動作状態にします
今日、NodeJSについて知らないと言う人はほとんどいません。最近では、NodeJSについて多くのことを話し、書いています。
私は6か月前にNodeJSを探索する方法を開始しましたが、私にとっては興味深く、新しいものでした。
すべてのトレーニング資料は非同期に関する記事、またはサーバーまたはチャットの作成方法のいずれかであるため、トレーニング資料で自分にとって興味深いものは見つかりませんでした。 彼は、さまざまなプロジェクトのPHPバックグラウンド作業を部分的に置き換えるさまざまな小さなアプリケーションをゆっくりと作成しました。
しかし、今では、初心者から本物の実用的なアプリケーションまで、退屈ではない本格的な教育資料をすでに作成している自分自身の強さを感じています。 これは単なるアプリケーションではなく、最も人気のあるExpressおよびSocket.IOツールを使用したオンラインゲームです。そうです、平均的な統計js開発者ができるマルチプレイヤーです。
ExpressとSocket.IOがすでに多くのことを書いているので、開発プロセスにもっと注意を払いながら、これについては説明しません。
手始めに、古き良き戦車を選択したかったのですが、選択しなかったのは良いことです。
開発プロセスをグラフィックスで複雑にせず、シンプルなゲームを採用することを決めたので、私の選択は三目並べでしたが、私のタスクを複雑にするために、あらゆる規模の競技場と勝つための任意の数の動きを設定できるように、普遍的にそれを行うことにしました。
そして、それは決定されました! 三目並べを作り始めています。
将来のゲームの構造、結果として必要なものを決定します。
- ゲームに関する情報を備えた何らかの一般的なインターフェイス
- ハンドラーを備えた競技場のUI
- ゲームのすべてのロジックを備えたサーバーパーツ
私はいつものようにインターフェースで開発を始めました。 それぞれ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つに統合されています。これは、現在の位置から競技場の通常のシフトであり、これらのフィールドの値をチェックします。
私は最初にタスクを複雑にし、ゲームは任意のフィールドサイズと勝つための任意の数の動きを持つことができるため、次のチェックで構成されるユニバーサルアルゴリズムがあります。
水平検索
最初に、サイクルの左右の変数境界と、X軸に沿った最小サイズと最大サイズを宣言します。
つまり、現在の移動のこの値+勝つための移動の数の値。
次に、最小値が1未満の場合、最大値と最小値を実数にし、1を設定します。
最大値がX軸のフィールドより大きい場合は、フィールド長の値を設定します。
さらにサイクルでは、極端から始めてすべての隣接するフィールドを通過し、勝利に必要な数にさらに進みます。
ループの各反復で、勝つために同じ動きで必要な数のセルが蓄積されているかどうかをチェックします。そうであれば、関数を終了してtrueを渡します-勝者がいます。
また、左から右、右へのシフトが不可能かどうかをチェックし、サイクルを停止して関数を終了しますが、すでにfalseで-勝者はいません。
次に、左側のセルをチェックできるかどうか、そしてその値が最小値よりも大きいかどうか、またこのセルに現在の移動の値があるかどうかを、左側の先頭から確認します。 すべての条件が一致する場合、変数winの値が増加することで一致することをマークします。 どの条件が一致しなかった場合は、左側を見ないようにマークします。 結局、値が連続するときに条件が勝っています。
右側のチェックにも同じアルゴリズムを使用しますが、右側へのシフトの最大値をチェックするための条件のみが変更されます。垂直検索
最初に、同様に、境界変数と境界を宣言します。
今回だけ、Y軸とその値を使用します。 変位もY軸に沿って通過します。
他のすべては同じです。上から下への対角線
最初に、同様に、境界変数と境界を宣言しますが、境界が異なるようになったので、両方の軸を調べます。(通常は左から右へのはずです。したがって、私はそれについて書いていません:)
オフセットで両方の軸を使用します。 上と左の場合、オフセットのXYを減らし、最小X値と最小Y値を確認します。 そして、最大バイアス値の適切なチェックを行うことにより、XYを増加させる権利と権利について。斜め下から上
ここでは、バイアスと検証を備えた1つのトライフルを除き、ほとんどすべてが同じです。
左下へのシフトは、X軸に沿って減少し、Y軸に沿って増加します。つまり、最小X値と最大Yをチェックします。
そして、右上にシフトすると、これはX軸に沿って増加し、Y軸に沿って減少し、最大Xと最小Yをチェックします。
ゲームモジュールの説明が終了しました。 すべての機能が準備できました! 次に、クライアントハンドラーとやり取りするためにサーバー側のハンドラーを切断し、このすべての動作を確認します。
ファイルを保存してメインの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人がオンラインです。サーバーの負荷はほとんど目立たず、非常に小さなジャンプがありますが、負荷と呼ぶには本当に小さすぎます。