ネット対戦ゲーム型のスネークゲームをつくります。WebSocketでネット対戦ゲームをつくりたいに機能を追加します。
プレイヤーはひとりではなく複数います。そこでプレイヤーの移動処理はNode.jsで行ない、そのあとクライアント側に各プレイヤーの位置や状態を送信して描画処理をおこなわせます。
app.jsがあるフォルダにsnake-gameフォルダをつくり、そのなかにsnake-game.htmlとsnake-game.jsを作ります。新しく作成するページは/snake-gameに、それが読み込むJavaScriptは/snake-game.jsにしたいので、CreateServerCallback関数に追記します。
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function CreateServerCallback(req, res){ const url_parts = url.parse(req.url); const pathname = url_parts.pathname; if(pathname == '/' || pathname == '/index.html') CreateServerCallback_TopPage(req, res); else if(pathname == '/test') CreateServerCallback_Text(req, res); // 追記ここから else if(pathname == '/snake-game') CreateServerCallback_SnakeGame(req, res); else if(pathname == '/snake-game.js') CreateServerCallback_SnakeGame_js(req, res); // 追記ここまで else CreateServerCallback_404(req, res); } |
1 2 3 4 5 |
// /snake-gameにアクセスしたらページを表示する function CreateServerCallback_SnakeGame(req, res){ res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(fs.readFileSync(__dirname + '/snake-game/snake-game.html', 'utf-8')); } |
それからスネークゲームでもHTTPサーバにソケットをひも付けるのでLinkSocketToHttpServer関数にも追記が必要です。
1 2 3 4 5 6 7 8 |
function LinkSocketToHttpServer(socket_io){ LinkSocketToHttpServer_Text(socket_io); LinkSocketToHttpServer_SnakeGame(socket_io); // 追加 } function LinkSocketToHttpServer_SnakeGame(socket_io){ new SnakeGame(socket_io); } |
Contents
SnakeGame_Playerクラス
SnakeGameクラスを作成しますが、その前提としてプレイヤーの状態を管理するためのSnakeGame_Playerクラスを作成します。
コンストラクタを示します。引数はプレイヤーが出現する座標です。最初の長さは50とし、餌を食べると長くなっていきます。移動した軌跡を利用してスネークの身体を描画するためXY座標をTrajectoryXとTrajectoryYに格納する仕様にしています。
CreateColorString関数はプレイヤーの色を決め、’#ff8800’のようなクライアント側で描画するときに必要な文字列を取得するためのものです。
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class SnakeGame_Player { constructor(initX, initY) { this.ID = 'none'; // ID this.Dead = false; // 生存確認用のフラグ this.Length = 50; // 最初の長さ this.Radius = 20; // 描画するときの円の半径 this.Speed = 2; // 移動速度 this.Angle = 0; // 進行方向の角度 this.PosX = initX; // 初期座標をセット this.PosY = initY; this.TrajectoryX = []; // 移動した軌跡を格納する配列 this.TrajectoryY = []; this.IsLeftKeyDown = false; // キーは押されているか? this.IsUpKeyDown = false; this.IsRightKeyDown = false; this.IsDownKeyDown = false; this.CreateColorString(); // 自キャラの色を決める for(let i=0; i<this.Length; i++){ this.TrajectoryX.unshift(this.PosX - this.Speed * Math.cos(this.Angle) * i); this.TrajectoryY.unshift(this.PosY - this.Speed * Math.sin(this.Angle) * i); } } } |
表示色を決める
CreateColorString関数を示します。まず赤緑青それぞれ127~255までの整数を取得します。r * 256 * 256 + g * 256 + bを計算したものを6桁の16進法で取得し、これに#をつけています。また単色では面白くないのでもうひとつの色は乱数で取得された値を半分にしたものも取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class SnakeGame_Player { CreateColorString(){ let r = this.GetRandomNumber(128) + 127; let g = this.GetRandomNumber(128) + 127; let b = this.GetRandomNumber(128) + 127; this.ColorString1 = this.GetColorString(r, g, b); r = Math.floor(r / 2); g = Math.floor(g / 2); b = Math.floor(b / 2); this.ColorString2 = this.GetColorString(r, g, b); } GetRandomNumber(max) { return Math.floor(Math.random() * max); } GetColorString(r, g, b) { let val = r * 256 * 256 + g * 256 + b; let str = val.toString(16); return '#' + ('000000' + str).slice(-6); } } |
移動範囲の変化に対応させる
プレイヤーが少ないうちは移動できる範囲が広すぎるとゲームになりません。またプレイヤーが増えてくると今度は狭すぎてもゲームになりません。SetRangeOfMovement関数はプレイヤーが移動できる範囲を変更するためのものです。移動できる範囲を変更は全プレイヤーに適用されるべきものなので静的関数をつかっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class SnakeGame_Player { static MinPosX = 0; static MinPosY = 0; static MaxPosX = 700; static MaxPosY = 500; static SetRangeOfMovement(minPosX, minPosY, maxPosX, maxPosY){ SnakeGame_Player.MinPosX = minPosX; SnakeGame_Player.MinPosY = minPosY; SnakeGame_Player.MaxPosX = maxPosX; SnakeGame_Player.MaxPosY = maxPosY; } } |
プレイヤーを移動させる
プレイヤーを移動させる関数を示します。移動できる範囲を超えて移動することはできません。周囲の壁にぶつかった場合は強制的に跳ね返すとか有無を言わせずゲームオーバーにするなどの処理が必要なのですが、いまは「とりあえず作った」段階なので移動不能にするだけです。
描画の処理はクライアント側でおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class SnakeGame_Player { Move() { let canNotMove = this.PosX < SnakeGame_Player.MinPosX || this.PosX > SnakeGame_Player.MaxPosX || this.PosY < SnakeGame_Player.MinPosY || this.PosY > SnakeGame_Player.MaxPosY; if (canNotMove) return; if (this.IsLeftKeyDown) this.Angle += -0.1; if (this.IsRightKeyDown) this.Angle += 0.1; this.PosX += this.Speed * Math.cos(this.Angle); this.PosY += this.Speed * Math.sin(this.Angle); this.TrajectoryX.push(this.PosX); this.TrajectoryY.push(this.PosY); let removeCount = this.TrajectoryX.length - this.Length; if (removeCount > 0) { this.TrajectoryX.splice(0, removeCount); this.TrajectoryY.splice(0, removeCount); } } } |
SnakeGameクラス
次にSnakeGameクラスを示します。
まず静的メンバー変数としてPlayersがあります。プレイヤーが参加するにしたがってここに追加していきます。
connectionイベントを受信時の処理
コンストラクタではconnectionイベントを受信したときのコールバック関数を定義しています。ユーザーがアクセスしたとき、キーが押されたり離されたときの処理、1000/60ミリ秒ごとにおこなう処理を定義しています。
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class SnakeGame { static Players = []; constructor(socket_io){ let snakeGame = socket_io.of('/snake-game').on('connection', function (socket){ socket.on('client_to_server_first', (data) => SnakeGame.ClientToServerFirst(snakeGame, socket, data)); socket.on('client_to_server_keydown', (data) => SnakeGame.ClientToServerKeydown(snakeGame, socket, data)); socket.on('client_to_server_keyup', (data) => SnakeGame.ClientToServerKeyup(snakeGame, socket, data)); socket.on('disconnect', () => SnakeGame.Disconnect(snakeGame, socket)); }); setInterval(() => SnakeGame.Interval(snakeGame), 1000/60); } } |
プレイヤーが増えたときの処理
ユーザーがページにアクセスしたときの処理を示します。socketのIDを調べて新しいSnakeGame_Playerオブジェクトを生成、IDを新しく生成したオブジェクトにセットしています。そのあと静的メンバ変数の配列 Playersに追加しています。
1 2 3 4 5 6 7 8 |
class SnakeGame { static ClientToServerFirst(snakeGame, socket, data){ let id = socket.id; let player = new SnakeGame_Player(150, 150); player.ID = id; SnakeGame.Players.push(player); } } |
キー操作に対する処理
クライアントで方向キーの操作がおこなわれたときの処理を示します。IDが一致するオブジェクトを配列のなかから探して一致するものが見つかった場合はIs~KeyDownフラグのセットとクリアをおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class SnakeGame { static ClientToServerKeydown(snakeGame, socket, data){ let player = SnakeGame.Players.find(x => x.ID == socket.id); if(player != null){ let keyCode = data.value; if(keyCode == 37) player.IsLeftKeyDown = true; if(keyCode == 38) player.IsUpKeyDown = true; if(keyCode == 39) player.IsRightKeyDown = true; if(keyCode == 40) player.IsDownKeyDown = true; } } static ClientToServerKeyup(snakeGame, socket, data){ let player = SnakeGame.Players.find(x => x.ID == socket.id); if(player != null){ let keyCode = data.value; if(keyCode == 37) player.IsLeftKeyDown = false; if(keyCode == 38) player.IsUpKeyDown = false; if(keyCode == 39) player.IsRightKeyDown = false; if(keyCode == 40) player.IsDownKeyDown = false; } } } |
1000/60ミリ秒ごとにおこなわれる処理を示します。SnakeGame_Playerオブジェクトのフラグを調べて適切な移動処理をおこないます。いまのところ他のプレイヤーとの当たり判定の処理はしていません。今後の課題ということで・・・。
現段階ではプレイヤーの状態を送れば描画処理はできるはずですが、今後それ以外のデータ、例えば餌の位置などをおくる必要がでてくることが予想されます。そこでSnakeGame_Dataというクラスをつくります。ここに必要なデータをすべて格納してから送ります。
app.js
1 2 3 4 5 |
class SnakeGame_Data{ constructor(){ this.Players = []; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class SnakeGame { static Interval(snakeGame){ SnakeGame.Players.forEach(player => { player.Move(); // 必要ならここで当たり判定をする }); // クライアントへデータを送って描画処理をさせる let data = new SnakeGame_Data(); data.Players = SnakeGame.Players; snakeGame.emit('server_to_client_objects', {value : data}); } } |
切断時の処理
クライアントとの通信が切断された場合はIDが一致するオブジェクトを配列のなかから取り除きます。
1 2 3 4 5 6 |
class SnakeGame { static Disconnect(snakeGame, socket){ let id = socket.id; SnakeGame.Players = SnakeGame.Players.filter(player => player.ID != id); } } |
クライアント側による描画の処理
描画はクライアント側でおこないます。
snake-game/snake-game.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>snake-game</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <!-- C01. Socket.IOクライアントライブラリの読込み --> <script src="./socket.io/socket.io.js"></script> </head> <body> <div class="container"> <h1>snake-game</h1> <canvas id = "can"></canvas> </div> <script src="./snake-game.js"></script> <script> </script> </body> </html> |
次にsnake-game.jsをどう書くかですが、canvasの幅と高さを700ピクセルと500ピクセルにして黒で塗りつぶします。そしてソケットへ接続します。
snake-game/snake-game.js
1 2 3 4 5 6 7 8 9 10 |
let can =document.getElementById("can"); can.width = 700; can.height = 500; let con = can.getContext("2d"); con.fillStyle = "#000000"; con.fillRect(0, 0, can.width, can.height); // ソケットへの接続 let snake_game = io('/snake-game'); |
ソケットに接続したらキー操作がおこなわれたらclient_to_server_keydownイベントとclient_to_server_keyupイベントを送信して、server_to_client_objectsイベントを受信したら描画処理をおこないます。
snake-game/snake-game.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
snake_game.emit("client_to_server_first", {value : ''}); // キーが押されたらclient_to_server_keydownイベントを送信 document.onkeydown = function(e){ snake_game.emit("client_to_server_keydown", {value : e.keyCode}); } // キーが離されたらclient_to_server_keyupイベントを送信 document.onkeyup = function(e){ snake_game.emit("client_to_server_keyup", {value : e.keyCode}); } snake_game.on("server_to_client_objects", function(data){ con.fillStyle = "#000000"; con.fillRect(0, 0, can.width, can.height); let dataFromServer = data.value; // 各プレイヤーのSnakeを描画する dataFromServer.Players.forEach(snake => { DrawSnake(snake); }); }); // Snakeを描画する function DrawSnake(snake){ let len = snake.TrajectoryX.length; for (let i = 0; i < len - 1; i++) { if (i % 4 == 0) con.fillStyle = snake.ColorString1; else if (i % 4 == 2) con.fillStyle = snake.ColorString2; else continue; con.beginPath(); con.arc(snake.TrajectoryX[i], snake.TrajectoryY[i], snake.Radius, 0, 2 * Math.PI, false); con.fill(); con.stroke(); } con.fillStyle = snake.ColorString1; con.beginPath(); con.arc(snake.TrajectoryX[len - 1], snake.TrajectoryY[len - 1], snake.Radius, 0, 2 * Math.PI, false); con.fill(); con.stroke(); } |