JavaScript 鳥が羽ばたく壁避けゲームをつくる(準備編)の続きです。今回はゲームとして完成させます。
最初にHTMLを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳥が羽ばたく壁避けゲーム</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel = "stylesheet" href = "./style.css" type = "text/css" media = "all"> </head> <body> <div id = "container"> <div id = "field"> <canvas id = "canvas"></canvas> <button id = "start" class = "buttton">START</button> <button id = "jump" class = "buttton">はばたく</button> <label><input type="checkbox" id="guide">ガイドを表示する</label> </div> </div> <script src= "./index.js"></script> </body> </html> |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
body { background-color: #000; color: #fff; } #container{ width: 360px; } #field { position: relative; } .buttton { position: absolute; left:120px; top:380px; width: 120px;; height: 70px;; background-color: transparent; color: #fff; font-size: 20px; } |
グローバル変数と定数
グローバル変数と定数を示します。
index.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 |
// canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 480; const AXIS_X = 80; // 放物線の軸(羽ばたいて右に80移動したときが頂点) const G_ACCELERATION = 0.02; // 重力加速度 // canvas要素 const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // プレイヤーのイメージ const playerImage = new Image(); // ボタン要素 const $jump = document.getElementById('jump'); const $start = document.getElementById('start'); // ガイドの表示非表示を切り替えるチェックボックス const $guide = document.getElementById('guide'); // プレイヤーの幅と高さ const PLAYER_WIDTH = 80; const PLAYER_HEIGHT = 48; const INIT_PLAYER_X = 100; // プレイヤーのX方向の初期座標 const DRAW_PLAYER_X = 50; // プレイヤーを描画するX座標 let isPlaying = false; // 現在プレイ中か? // 現在のプレイヤーの座標 let playerX = INIT_PLAYER_X; let playerY = 0; // 前回羽ばたいた座標(undefinedなら一度も羽ばたいていない) let jumpX = undefined; let jumpY = undefined; // 連続羽ばたきの制限用の変数 let allowJump = true; const guidePoints = []; // ガイドの点の座標の配列 const topPoints = []; // 壁の穴がある位置の座標の配列 const walls = []; // 壁オブジェクトの配列 // 効果音 const deadSound = new Audio('./sounds/dead.wav'); const jumpSound = new Audio('./sounds/jump.wav'); const gameoverSound = new Audio('./sounds/gameover.mp3'); |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。canvasのサイズを調整し、画像ファイルの読み込み、イベントリスナーの追加をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; playerImage.src = './images/player.png'; $start.addEventListener('click', () => gameStart()); $jump.addEventListener('click', () => jump()); clearCanvas(); // canvas全体を黒で塗りつぶす // ゲーム開始前なのに「羽ばたく」ボタンが表示されているのはおかしいので非表示にする $jump.style.display = 'none'; } function clearCanvas(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, $canvas.width, $canvas.height); } |
ゲーム開始の処理
ゲーム開始時におこなわれる処理を示します。
配列に格納されているデータをクリアし、プレイヤーの座標を初期値に戻します。プレイ開始時は前回の羽ばたきの座標は存在しないのでundefinedを代入します。ガイドと壁を生成し、isPlayingフラグをセットします。スタートボタンを非表示にして羽ばたくためのボタンを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function gameStart(){ guidePoints.length = 0; topPoints.length = 0; walls.length = 0; jumpX = undefined; jumpY = undefined; playerX = INIT_PLAYER_X; playerY = 0; createGuide() createWalls() isPlaying = true; $start.style.display = 'none'; $jump.style.display = 'block'; } |
createGuide関数とcreateWalls関数、getPositionY関数は前回示したものとまったく同じです。
羽ばたく動作
プレイヤーが羽ばたいたときに行なわれる処理を示します。
連続でボタンを押しても最低1秒は経過しないと処理を受け付けないようにします。また処理がおこなわれたときは効果音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 |
function jump(){ if(!allowJump) return; allowJump = false; jumpX = playerX; jumpY = playerY; jumpSound.currentTime = 0; jumpSound.play(); setTimeout(() => allowJump = true, 1000); } |
更新処理
更新処理を示します。
1秒間に60回更新処理をおこないますが、isPlayingフラグがfalseのときはなにもしません。
更新時はプレイヤーのX座標を1大きくしてY座標を求めます。そして壁とガイド(ただし表示する設定の場合のみ)、プレイヤーの描画をおこないます。このときプレイヤーが常にcanvasの左側に描画されるように全体を平行移動してから描画します。そのあと当たり判定をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
setInterval(() => { update(); }, 1000 / 60); function update(){ if(!isPlaying) return; playerX++; playerY = getPositionY(playerX, jumpX, jumpY) clearCanvas(); drawWalls(); if($guide.checked) drawGuide(); drawPlayer(); check(); } |
drawWalls関数を示します。前回示したものと違ってX座標を(playerX – DRAW_PLAYER_X)だけ左にズラしています。
1 2 3 4 5 6 7 8 |
function drawWalls(){ for(let i = 0; i < walls.length; i++){ ctx.fillStyle = '#f00'; ctx.fillRect(walls[i].X - (playerX - DRAW_PLAYER_X) , 0, 4, $canvas.height); ctx.fillStyle = '#000'; ctx.fillRect(walls[i].X - (playerX - DRAW_PLAYER_X) , walls[i].Min, 4, walls[i].Max - walls[i].Min); } } |
drawGuide関数を示します。これも前回示したものと違ってX座標を(playerX – DRAW_PLAYER_X)だけ左にズラしています。
1 2 3 4 5 6 |
function drawGuide(){ for(let i=0; i<guidePoints.length; i++){ ctx.fillStyle = '#0f0'; ctx.fillRect(guidePoints[i].x + PLAYER_WIDTH / 2 - (playerX - DRAW_PLAYER_X), guidePoints[i].y - PLAYER_HEIGHT / 2, 2, 2); } } |
プレイヤーを描画するためのdrawPlayer関数を示します。矩形だけでなく鳥のイメージも合わせて描画します。
1 2 3 4 5 |
function drawPlayer(){ ctx.drawImage(playerImage, playerX- PLAYER_WIDTH/2 - (playerX - DRAW_PLAYER_X), playerY- PLAYER_HEIGHT/2, PLAYER_WIDTH, PLAYER_HEIGHT); ctx.strokeStyle = '#fff'; ctx.strokeRect(playerX- PLAYER_WIDTH/2 - (playerX - DRAW_PLAYER_X), playerY- PLAYER_HEIGHT/2, PLAYER_WIDTH, PLAYER_HEIGHT); } |
当たり判定とゲームオーバー時の処理
当たり判定をする処理を示します。当たり判定の対象となる壁があるか調べて、ある場合はその穴の一番上の座標と下の座標、プレイヤーのY座標を比較して壁に当たっているかを調べます。壁に当たっている場合やcanvasの上下からはみ出した場合はゲームオーバーです。当たった部分に×を描画し、更新処理を停止させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function check(){ const checkWalls = walls.filter(wall => wall.X < playerX + PLAYER_WIDTH / 2 && wall.X > playerX - PLAYER_WIDTH / 2); if(checkWalls.length > 0){ if(checkWalls[0].Min > playerY - PLAYER_HEIGHT / 2) onDead(checkWalls[0].X, playerY - PLAYER_HEIGHT / 2); if(checkWalls[0].Max < playerY + PLAYER_HEIGHT / 2) onDead(checkWalls[0].X, playerY + PLAYER_HEIGHT / 2); } // canvasの上下からはみ出した場合も死亡判定とする if(playerY - PLAYER_HEIGHT / 2 < 0) onDead(playerX, playerY - PLAYER_HEIGHT / 2); if(playerY + PLAYER_HEIGHT / 2 > $canvas.height) onDead(playerX, playerY + PLAYER_HEIGHT / 2); } |
ゲームオーバー時の処理を示します。
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 |
function onDead(deadX, deadY){ isPlaying = false; // 更新処理の停止 // 死亡効果音の再生 deadSound.currentTime = 0; deadSound.play(); // ぶつかった部分に×を描画する if(deadX != undefined){ ctx.lineWidth = 4; ctx.strokeStyle = '#f00'; ctx.beginPath(); ctx.moveTo(deadX - 16 - (playerX - DRAW_PLAYER_X), deadY - 16); ctx.lineTo(deadX + 16 - (playerX - DRAW_PLAYER_X), deadY + 16); ctx.stroke(); ctx.beginPath(); ctx.moveTo(deadX + 16 - (playerX - DRAW_PLAYER_X), deadY - 16); ctx.lineTo(deadX - 16 - (playerX - DRAW_PLAYER_X), deadY + 16); ctx.stroke(); } ctx.lineWidth = 1; // ゲームオーバーなので羽ばたくボタンは非表示 $jump.style.display = 'none'; // しばらく待機してゲームオーバーの効果音をならしてゲームスタートのボタンを再表示する setTimeout(() => { gameoverSound.currentTime = 0; gameoverSound.play(); }, 1000); setTimeout(() => { $start.style.display = 'block'; }, 3000); } |