衝突判定と爆発

UFOもキャノン砲も動き、ビームも発射できるようになったので、最後に、ビームがUFOに当たったときにUFOを爆発させる処理を組み込みます。

爆発したUFOの絵を用意する

ここに置いてあるUFOゲームをやってみるとわかるように、今回のUFOゲームでは、UFOにビームが当たったら、UFOはしばらく爆発炎上し、その後また新たなUFOがやってきてゲームが再開します。

UFOの爆発は、爆発したUFOの絵を2枚、切り替えて表示することで表現しています。そこで事前準備として、爆発したUFOの絵を2枚用意します。これもペイントで描きました。

図1: 爆発したUFOの絵

ファイル名はそれぞれufo_explosion1.png, ufo_explosion2.pngです(explosion(エクスプロージョン)とは、爆発という意味です)。今回は、いろいろ簡略化するために、爆発前のUFOと同じサイズの画像にしています。爆発前のUFOの画像の上のところにちょっと隙間があるのは、このためです。

UFOゲームプログラムリスト その1

今回も、特に新しい知識が必要なわけではないので、いきなりプログラムを載せてしまいます(リスト1)。今回追記した部分は赤字にしました。

リスト1: lesson06_1.html (衝突判定あり)
  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の表示領域の中に入っていることを判定すればよいわけです。

図2: 当たり判定

まず、72行目は、現在ビームが発射中であるかどうかの判定です。ビームが発射されていなければ判定も不要なので何もしません。

73~74行目のif文の条件式が、当たり判定の本体です。図2にあるように、ビームの先端のX座標beamXufoXufoX + 79の間にあること、およびビームの先端のY座標beamYufoYufoY + 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爆発画像が交互に切り替えて表示されます。なお、ufoExplosionImage1ufoExplosionImage2はそれぞれ15~18行目で読み込んでいます。

そして、UFOの爆発アニメーションの表示回数が30回以下なら、setTimeout()を仕掛けてまたshootDown()が呼び出されるようにしますし(146行目)、30回を超えたら、UFOの位置とビーム発射状態を初期化して、mainLoop()を呼び出します。これによりゲームが再開されます。

ここをクリックすると、リスト1の状態のページが別タブで開きます。

――どうでしょうか。確かにUFOがビームで撃墜できるようになったので、これで完成としてもよいように思いますが、ビームがUFOに当たってUFOが爆発炎上している間、ビームが表示されっぱなしなのが微妙に気になります(図3)。

図3: ビームが残っている

撃っているものがビームじゃなくて実体兵器の矢とかなら※1、UFOに刺さったまま、というのもありかもしれませんが、ビームだもんねえ消えるべきだよねえ、ということであとちょっとだけ修正します。

UFOゲームプログラムリスト いったんの完成版

というわけで微妙に直したのがリスト2です。

リスト2: lesson06_2.html (UFOゲーム完成版)
  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



前のページ | ひとつ上のページに戻る | トップページに戻る