ASP.NET Core版 対戦型のマインスイーパーをつくる(2)の続きです。
実はこの記事を書く前にプログラミング実況配信系YouTuberのT.Umezawaさんとその視聴者さんにレビューしていただきました。そこでいくつか改善すべき点を指摘されました。
ここから辛口レビューがはじまります。
(1)旗は右クリックで立てるようにしているが、マウスダウンが押さえた段階で立てて欲しい。
(2)自分で開いたセルとそれ以外のプレイヤーが開いたセルは色を変えるなど視覚的にわかるようにしてほしい。
(3)最後のマスを開いたプレイヤーにはボーナスポイントを与えてはどうか。
(4)マウスをポチポチしなくてもドラッグしたら複数のセルが開くようにしてほしい。
(5)ドラッグの速度が速いと連続したセルがうまく開かない。
(6)旗を立てているセルはクリックしても開かないようにしてほしい。
(7)すでに開いているセルには旗を立てられないようにしてほしい。
(8)終盤になってミスをすると高得点は見込めないので、難しいところは配点を上げるとか、参加人数や残りのセル数で配点を変えてほしい。
(9)クリア時に成績優秀者を表示して欲しい。
などの要望が出されました。
(6)旗を立てているセルはクリックしても開かないようにする。これは旗が立っているにもかかわらず左クリックするあなたが悪い!と言いたいのですが、要望が多かったので対応しました。ただし旗を立ててみたものの間違いに気づいた場合は左クリックするまえに旗をキャンセルする必要があります。
(7)すでに開いているセルには旗を立てられないようにするのも対応しました。
(1)(2)(4)(5)はクライアントサイドの処理で実装します。それから「地雷が置かれていない部分をクリックしたらゲームオーバーになる」のはおかしいのではないかという意見も出されましたが、これはバグではなくそのように見えるだけです。ただ紛らわしい動作なのでこの点も改善します。
(3)と(8)と(9)はしばらくお待ちください。配点はどうやって決めたらいいのかいい案があったら教えてください。
Contents
cshtmlファイル
ここではPages\Minesweeperフォルダ内にgame.cshtmlファイルを作成します。そしてgame.cshtmlには以下のように記述します。
Pages\Minesweeper\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 72 73 74 75 76 77 78 79 80 81 |
@page @{ ViewData["Title"] = "対戦型マインスイーパ"; Layout = "_Layout_none"; string baseurl = "https://lets-csharp.com/samples/2204/aspnetcore-app-zero"; } <div class = "display-none"> <img src = "@baseurl/mine-sweeper/a0.png" id ="a0"> <img src = "@baseurl/mine-sweeper/a1.png" id ="a1"> <img src = "@baseurl/mine-sweeper/a2.png" id ="a2"> <img src = "@baseurl/mine-sweeper/a3.png" id ="a3"> <img src = "@baseurl/mine-sweeper/a4.png" id ="a4"> <img src = "@baseurl/mine-sweeper/a5.png" id ="a5"> <img src = "@baseurl/mine-sweeper/a6.png" id ="a6"> <img src = "@baseurl/mine-sweeper/a7.png" id ="a7"> <img src = "@baseurl/mine-sweeper/a8.png" id ="a8"> <img src = "@baseurl/mine-sweeper/b0.png" id ="b0"> <img src = "@baseurl/mine-sweeper/b1.png" id ="b1"> <img src = "@baseurl/mine-sweeper/b2.png" id ="b2"> <img src = "@baseurl/mine-sweeper/b3.png" id ="b3"> <img src = "@baseurl/mine-sweeper/b4.png" id ="b4"> <img src = "@baseurl/mine-sweeper/b5.png" id ="b5"> <img src = "@baseurl/mine-sweeper/b6.png" id ="b6"> <img src = "@baseurl/mine-sweeper/b7.png" id ="b7"> <img src = "@baseurl/mine-sweeper/b8.png" id ="b8"> <img src = "@baseurl/mine-sweeper/unknown.png" id ="unknown"> <img src = "@baseurl/mine-sweeper/flag.png" id ="flag"> <img src = "@baseurl/mine-sweeper/fire1.png" id ="fire1"> <img src = "@baseurl/mine-sweeper/fire2.png" id ="fire2"> <img src = "@baseurl/mine-sweeper/fire3.png" id ="fire3"> <img src = "@baseurl/mine-sweeper/fire4.png" id ="fire4"> <img src = "@baseurl/mine-sweeper/fire5.png" id ="fire5"> <img src = "@baseurl/mine-sweeper/fire6.png" id ="fire6"> </div> <div style = "display:flex;"> <div style="margin-left:20px; margin-top:20px;"> <canvas id="can"></canvas> <p id = "conect-result"></p> </div> <div style="margin-left:0px; margin-top:20px;"> <p style = "color:#ff0000; font-weight:bold">遊び方</p> <p>左クリック マスを開ける。ドラッグすれば連続で開けます。<br> 右クリック 地雷があると思うところに旗を立てる</p> <p style = "color:#ff0000; font-weight:bold">注意点</p> <p>他のプレイヤーが立てた旗は見えません。<br> すでに開いている場所に旗は立てられません。<br> 自分で旗を立てた場所を開くことはできません。いったん旗をクリアしてから開いてください。</p> <div style="margin-bottom:20px;"> <input type="checkbox" value="音を出す" id="sound-checkbox">音を出す <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /><br> </div> <input type="button" value="ゲームスタート" id = "start-button" onclick="GameStart()" > <div id = "score" style="margin-top:20px;"></div> <div id = "count" style="margin-top:20px;"></div> <p><a href="./hi-score">トップ30を見る</a></p> <div id = "scores" style="margin-top:20px;"> </div> </div> </div> <script src="@baseurl/js/signalr.js"></script> <script> const connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/MinesweeperHub").build(); const base_url = '@baseurl'; </script> <script src="@baseurl/mine-sweeper/app.js"></script> |
そのセルの周囲に埋められている地雷の個数を灰色の背景に色文字でPNGファイルに書いています。自分で開けたセルの場合は背景色と文字色を反転させています(ただし色は白)。
JavaScript部分
それではJavaScriptの部分を示します。
wwwroot\mine-sweeper\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 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 |
// セルの表示サイズ const CELL_SIZE = 24; // canvasのサイズ const CANVAS_WIDTH = 800; const CANVAS_HEIGHT = 800; const con = document.getElementById('can'); const ctx = con.getContext('2d'); // マウスの左ボタンは押されているか? let isMouseDwon = false; // ドラッグしているとき"mousemove"イベントで最後に取得された座標 let dragLastX = -1; let dragLastY = -1; // 爆発の描画時に必要なデータのバックアップ用 let oldCellArray = []; let oldRowMax = 0; let oldColMax = 0; // 踏んでしまった地雷の位置 let deadCol = 0; let deadRow = 0; // ミス時の描画で使用するグローバル変数 let oldCellArray = []; let oldRowMax = 0; let oldColMax = 0; // 効果音再生用 let deadSound = new Audio(base_url + '/mine-sweeper/dead.mp3'); let openSound = new Audio(base_url + '/mine-sweeper/open.mp3'); let clearedSound = new Audio(base_url + '/mine-sweeper/cleared.mp3'); // セルを開いたときに数字を表示するためのイメージと配列 let imgNumbers1 = []; imgNumbers1.push(document.getElementById('a0')); imgNumbers1.push(document.getElementById('a1')); imgNumbers1.push(document.getElementById('a2')); imgNumbers1.push(document.getElementById('a3')); imgNumbers1.push(document.getElementById('a4')); imgNumbers1.push(document.getElementById('a5')); imgNumbers1.push(document.getElementById('a6')); imgNumbers1.push(document.getElementById('a7')); imgNumbers1.push(document.getElementById('a8')); // 自分自身がセルを開いたときに数字を表示するためのイメージと配列 let imgNumbers2 = []; imgNumbers2.push(document.getElementById('b0')); imgNumbers2.push(document.getElementById('b1')); imgNumbers2.push(document.getElementById('b2')); imgNumbers2.push(document.getElementById('b3')); imgNumbers2.push(document.getElementById('b4')); imgNumbers2.push(document.getElementById('b5')); imgNumbers2.push(document.getElementById('b6')); imgNumbers2.push(document.getElementById('b7')); imgNumbers2.push(document.getElementById('b8')); // まだ開かれていないセルのイメージ let imgUnknown = document.getElementById('unknown'); // 旗と地雷のイメージ let imgFlag = document.getElementById('flag'); let imgMine = document.getElementById('mine'); // ミス時に飛ぶ火花のイメージと配列 let imgFires = []; imgFires.push(document.getElementById('fire1')); imgFires.push(document.getElementById('fire2')); imgFires.push(document.getElementById('fire3')); imgFires.push(document.getElementById('fire4')); imgFires.push(document.getElementById('fire5')); imgFires.push(document.getElementById('fire6')); // 自分で開けたセルを格納する二次元配列 let cellsOpenedYourself = []; // 火花のオブジェクトを格納する配列 let fires = []; // 火花のクラス class Fire{ constructor(x, y, vx, vy){ this.InitX = x; this.InitY = y; this.InitVX = vx; this.InitVY = vy; this.X = x; this.Y = y; this.updateCount = 0; this.TimeToDisappearance = 12; } Update(){ this.updateCount++; this.TimeToDisappearance--; this.X = this.InitX + this.InitVX * this.updateCount; this.Y = this.InitY + this.InitVY * this.updateCount; } } |
初期化
初期化の処理を示します。ここではcanvasのサイズと効果音のボリュームを設定しています。そしてAspNetCore.SignalRで接続しようとしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
window.addEventListener('load', function(){ con.width = CANVAS_WIDTH; con.height = CANVAS_HEIGHT; SetVolume(0.1); document.getElementById('score').style.fontSize = '32px'; document.getElementById('score').style.fontWeight = 'bold'; document.getElementById('sound-checkbox').checked = true; connection.start().catch(function (err) { document.getElementById("conect-result").innerHTML = '接続失敗'; }); }); function SetVolume(volume) { deadSound.volume = volume; openSound.volume = volume; clearedSound.volume = volume; } |
接続に成功時の処理
接続に成功したときの処理を示します。サーバーサイドから送られてきたIDをグローバル変数connectionIDに格納します。
1 2 3 4 5 6 7 |
let connectionID = ''; connection.on("ReceiveConnected", function (result, id) { connectionID = id; document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; console.log(connectionID); }); |
ゲームを開始時の処理
[ゲームスタート]のボタンを押したとき、AspNetCore.SignalRで接続されているのであれば、グローバル変数connectionIDは空文字列ではありません。その場合はサーバーサイドにGameStartを送信します。また表示されているスコアを0に戻します。
1 2 3 4 5 6 7 8 9 10 |
function GameStart(){ if(connectionID != ''){ let playerName = document.getElementById('player-name').value; document.getElementById('score').innerHTML = 'SCORE 0'; connection.invoke("GameStart", connectionID, playerName).catch(function (err) { return console.error(err.toString()); }); } } |
ゲーム中はゲームスタートできない
サーバーサイドでゲームに参加する処理が正しく行なわれたとき、クライアントサイドではReceiveGameStartedを受信することになります。二重にゲームに参加できないように[ゲームスタート]のボタンを非表示にします(二重にゲーム開始の処理が行なわれないようにサーバーサイドでもチェックをしている)。もっともブラウザを複数起動すれば別のユーザーとみなされ、参加はできますが・・・。
1 2 3 |
connection.on("ReceiveGameStarted", function () { document.getElementById('start-button').style.display = 'none'; }); |
セルの描画
セルを描画する処理を示します。自分で開いたセルと他のプレイヤーが開いたセルの見た目を変えたいので、自分で開けたセルを格納する二次元配列を定義します。そしてInitCellsOpenedYourself関数で初期化をします。
1 2 3 4 5 6 7 8 9 10 |
function InitCellsOpenedYourself(rowMax, colMax){ cellsOpenedYourself = []; for(let i=0; i<rowMax; i++) { let temp =[]; for(let i=0; i<colMax; i++) temp.push(false); cellsOpenedYourself.push(temp); } } |
セルを描画する処理を示します。ReceiveDrawを受信したらセルを描画します。
ここでは自分で開けたセルを格納する二次元配列が存在するか確認し、存在しない場合は生成しています。そのあとサーバーサイドから送られてきた文字列で配列をつくり、DrawCell関数に渡しています。
またミス時の描画処理をする関係上、セルの行数と列数、サーバーサイドから送られてきた文字列を保存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
connection.on("ReceiveDraw", function (rowMax, colMax, str) { if(cellsOpenedYourself.length == 0) InitCellsOpenedYourself(rowMax, colMax); let arr = str.split(',') let index = 0; for(let row=0; row<rowMax; row++) { for(let col=0; col<colMax; col++) { DrawCell(col, row, arr[index]); index++; } } oldCellArray = arr; oldRowMax = rowMax; oldColMax = colMax; }); |
DrawCell関数を示します。cellsOpenedYourself[row][col] == trueであれば自分で開いたセルなので描画するイメージを変えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function DrawCell(col, row, index){ if(index >= 0){ if(cellsOpenedYourself[row][col]) ctx.drawImage(imgNumbers2[index], col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); else ctx.drawImage(imgNumbers1[index], col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); } else if(index == -1) // 地雷 ctx.drawImage(imgMine, col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); else if(index == -2) // 開かれていないセル ctx.drawImage(imgUnknown, col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); else if(index == -3) // 自身で旗を立てたセル ctx.drawImage(imgFlag, col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); ctx.strokeRect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE); } |
セルを開く処理
まずマウスボタンが押されたり離されたとき、マウスが移動したときのイベントリスナーを登録しています。
左ボタンが押された場合はOpenCell関数を呼び出してセルを開く処理をするとともに、ドラッグの始まりかもしれないのでisMouseDwonフラグをtrueにしています。右ボタンが押されたときは旗を立てるための関数を呼び出します。
マウスポインタがcanvasの外に出た場合もマウスドラッグが終了したと見なしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
can.addEventListener("mousedown", MouseDown, false); can.addEventListener("mousemove", MouseMove, false); can.addEventListener("mouseleave", MouseUp, false); can.addEventListener("mouseup", MouseUp, false); function MouseDown(e) { // e.button == 0 なら左 e.button == 2 なら右 if(e.button == 0) { OpenCell(e); isMouseDwon = true; } if(e.button == 2) { SetFlag(e); } } |
左ボタンがおされたときにセルを開く処理をおこなうOpenCell関数を示します。
ブラウザ上でのクリック座標をキャンバス上に変換してサーバーサイドに送ります。マウスがドラッグされているときはOpenCell関数が連続して呼び出されるわけですが、そのとき前回取得したキャンバス上のマウス座標と今回取得された座標を比較します(canvasXとdragLastX、canvasYとdragLastY)。
両者のXY成分がそれぞれCELL_SIZE以上離されている場合は間が抜けていることになるので、この場合はその間の距離を分割して連続したセルがすべて開くように調整します。
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 |
function OpenCell(e){ const rect = e.target.getBoundingClientRect(); // ブラウザ上での座標を求める const viewX = e.clientX - rect.left, viewY = e.clientY - rect.top; // 表示サイズとキャンバスの実サイズの比率を求める const scaleWidth = can.clientWidth / can.width, scaleHeight = can.clientHeight / can.height; // ブラウザ上でのクリック座標をキャンバス上に変換 const canvasX = Math.floor( viewX / scaleWidth ), canvasY = Math.floor( viewY / scaleHeight ); let playerName = document.getElementById('player-name').value; // dragLastX != -1 && dragLastY != -1 ならドラッグが継続している if(dragLastX != -1 && dragLastY != -1) { let xCount = Math.ceil((canvasX - dragLastX) / CELL_SIZE); let yCount = Math.ceil((canvasY - dragLastY) / CELL_SIZE); if(xCount < 0) xCount*= -1; if(yCount < 0) yCount*= -1; let count = xCount > yCount ? xCount : yCount; count += 2; // 同じ場所にあるセルをなんども開こうとしないように前のセルの位置を記憶しておく let oldColumn = 0; let oldRow = 0; for(let i = 0; i < count; i++) { let x = dragLastX + (i)*(canvasX - dragLastX)/count; let y = dragLastY + (i)*(canvasY - dragLastY)/count; if(oldColumn != Math.floor(x/CELL_SIZE) || oldRow != Math.floor(y/CELL_SIZE)) { connection.invoke("OpenCell", playerName, Math.floor(x/CELL_SIZE) ,Math.floor(y/CELL_SIZE)).catch(function (err) { return console.error(err.toString()); }); } oldColumn = Math.floor(x/CELL_SIZE); oldRow = Math.floor(y/CELL_SIZE); } } else { connection.invoke("OpenCell", playerName, Math.floor(canvasX/CELL_SIZE) ,Math.floor(canvasY/CELL_SIZE)).catch(function (err) { return console.error(err.toString()); }); } // ドラッグしている最中の最後の座標を記憶しておく dragLastX = canvasX; dragLastY = canvasY; } |
旗を立てる処理
旗を立てるときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function SetFlag(e) { const rect = e.target.getBoundingClientRect(); // ブラウザ上での座標を求める const viewX = e.clientX - rect.left, viewY = e.clientY - rect.top; // 表示サイズとキャンバスの実サイズの比率を求める const scaleWidth = can.clientWidth / can.width, scaleHeight = can.clientHeight / can.height; // ブラウザ上でのクリック座標をキャンバス上に変換 const canvasX = Math.floor( viewX / scaleWidth ), canvasY = Math.floor( viewY / scaleHeight ); let playerName = document.getElementById('player-name').value; connection.invoke("SetFlag", playerName, Math.floor(canvasX / CELL_SIZE) ,Math.floor(canvasY / CELL_SIZE)).catch(function (err) { return console.error(err.toString()); }); } |
マウスが移動しているときの処理です。マウスの左ボタンが押されているときだけ、前述のOpenCell関数を呼びます。
1 2 3 4 5 |
function MouseMove(e) { if(isMouseDwon){ OpenCell(e); } } |
マウスのボタンが離されたときの処理を示します。isMouseDwonフラグをクリアし、dragLastXとdragLastYには不適切な値を設定します。
1 2 3 4 5 |
function MouseUp(e) { isMouseDwon = false; dragLastX = -1; dragLastY = -1; } |
右クリックしたときのデフォルトの動作(コンテキストメニューの表示)を抑制します。
1 2 3 |
can.addEventListener("contextmenu", e => { e.preventDefault(); }); |
プレイヤー名とスコアの表示
プレイヤーのスコアや表示内容に変更があった場合、クライアントサイドではReceiveScoresを受信します。この場合は受け取った文字列を各要素のinnerHTMLにセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
connection.on("ReceiveScores", function (names, scores) { if(names == ''){ document.getElementById('scores').innerHTML = ''; return; } let playerNames = names.split(','); let playerScores = scores.split(','); let html = ''; for(let i = 0; i < playerNames.length; i++) html += '<div style = "margin-bottom:5px;">' + playerNames[i] + ' ( ' + playerScores[i] + ' )</div>'; document.getElementById('scores').innerHTML = html; }); |
プレイヤーが地雷ではないセルを開いたときの処理を示します。この場合はReceiveOpenを受信します。効果音を鳴らし、スコアを表示します。また二次元配列cellsOpenedYourselfの要素をtrueにします。
1 2 3 4 5 6 7 8 |
connection.on("ReceiveOpen", function (score, row, col) { if (document.getElementById('sound-checkbox').checked) { openSound.currentTime = 0; openSound.play(); } document.getElementById('score').innerHTML = 'SCORE ' + score; cellsOpenedYourself[row][col] = true; }); |
残りのセル数はReceiveUnopenedCellCountで送られてきます。これもページに表示させます。
1 2 3 |
connection.on("ReceiveUnopenedCellCount", function (count) { document.getElementById('count').innerHTML = '残り ' + count; }); |
ステージクリア時の処理
クリアしたときは効果音を鳴らして二次元配列cellsOpenedYourselfを空にします。次のステージで誰かがセルを開けると二次元配列が再構築されます。
1 2 3 4 5 6 7 8 |
connection.on("ReceiveCleared", function (count) { cellsOpenedYourself = []; if (document.getElementById('sound-checkbox').checked) { clearedSound.currentTime = 0; clearedSound.play(); } }); |
ゲームオーバー時の処理
ゲームオーバー時の処理を示します。
ゲームオーバーになるとクライアントサイドではReceiveGameOverを受信することになります。このときは再度[ゲームスタート]のボタンが押せるようにします。それから二次元配列cellsOpenedYourselfを空にすると同時にSetSparks関数を呼び出して周囲に飛び散る火花を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
connection.on("ReceiveGameOver", function (row, col) { document.getElementById('start-button').style.display = 'block'; deadCol = col; deadRow = row; SetSparks(deadCol * CELL_SIZE, deadRow * CELL_SIZE); cellsOpenedYourself = []; if (document.getElementById('sound-checkbox').checked) { deadSound.currentTime = 0; deadSound.play(); } }); |
火花を飛ばす演出
1 2 3 4 5 6 7 8 9 10 |
function SetSparks(x, y){ for (let i = 0; i < 48; i++) { let r = Math.random() * 100; let rad = Math.PI * 2 * r / 100; let v = 8 + Math.random() * 16; fires.push(new Fire(x, y, v * Math.cos(rad), v * Math.sin(rad))); } } |
火花を飛ばす演出をいれるのでクライアント側でタイマーを使います。火花はfires配列に格納されますが、通常時は配列は空です。配列内に要素が存在するときはバックアップしておいた配列をつかってセルを描画し、そのうえに火花を描画します。
火花の描画はFireクラスのUpdate関数を呼び出して位置を移動させるとともに描画処理をおこなうことで実現します。1回の更新処理をするたびにFireクラスのTimeToDisappearanceが減少していきます。0になったらfires配列をクリアします。
それから自分が踏んでしまった地雷も描画しないと、クリックした瞬間爆発し、しかもそこには地雷がないのでユーザーはバグではないかと不信に思ってしまいます。そこで踏んでしまった地雷も描画します。この地雷はゲームに再挑戦してどこか別のセルをクリックすると消滅します。
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 |
setInterval(()=>{ if(fires.length == 0) return; // バックアップしておいたデータでセルを描画する ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); let index = 0; for(let row=0; row<oldRowMax; row++) { for(let col=0; col<oldColMax; col++) { // 踏んでしまった地雷を描画する if(deadCol == col && deadRow == row) DrawCell(col, row, -1); else DrawCell(col, row, oldCellArray[index]); index++; } } let size = 48; for(let i=0; i<fires.length; i++){ fires[i].Update(); if(fires[i].TimeToDisappearance > 10) ctx.drawImage(imgFires[0], fires[i].X, fires[i].Y, size, size); else if(fires[i].TimeToDisappearance > 8) ctx.drawImage(imgFires[1], fires[i].X, fires[i].Y, size, size); else if(fires[i].TimeToDisappearance > 6) ctx.drawImage(imgFires[2], fires[i].X, fires[i].Y, size, size); else if(fires[i].TimeToDisappearance > 4) ctx.drawImage(imgFires[3], fires[i].X, fires[i].Y, size, size); else if(fires[i].TimeToDisappearance > 2) ctx.drawImage(imgFires[4], fires[i].X, fires[i].Y, size, size); else if(fires[i].TimeToDisappearance > 0) ctx.drawImage(imgFires[5], fires[i].X, fires[i].Y, size, size); } if(fires.length > 0 && fires[0].TimeToDisappearance < 0) fires = []; }, 33); |
こんにちは。
マインスイーパーのフォームアプリを作成しました。
今回は、地雷の埋め込み、一気にセルを開くなど参考にさせていただきました。
それでは。