UFOもキャノン砲も動き、ビームも発射できるようになったので、最後に、ビームがUFOに当たったときにUFOを爆発させる処理を組み込みます。
ここに置いてあるUFOゲームをやってみるとわかるように、今回のUFOゲームでは、UFOにビームが当たったら、UFOはしばらく爆発炎上し、その後また新たなUFOがやってきてゲームが再開します。
UFOの爆発は、爆発したUFOの絵を2枚、切り替えて表示することで表現しています。そこで事前準備として、爆発したUFOの絵を2枚用意します。これもペイントで描きました。
ファイル名はそれぞれufo_explosion1.png, ufo_explosion2.pngです(explosion(エクスプロージョン)とは、爆発という意味です)。今回は、いろいろ簡略化するために、爆発前のUFOと同じサイズの画像にしています。爆発前のUFOの画像の上のところにちょっと隙間があるのは、このためです。
今回も、特に新しい知識が必要なわけではないので、いきなりプログラムを載せてしまいます(リスト1)。今回追記した部分は赤字にしました。
1: <!DOCTYPE html> 2: <html lang="ja"> 3: <head> 4: <meta charset="UTF-8"> 5: <title>UFOゲーム</title> 6: </head> 7: <body> 8: <canvas id="canvas" width="800px" height="600px" style="background-color:black;"></canvas> 9: <script> 10: "use strict"; 11: // UFOの画像読み込み 12: const ufoImage = new Image(); 13: ufoImage.src = "./ufo.png"; 14: // UFOの画像読み込み(爆発2パターン) 15: const ufoExplosionImage1 = new Image(); 16: ufoExplosionImage1.src = "./ufo_explosion1.png"; 17: const ufoExplosionImage2 = new Image(); 18: ufoExplosionImage2.src = "./ufo_explosion2.png"; 19: 20: // キャノン砲の画像読み込み 21: const cannonImage = new Image(); 22: cannonImage.src = "./cannon.png"; 23: 24: const canvas = document.getElementById("canvas"); 25: const context = canvas.getContext("2d"); 26: 27: // UFOの初期位置は、とりあえず左上とする 28: let ufoX = 0; 29: let ufoY = 0; 30: // 「目標点」の初期位置も、左上とする 31: let ufoTargetX = 0; 32: let ufoTargetY = 0; 33: 34: // キャノン砲の初期位置は、真ん中あたりとする 35: let cannonX = 375; 36: 37: // 左右のカーソルキーが押されているかどうかを保持するフラグ 38: let leftKeyPressed = false; 39: let rightKeyPressed = false; 40: 41: // ビームが現在発射中であることを示すフラグ 42: let beamFlag = false; 43: // ビームのX, Y座標 44: let beamX; 45: let beamY; 46: 47: document.onkeydown = keyDown; 48: document.onkeyup = keyUp; 49: 50: // UFOが爆発するアニメーションの繰り返し回数 51: let ufoExplosionCounter; 52: 53: mainLoop(); 54: 55: function mainLoop() { 56: // 画面をクリアする 57: context.clearRect(0, 0, 800, 600); 58: // UFOを描く 59: context.drawImage(ufoImage, ufoX, ufoY); 60: // キャノン砲を描く 61: context.drawImage(cannonImage, cannonX, 570); 62: // (発射中なら)ビームを描く 63: if (beamFlag) { 64: context.strokeStyle = "yellow"; 65: context.beginPath(); 66: context.moveTo(beamX, beamY); 67: context.lineTo(beamX, beamY + 20); 68: context.stroke(); 69: } 70: 71: // ビームが当たったかどうかを判定する 72: if (beamFlag) { 73: if (beamX > ufoX && beamX < ufoX + 79 74: && beamY > ufoY && beamY < ufoY + 31) { 75: ufoExplosionCounter = 0; 76: shootDown(); 77: return; 78: } 79: } 80: 81: // UFOを動かす 82: if (ufoX === ufoTargetX && ufoY === ufoTargetY) { 83: // UFOの位置と目標点が一致しているので、新たな目標点を設定 84: ufoTargetX = Math.floor(Math.random() * 73) * 10; 85: ufoTargetY = Math.floor(Math.random() * 51) * 10; 86: } 87: // UFOを目標点に近づける 88: if (ufoX < ufoTargetX) { 89: ufoX += 10; 90: } else if (ufoX > ufoTargetX) { 91: ufoX -= 10; 92: } 93: if (ufoY < ufoTargetY) { 94: ufoY += 10; 95: } else if (ufoY > ufoTargetY) { 96: ufoY -= 10; 97: } 98: 99: // キャノン砲を動かす 100: if (leftKeyPressed && cannonX > 0) { 101: cannonX -= 10; 102: } else if (rightKeyPressed && cannonX < 750) { 103: cannonX += 10; 104: } 105: // (発射中なら)ビームを動かす 106: if (beamFlag) { 107: if (beamY > 0) { 108: beamY -= 10; 109: } else { 110: beamFlag = false; 111: } 112: } 113: 114: setTimeout(mainLoop, 20); 115: } 116: 117: function keyDown(e) { 118: if (e.code === "ArrowLeft") { 119: leftKeyPressed = true; 120: } else if (e.code === "ArrowRight") { 121: rightKeyPressed = true; 122: } else if (e.code === "Space" && !beamFlag) { 123: beamFlag = true; 124: beamX = cannonX + 25; 125: beamY = 550; 126: } 127: } 128: 129: function keyUp(e) { 130: if (e.code === "ArrowLeft") { 131: leftKeyPressed = false; 132: } else if (e.code === "ArrowRight") { 133: rightKeyPressed = false; 134: } 135: } 136: 137: // UFOを撃墜したときに呼び出される関数 138: function shootDown() { 139: if (ufoExplosionCounter % 2 === 0) { 140: context.drawImage(ufoExplosionImage1, ufoX, ufoY); 141: } else { 142: context.drawImage(ufoExplosionImage2, ufoX, ufoY); 143: } 144: ufoExplosionCounter++; 145: if (ufoExplosionCounter <= 30) { 146: setTimeout(shootDown, 100); 147: } else { 148: ufoX = 0; 149: ufoY = 0; 150: ufoTargetX = 0; 151: ufoTargetY = 0; 152: beamFlag = false; 153: mainLoop(); 154: } 155: } 156: </script> 157: </body> 158: </html>
まずは、ビームがUFOに当たったことを判定しているところ(72~79行目)から見ていきましょう。
71: // ビームが当たったかどうかを判定する 72: if (beamFlag) { 73: if (beamX > ufoX && beamX < ufoX + 79 74: && beamY > ufoY && beamY < ufoY + 31) { 75: ufoExplosionCounter = 0; 76: shootDown(); 77: return; 78: } 79: }
「当たったことを判定」するには、UFOと(キャノン砲と)ビームを描いた後で、ビームの先端(このゲームでは、ビームの先端だけで当たり判定しています)が、UFOの表示領域の中に入っていることを判定すればよいわけです。
まず、72行目は、現在ビームが発射中であるかどうかの判定です。ビームが発射されていなければ判定も不要なので何もしません。
73~74行目のif文の条件式が、当たり判定の本体です。図2にあるように、ビームの先端のX座標beamXがufoXとufoX + 79の間にあること、およびビームの先端のY座標beamYがufoYとufoY + 31の間にあることを判定しています。この79とか31とかの値がどこから来ているかといえば、UFOの画像サイズが79×31だからです。
今回は、UFOの画像そのものに、左右と上部に黒い余白があります。今回の判定ではこの部分に当たった時も当たりになってしまう――ように見えますが、実のところUFOもビームも座標は10の倍数にしかならず、当たり判定の条件式では等号を含まない「<」、「>」を使っていますから、ちょうど等しければ判定から外れます。まあ、このあたりは、ゲームの難易度調整として適当に調整すればよいかと思います。
ところで、このUFOゲームでは、UFOもビームも10ピクセル単位で動きます。これがUFOやビームの動くスピードになっているわけですが、ビームがなかなか当たらないのでビームの速度を上げるため、一度に40ピクセル動かすようにしたりすると、運が悪いとビームがUFOをすり抜けます。40ピクセルはUFOの高さよりも大きいので無理もないですが、UFOも動いています。UFOが下りてくる時に重なると、もっと簡単にすり抜けが発生します。ビームやUFOの速度を調整するときは、当たり判定も考慮する必要があるでしょう。
さて、いざビームがUFOに当たった後はどうしているかというと、75行目で、ufoExplosionCounterという変数を0にしています。この変数は51行目で宣言していますが、UFOが爆発するアニメーションの繰り返し回数です。これを0にしたうえで、shootDown()という関数を呼び出して、そのままreturnしています(shoot downとは「撃墜する」という意味です)。なので次はshootDown()関数を見てみましょう。
137: // UFOを撃墜したときに呼び出される関数 138: function shootDown() { 139: if (ufoExplosionCounter % 2 === 0) { 140: context.drawImage(ufoExplosionImage1, ufoX, ufoY); 141: } else { 142: context.drawImage(ufoExplosionImage2, ufoX, ufoY); 143: } 144: ufoExplosionCounter++; 145: if (ufoExplosionCounter <= 30) { 146: setTimeout(shootDown, 100); 147: } else { 148: ufoX = 0; 149: ufoY = 0; 150: ufoTargetX = 0; 151: ufoTargetY = 0; 152: beamFlag = false; 153: mainLoop(); 154: } 155: }
shootDown()関数の139~143行目では、爆発したUFOの画像を表示しています。139行目で使っている%演算子は、割り算の余りを求める演算子です(こちらの表のNo.7を参照)。139行目では、「2で割った余りが0であるかどうか」を判定していますから、これはつまりufoExplosionCounterが偶数だったらufoExplosionImage1を、そうでなければ(奇数なら)ufoExplosionImage2を表示する、ということです。そして、ufoExplosionCounterは144行目でインクリメントしていますから、shootDown()が呼び出されるたびに、2種類のUFO爆発画像が交互に切り替えて表示されます。なお、ufoExplosionImage1、ufoExplosionImage2はそれぞれ15~18行目で読み込んでいます。
そして、UFOの爆発アニメーションの表示回数が30回以下なら、setTimeout()を仕掛けてまたshootDown()が呼び出されるようにしますし(146行目)、30回を超えたら、UFOの位置とビーム発射状態を初期化して、mainLoop()を呼び出します。これによりゲームが再開されます。
ここをクリックすると、リスト1の状態のページが別タブで開きます。
――どうでしょうか。確かにUFOがビームで撃墜できるようになったので、これで完成としてもよいように思いますが、ビームがUFOに当たってUFOが爆発炎上している間、ビームが表示されっぱなしなのが微妙に気になります(図3)。
撃っているものがビームじゃなくて実体兵器の矢とかなら※1、UFOに刺さったまま、というのもありかもしれませんが、ビームだもんねえ消えるべきだよねえ、ということであとちょっとだけ修正します。
というわけで微妙に直したのがリスト2です。
1: <!DOCTYPE html> 2: <html lang="ja"> 3: <head> 4: <meta charset="UTF-8"> 5: <title>UFOゲーム</title> 6: </head> 7: <body> 8: <canvas id="canvas" width="800px" height="600px" style="background-color:black;"></canvas> 9: <script> 10: "use strict"; 11: // UFOの画像読み込み 12: const ufoImage = new Image(); 13: ufoImage.src = "./ufo.png"; 14: // UFOの画像読み込み(爆発2パターン) 15: const ufoExplosionImage1 = new Image(); 16: ufoExplosionImage1.src = "./ufo_explosion1.png"; 17: const ufoExplosionImage2 = new Image(); 18: ufoExplosionImage2.src = "./ufo_explosion2.png"; 19: 20: // キャノン砲の画像読み込み 21: const cannonImage = new Image(); 22: cannonImage.src = "./cannon.png"; 23: 24: const canvas = document.getElementById("canvas"); 25: const context = canvas.getContext("2d"); 26: 27: // UFOの初期位置は、とりあえず左上とする 28: let ufoX = 0; 29: let ufoY = 0; 30: // 「目標点」の初期位置も、左上とする 31: let ufoTargetX = 0; 32: let ufoTargetY = 0; 33: 34: // キャノン砲の初期位置は、真ん中あたりとする 35: let cannonX = 375; 36: 37: // 左右のカーソルキーが押されているかどうかを保持するフラグ 38: let leftKeyPressed = false; 39: let rightKeyPressed = false; 40: 41: // ビームが現在発射中であることを示すフラグ 42: let beamFlag = false; 43: // ビームのX, Y座標 44: let beamX; 45: let beamY; 46: 47: document.onkeydown = keyDown; 48: document.onkeyup = keyUp; 49: 50: // UFOが爆発するアニメーションの繰り返し回数 51: let ufoExplosionCounter; 52: 53: mainLoop(); 54: 55: function mainLoop() { 56: // 画面をクリアする 57: context.clearRect(0, 0, 800, 600); 58: // UFOを描く 59: context.drawImage(ufoImage, ufoX, ufoY); 60: // キャノン砲を描く 61: context.drawImage(cannonImage, cannonX, 570); 62: if (beamFlag) { 63: // (発射中なら)ビームが当たったかどうかを判定する 64: if (beamX > ufoX && beamX < ufoX + 79 65: && beamY > ufoY && beamY < ufoY + 31) { 66: ufoExplosionCounter = 0; 67: shootDown(); 68: return; 69: } 70: 71: // ビームを描く 72: context.strokeStyle = "yellow"; 73: context.beginPath(); 74: context.moveTo(beamX, beamY); 75: context.lineTo(beamX, beamY + 20); 76: context.stroke(); 77: } 78: 79: // UFOを動かす 80: if (ufoX === ufoTargetX && ufoY === ufoTargetY) { 81: // UFOの位置と目標点が一致しているので、新たな目標点を設定 82: ufoTargetX = Math.floor(Math.random() * 73) * 10; 83: ufoTargetY = Math.floor(Math.random() * 51) * 10; 84: } 85: // UFOを目標点に近づける 86: if (ufoX < ufoTargetX) { 87: ufoX += 10; 88: } else if (ufoX > ufoTargetX) { 89: ufoX -= 10; 90: } 91: if (ufoY < ufoTargetY) { 92: ufoY += 10; 93: } else if (ufoY > ufoTargetY) { 94: ufoY -= 10; 95: } 96: 97: // キャノン砲を動かす 98: if (leftKeyPressed && cannonX > 0) { 99: cannonX -= 10; 100: } else if (rightKeyPressed && cannonX < 750) { 101: cannonX += 10; 102: } 103: // (発射中なら)ビームを動かす 104: if (beamFlag) { 105: if (beamY > 0) { 106: beamY -= 10; 107: } else { 108: beamFlag = false; 109: } 110: } 111: 112: setTimeout(mainLoop, 20); 113: } 114: 115: function keyDown(e) { 116: if (e.code === "ArrowLeft") { 117: leftKeyPressed = true; 118: } else if (e.code === "ArrowRight") { 119: rightKeyPressed = true; 120: } else if (e.code === "Space" && !beamFlag) { 121: beamFlag = true; 122: beamX = cannonX + 25; 123: beamY = 550; 124: } 125: } 126: 127: function keyUp(e) { 128: if (e.code === "ArrowLeft") { 129: leftKeyPressed = false; 130: } else if (e.code === "ArrowRight") { 131: rightKeyPressed = false; 132: } 133: } 134: 135: // UFOを撃墜したときに呼び出される関数 136: function shootDown() { 137: if (ufoExplosionCounter % 2 === 0) { 138: context.drawImage(ufoExplosionImage1, ufoX, ufoY); 139: } else { 140: context.drawImage(ufoExplosionImage2, ufoX, ufoY); 141: } 142: ufoExplosionCounter++; 143: if (ufoExplosionCounter <= 30) { 144: setTimeout(shootDown, 100); 145: } else { 146: ufoX = 0; 147: ufoY = 0; 148: ufoTargetX = 0; 149: ufoTargetY = 0; 150: beamFlag = false; 151: mainLoop(); 152: } 153: } 154: </script> 155: </body> 156: </html>
修正点は、リスト1では、UFOとキャノン砲とビームを表示してから当たり判定していたのを、ビームの表示だけ、衝突判定の後にしたことです。すべての登場人物を表示したうえで、画面上で重なっているから当たったと判定する、というのは、確かにわかりやすくてよいのですが、やはりビームが残るのは変ですし、もしビームを描画していたとしたら重なって見えたはずなので、問題ないでしょう。
ここをクリックすると、動く画面が別タブで開きます。
さて、ここまでのプログラムで、UFOゲームを作ることができました。いかがでしたでしょうか? ←書いてみたかった
最終版のUFOゲーム(lesson06_2.html)は、HTML部分とか空行(改行だけの行)とかコメント行とか込み込みで156行です。初心者のうちは巨大なプログラムに見えるかもしれませんが、慣れればすぐに読み書きできるようになるサイズでしょうし、こちらのシューティングゲームで505行、こちらのテトリス風ゲームで407行程度です。もうちょっと頑張れば、これくらいのゲームならすぐに手が届きます。
読者の皆様がプログラミングの楽しさに触れられますように。
公開日: 2021/06/20
前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る