ASP.NET Core版 ボンバーマンのような対戦型ゲームをつくる(3)の続きです。
Contents
cshtmlファイル
Pages\Bomber\game.cshtml
ここではPages\Bomberフォルダ内にgame.cshtmlファイルを作成します。そしてgame.cshtmlには以下のように記述します。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
@page @{ ViewData["Title"] = "対戦ボンバーマン"; Layout = "_Layout_none"; string baseurl = Global.BaseUrl; } <div class = "display-none"> <img src = "@baseurl/bomber/player1.png" id ="player1"> <img src = "@baseurl/bomber/player2.png" id ="player2"> <img src = "@baseurl/bomber/player3.png" id ="player3"> <img src = "@baseurl/bomber/player4.png" id ="player4"> <img src = "@baseurl/bomber/wall1.png" id ="wall1"> <img src = "@baseurl/bomber/wall2.png" id ="wall2"> <img src = "@baseurl/bomber/bomb1.png" id ="bomb1"> <img src = "@baseurl/bomber/bomb2.png" id ="bomb2"> <img src = "@baseurl/bomber/fire1.png" id ="fire1"> <img src = "@baseurl/bomber/fire2.png" id ="fire2"> <img src = "@baseurl/bomber/fire3.png" id ="fire3"> <img src = "@baseurl/bomber/fire4.png" id ="fire4"> <img src = "@baseurl/bomber/fire5.png" id ="fire5"> <img src = "@baseurl/bomber/fire6.png" id ="fire6"> </div> <div style="position: relative; overflow: hidden; margin-left:20px;margin-top:20px;float:left"> <canvas id="can"></canvas> <br> <p>遊び方</p> <p>移動:↑→↓←キー<br> 爆弾のセット:SPACEキー<br> 「音を出す」にチェックをいれてもゲームに参加していない場合は音はでません。</p> <form name="form1"> <input type="checkbox" value="音を出す" id="sound-checkbox">音を出す <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /><br> <input type="button" id="startButton1" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;"> </form> <p><a href="./hi-score">トップ30を見る</a></p> <p id = "conect-result"></p> <p id = "pos-result"></p> </div> <div style="margin-left:0px;margin-top:20px;float:left;width:300px;"> <div id = "gameStatus"></div> <input type="button" id="startButton2" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;"> <br> <div id = "score"></div> <div id = "rest"></div> <br> <img src = "@baseurl/bomber/player1.png"> <span id = "playerName1" style = "color:white; font-weight:bold"></span><br> <img src = "@baseurl/bomber/player2.png"> <span id = "playerName2" style = "color:white; font-weight:bold"></span><br> <img src = "@baseurl/bomber/player3.png"> <span id = "playerName3" style = "color:white; font-weight:bold"></span><br> <img src = "@baseurl/bomber/player4.png"> <span id = "playerName4" style = "color:white; font-weight:bold"></span><br> <div id = "info"></div> </div> <script src="@baseurl/js/signalr.js"></script> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/BomberHub").build(); </script> <script src="@baseurl/bomber/app.js"></script> |
またwwwroot\bomberフォルダ内には以下のファイルを置きます。
player1.png
player2.png
player3.png
player4.png
bomb1.png
bomb2.png
fire1.png
fire2.png
fire3.png
fire4.png
fire5.png
fire6.png
wall1.png
wall2.png
それから音声ファイルとして以下を準備しました。
explotion.mp3
dead.mp3
gameover.mp3
notification.mp3
JavaScript部分
次にJavaScriptですが、wwwroot\bomberフォルダ内にapp.jsを作成します。
グローバル変数
まずグローバル変数を宣言します。
wwwroot\bomber\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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
let can; let ctx; // 方向キーが押されているか? let isUpKeyDown = false; let isDownKeyDown = false; let isLeftKeyDown = false; let isRightKeyDown = false; // 接続時のID let connectionID = ''; // 破壊できない壁 let indestructibleWallXs = []; let indestructibleWallYs = []; // 破壊できる壁 let wallXs = []; let wallYs = []; // CANVASのサイズ const CANVAS_WIDTH = 480; const CANVAS_HEIGHT = 480; // 描画用のイメージ let imgPlayer1 = document.getElementById('player1'); let imgPlayer2 = document.getElementById('player2'); let imgPlayer3 = document.getElementById('player3'); let imgPlayer4 = document.getElementById('player4'); let imgWall1 = document.getElementById('wall1'); let imgWall2 = document.getElementById('wall2'); let imgBomb1 = document.getElementById('bomb1'); let imgBomb2 = document.getElementById('bomb2'); let imgFire1 = document.getElementById('fire1'); let imgFire2 = document.getElementById('fire2'); let imgFire3 = document.getElementById('fire3'); let imgFire4 = document.getElementById('fire4'); let imgFire5 = document.getElementById('fire5'); let imgFire6 = document.getElementById('fire6'); // 効果音 let explodeSound = new Audio('../bomber/explotion.mp3'); let deadSound = new Audio('../bomber/dead.mp3'); let gameoverSound = new Audio('../bomber/gameover.mp3'); let notificationSound = new Audio('../bomber/notification.mp3'); |
以下は更新処理用のグローバル変数です。
wwwroot\bomber\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 29 30 31 32 33 34 |
// ゲームに参加中? let isPlaying = false; // プレイヤーの名前、座標、生きているかどうか? スコア、残機、無敵かどうか?(正ならそのあいだ無敵) let playerName; let playerX; let playerY; let isPlayerDead; let score; let rest; let invincibleTime; // 自分以外のプレイヤー(NPC含む)の名前、座標、生きているかどうか? スコア、残機、無敵かどうか? let playersName = []; let playersX = []; let playersY = []; let isPlayersDead = []; let scores = []; let rests = []; let invincibleTimes = []; // 爆弾の座標 let bombsX = []; let bombsY = []; // 火花の座標と消滅までの時間 let firesX = []; let firesY = []; let firesLife = []; // プレイヤー死亡時に周囲に飛ぶ火花の座標と消滅までの時間 let deadFiresX = []; let deadFiresY = []; let deadFiresLife = []; |
ページが読み込まれたときの処理
ページが読み込まれたらCANVASのサイズを設定してボリュームをうるさくない音量(経験的に0.03くらいがよさそう)に設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
window.addEventListener('load', Init); function Init() { can = document.getElementById('can'); can.width = CANVAS_WIDTH; can.height = CANVAS_HEIGHT; ctx = can.getContext('2d'); SetVolume(0.03); } function SetVolume(volume) { explodeSound.volume = volume; deadSound.volume = volume; gameoverSound.volume = volume; notificationSound.volume = volume; } |
それからページにアクセスしたらAspNetCore.SignalRで接続を開始します。
1 2 3 |
connection.start().catch(function (err) { document.getElementById("conect-result").innerHTML = '接続失敗'; }); |
接続に成功したらそれがわかるようにメッセージを表示させます。
1 2 3 4 5 6 |
connection.on("ReceiveConnected", function (result, id) { // 接続したときのIDと「接続成功」の文字列が送られてくるので・・・ connectionID = id; document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; console.log(connectionID); }); |
ゲームに参加したときの処理
「ゲームスタート」のボタンをクリックしたらサーバーサイドにプレイヤー名とIDを送信します。これによってBomberHubクラスのGameStartメソッドが呼び出されゲームに参加することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function GameStart() { if (connectionID != '') { // 接続に成功しているならサーバーサイドにプレイヤー名とIDを送信 // プレイヤー名未設定のときは「名無しさん」 let playerName = document.getElementById('player-name').value; if (playerName == '') playerName = '名無しさん'; connection.invoke("GameStart", connectionID, playerName).catch(function (err) { return console.error(err.toString()); }); } } |
キーが押されたときの処理
キーが押されたらサーバーサイドにこれを送信します。サーバーサイドでは対応するPlayerオブジェクトが生成されているのであれば処理がおこなわれます。観戦しているだけで参加していない場合はなにも起きません。
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 47 48 49 50 51 52 53 |
document.onkeydown = function (e) { // ゲーム開始以降はデフォルトの動作を抑制する if (connectionID != '' && (e.key == "ArrowUp" || e.key == "ArrowDown" || e.key == "ArrowLeft" || e.key == "ArrowRight" || e.key == " ")) e.preventDefault(); // キーが押しっぱなしになっているときの二重送信を防ぐ if (e.key == "ArrowUp" && isUpKeyDown) return; if (e.key == "ArrowDown" && isDownKeyDown) return; if (e.key == "ArrowLeft" && isLeftKeyDown) return; if (e.key == "ArrowRight" && isRightKeyDown) return; if (e.key == "ArrowUp") isUpKeyDown = true; if (e.key == "ArrowDown") isDownKeyDown = true; if (e.key == "ArrowLeft") isLeftKeyDown = true; if (e.key == "ArrowRight") isRightKeyDown = true; // ゲーム中にプレイヤー名を変更可能なのでキーが押されたらそのつどプレイヤー名も送信する // 接続されていない場合はなにもしない if (connectionID != '') { let playerName = document.getElementById('player-name').value; if (playerName == '') playerName = '名無しさん'; connection.invoke("DownKey", e.key, playerName).catch(function (err) { return console.error(err.toString()); }); } } document.onkeyup = function (e) { if (e.key == "ArrowUp") isUpKeyDown = false; if (e.key == "ArrowDown") isDownKeyDown = false; if (e.key == "ArrowLeft") isLeftKeyDown = false; if (e.key == "ArrowRight") isRightKeyDown = false; // 接続されていない場合はなにもしない if (connectionID != '') { connection.invoke("UpKey", e.key).catch(function (err) { return console.error(err.toString()); }); } } |
更新処理
更新処理に関する関数を示します。
更新処理の開始を知らせるためにサーバーサイドからReceiveStartUpdateが送られてきます。このときに描画処理に必要なデータを一時的に保存しておく配列を初期化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
connection.on("ReceiveStartUpdate", function () { isPlaying = false; playersName = []; playersX = []; playersY = []; isPlayersDead = []; scores = []; rests = []; invincibleTimes = []; bombsX = []; bombsY = []; firesX = []; firesY = []; firesLife = []; deadFiresX = []; deadFiresY = []; deadFiresLife = []; }); |
更新すべきデータを受信したときの処理を示します。ライバルや爆弾、火花、破壊された壁の位置や状態など配列に格納すべきデータが文字列で送られてくるので配列に変換して保存しておきます。自機に関するデータはそのままNumber型で送られてくるのでそのまま格納します。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
connection.on("ReceiveIndestructibleWalls", function (xs, ys) { indestructibleWallXs = xs.split(','); indestructibleWallYs = ys.split(','); }); connection.on("ReceiveWalls", function (xs, ys) { wallXs = xs.split(','); wallYs = ys.split(','); }); connection.on("ReceiveUpdateBrokenWalls", function (xs, ys) { if (xs != '') { brokenX = xs.split(','); brokenY = ys.split(','); for (let i = 0; i < xs.length; i++) { for (let k = 0; k < wallXs.length; k++) { if (wallXs[k] == brokenX[i] && wallYs[k] == brokenY[i]) { wallXs.splice(k, 1); wallYs.splice(k, 1); break; } } } } }); connection.on("ReceiveUpdatePlayer", function (key, x, y, name, isdead, scre, rst, invincible) { if (key == connectionID) { isPlaying = true; playerName = name; playerX = x; playerY = y; isPlayerDead = isdead; score = scre; rest = rst; invincibleTime = invincible; } else { playersName.push(name); playersX.push(x); playersY.push(y); isPlayersDead.push(isdead); scores.push(scre); rests.push(rst); invincibleTimes.push(invincible); } }); connection.on("ReceiveUpdateBombs", function (xs, ys) { if (xs != '') { bombsX = xs.split(','); bombsY = ys.split(','); } }); connection.on("ReceiveUpdateFires", function (xs, ys, lifes) { if (xs != '') { firesX = xs.split(','); firesY = ys.split(','); firesLife = lifes.split(','); } }); connection.on("ReceiveUpdateDeadFires", function (xs, ys, lifes) { if (xs != '') { deadFiresX = xs.split(','); deadFiresY = ys.split(','); deadFiresLife = lifes.split(','); } }); connection.on("ReceiveUpdateGameStatus", function (gameStatus, canGameStart) { let gameStatusElement = document.getElementById('gameStatus'); gameStatusElement.style.fontWeight = 'bold'; gameStatusElement.style.color = 'yellow'; gameStatusElement.innerHTML = gameStatus; if (canGameStart) { document.getElementById('startButton1').style.display = 'block'; document.getElementById('startButton2').style.display = 'block'; } else { document.getElementById('startButton1').style.display = 'none'; document.getElementById('startButton2').style.display = 'none'; } }); |
更新すべきデータがすべて送られたら最後にReceiveEndUpdateが送られてきます。その場合は描画処理をおこないます。自機は黄色いひよこにしてそれ以外のプレイヤーは3種類のべつのイメージを使います。
プレイヤーが生存している場合は描画し、死亡している場合は表示しません。無敵状態のときは点滅させるのでこの処理が実行された回数をカウントして偶数回、奇数回で表示非表示を切り替えています。
また爆発の火花は消滅するまでの時間でイメージを切り替えて爆発っぽく描画しています。
Canvasの隣に自分と他のプレイヤーの名前と残機、スコアを表示させます。NPCの場合は残機とスコアの概念はないのでそれらは表示させません。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
let updateCount = 0; connection.on("ReceiveEndUpdate", function () { updateCount++; // CANVAS内部を黒で塗りつぶす ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); DrawWalls(); DrawBombs(); DrawFires(); DrawPlayers(); ShowPlayerInfos(); // プレイヤー一覧に自分自身のイメージとプレイヤー名、スコア、残機を描画 if (isPlaying) { // 死亡時は表示せず無敵状態であれば点滅させる let show = true; if (invincibleTime > 0 && invincibleTime % 2 == 0) show = false; if (!isPlayerDead && show) ctx.drawImage(imgPlayer1, playerX - 5, playerY - 5, 32 + 10, 32 + 10); // プレイヤー一覧にスコア、残機を表示 document.getElementById('playerName1').innerText = playerName + ' (' + score + ' - ' + rest + ')'; // プレイヤー一覧とは別の部分にもスコア、残機を表示 let scoreElement = document.getElementById('score'); scoreElement.style.fontWeight = 'bold'; scoreElement.style.color = 'white'; scoreElement.style.fontSize = '125%'; scoreElement.innerText = 'Score ' + score; let restElement = document.getElementById('rest'); restElement.style.fontWeight = 'bold'; restElement.style.color = 'white'; restElement.style.fontSize = '125%'; restElement.innerText = '残 ' + rest; } }); // 壁を描画 function DrawWalls() { // 破壊できない壁 for (let i = 0; i < indestructibleWallXs.length; i++) ctx.drawImage(imgWall2, indestructibleWallXs[i], indestructibleWallYs[i], 32, 32); // 破壊できる壁 for (let i = 0; i < wallXs.length; i++) ctx.drawImage(imgWall1, wallXs[i], wallYs[i], 32, 32); } // 爆弾を描画 function DrawBombs() { // 2種類のイメージを切り替える for (let i = 0; i < bombsX.length; i++) { if (updateCount % 16 < 8) ctx.drawImage(imgBomb1, bombsX[i] - 2, bombsY[i] - 2, 32 + 4, 32 + 4); else ctx.drawImage(imgBomb2, bombsX[i] - 2, bombsY[i] - 2, 32 + 4, 32 + 4); } } // 火花を描画 function DrawFires() { // 消滅までの時間でイメージを切り替える for (let i = 0; i < firesX.length; i++) { let imgFire = null; if (firesLife[i] >= 11) imgFire = imgFire1; else if (firesLife[i] >= 9) imgFire = imgFire2; else if (firesLife[i] >= 7) imgFire = imgFire3; else if (firesLife[i] >= 5) imgFire = imgFire4; else if (firesLife[i] >= 3) imgFire = imgFire5; else if (firesLife[i] >= 1) imgFire = imgFire6; if (imgFire != null) ctx.drawImage(imgFire, firesX[i], firesY[i], 32, 32); } for (let i = 0; i < deadFiresX.length; i++) { let imgFire = null; if (deadFiresLife[i] >= 11) imgFire = imgFire1; else if (deadFiresLife[i] >= 9) imgFire = imgFire2; else if (deadFiresLife[i] >= 7) imgFire = imgFire3; else if (deadFiresLife[i] >= 5) imgFire = imgFire4; else if (deadFiresLife[i] >= 3) imgFire = imgFire5; else if (deadFiresLife[i] >= 1) imgFire = imgFire6; if (imgFire != null) ctx.drawImage(imgFire, deadFiresX[i], deadFiresY[i], 48, 48); } } // プレイヤーのイメージを描画 function DrawPlayers() { for (let i = 0; i < 4; i++) { if (playersX[i] != undefined) { let imgPlayer = null; if (i == 0) imgPlayer = imgPlayer2; else if (i == 1) imgPlayer = imgPlayer3; else if (i == 2) imgPlayer = imgPlayer4; else if (i == 3) imgPlayer = imgPlayer1; let show = true; if (isPlayersDead[i] || (invincibleTimes[i] > 0 && invincibleTimes[i] % 2 == 0)) show = false; if (show) ctx.drawImage(imgPlayer, playersX[i] - 5, playersY[i] - 5, 32 + 10, 32 + 10); } } } // プレイヤー一覧にプレイヤー名、スコア、残機を描画 function ShowPlayerInfos() { for (let i = 0; i < 4; i++) { if (playersX[i] != undefined) { let playerNameElement = null; let imgPlayer = null; if (i == 0) playerNameElement = document.getElementById('playerName2'); else if (i == 1) playerNameElement = document.getElementById('playerName3'); else if (i == 2) playerNameElement = document.getElementById('playerName4'); else if (i == 3) playerNameElement = document.getElementById('playerName1'); let score = ''; if (scores[i] != "") score = ' (' + scores[i] + ' - ' + rests[i] + ')'; if (playerNameElement != null) playerNameElement.innerText = playersName[i] + score; } } } |
それ以外の通知の表示
プレイヤーが参戦したりゲームオーバーになったり試合放棄した場合もサーバーサイドからデータが送られてきます。ReceiveNotificationを受信したらそれを表示します。通知は配列のなかに格納し、3秒後に消します。また音を鳴らす設定の場合は通知音を鳴らします。
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 |
let messages = []; connection.on("ReceiveNotification", function (str) { // 通知は配列のなかに格納 messages.push(str); ShowNotification(); if (isPlaying && document.getElementById('sound-checkbox').checked) { notificationSound.currentTime = 0; notificationSound.play(); } // 3秒後に配列のなかの通知を削除 setTimeout(() => { if (messages.length > 0) { messages.shift(); ShowNotification(); } }, 3000); }); function ShowNotification() { let infoElement = document.getElementById('info'); infoElement.style.marginTop = '30px'; let message = ""; for (let i = 0; i < messages.length; i++) { message += messages[i] + '<br>'; } // サーバーサイドから送られてくる文字列はエスケープしているのでこのままで問題ない infoElement.innerHTML = message; } |
爆発音、ゲームオーバー時の効果音の再生
クライアントサイドでReceiveBombExploded、ReceivePlayerDead、ReceiveGameOverを受信したときは爆弾が爆発したりプレイヤーが死亡したりゲームオーバーになったときです。この場合は効果音を鳴らします。その処理を示します。
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 |
connection.on("ReceiveBombExploded", function () { if (isPlaying && document.getElementById('sound-checkbox').checked) { explodeSound.currentTime = 0; explodeSound.play(); } }); connection.on("ReceivePlayerDead", function () { if (isPlaying && document.getElementById('sound-checkbox').checked) { deadSound.currentTime = 0; deadSound.play(); } }); connection.on("ReceiveGameOver", function (id) { if (connectionID == id) { isPlaying = false; document.getElementById('rest').style.fontWeight = 'bold'; document.getElementById('rest').style.color = 'red'; document.getElementById('rest').innerHTML = 'Game Over'; if (document.getElementById('sound-checkbox').checked) { gameoverSound.currentTime = 0; gameoverSound.play(); } } }); |