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
前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る