UFOが動かせたので、次は、UFOを撃墜するキャノン砲を動かします。
キャノン砲は、キーボードの左右のカーソルキーで動かします。よって、キーボードからの入力を取得する必要があります。
BMI計算プログラムで「計算する」ボタンのクリックがonclickで取得できたように、キーボードのキーを押したことと離したことは、onkeydownとonkeyupで取得できます。やってみましょう。
この下の入力フィールドをクリックしてフォーカスを当ててから、キーを押してください。
キー押下のイベントがここに表示されます
キーを離したイベントがここに表示されます
一番上のテキストフィールドをクリックしてフォーカスを当ててから、何らかのキーを押すと、押した瞬間に「キー押下のイベントがここに表示されます」のところにそのキーを表す文字列が表示され(たとえば「A」のキーならKeyA)、離したときに「キーを話したイベントがここに表示されます」のところに離したキーを表す文字列が表示されることがわかると思います。キーを押しっぱなしにすると、キー押下のイベントのところに「タッ、タタタタタタタタタ……」という感じで、押したちょっと後から繰り返しイベントが発生していることがわかると思います(エディタの中でキーを押しっぱなしにしたときと同じですね)。ただし、キーを離したイベントは、本当にキーを離したときに1回だけ起きます。
これを実現しているプログラムが、下のリスト1です(見た目についてはCSSでいろいろ制御しているのですが、そこは割愛しています)。
1: <div style="background-color: pink;"> 2: <p>この下の入力フィールドをクリックしてフォーカスを当ててから、キーを押してください。</p> 3: <p><input id="key_event_area" style="width: 80%;"></p> 4: <p>キー押下のイベントがここに表示されます</p> 5: <p class="key_code_area" id="onkeydown_result"></p> 6: <p>キーを離したイベントがここに表示されます</p> 7: <p class="key_code_area" id="onkeyup_result"></p> 8: <p><button id="clear_button">クリアする</button></p> 9: </div> 10: <script> 11: const keyEventArea = document.getElementById("key_event_area"); 12: const onKeyDownResult = document.getElementById("onkeydown_result"); 13: const onKeyUpResult = document.getElementById("onkeyup_result"); 14: const clearButton = document.getElementById("clear_button"); 15: 16: // キーを押したときのイベントハンドラを仕掛ける 17: keyEventArea.onkeydown = keyDown; 18: // キーを離したときのイベントハンドラを仕掛ける 19: keyEventArea.onkeyup = keyUp; 20: clearButton.onclick = clearButtonPressed; 21: 22: function keyDown(e) { 23: onKeyDownResult.textContent += e.code + ", "; 24: } 25: 26: function keyUp(e) { 27: onKeyUpResult.textContent += e.code + ", "; 28: } 29: 30: function clearButtonPressed() { 31: keyEventArea.value = ""; 32: onKeyDownResult.textContent = ""; 33: onKeyUpResult.textContent = ""; 34: } 35: </script>
キー入力を受けるために、3行目でinput要素を作っています。今回のテストプログラムでは、特定の要素でキー入力を受け取りたいのですが、それをするためには、まずその要素をクリックして「フォーカスを当てる」必要があります。現状のブラウザでは、たとえばp要素には、フォーカスを当てて選択状態にすることができません(本来は、もっと多くの要素でキーイベントを受けられそうではあるのですが)。そこで、このテストプログラムではinput要素を使っています。「UFOゲームを遊ぶとき、先にテキストフィールドをクリックしてくれなんて聞いたことないよ!」と思うかもしれませんが、ページ全体でキーボードイベントを受けるなら、bodyなりdocumentなりで受ければ大丈夫です。今回は、キーボードイベントを受ける場所を、ページ内に複数個所作りたかったので(下に出てきます)こうしました。
17行目と19行目で、そのinput要素に、キーを押したときのイベントハンドラと、キーを話したときのイベントハンドラを仕掛けています。これにより、キーが押されたときにはkeyDown()が、キーを離したときにはkeyUp()が呼び出されます。
keyDown()、keyUp()には、それぞれ引数として、キーボードイベントを表すオブジェクトが渡されていて、これらの関数ではそれを引数eで受け取っています。e.codeで、押された(または離された)キーを表現する「コード」が取得できるので、それを23行目、27行目で表示しています。「A」のキーを押したときに「KeyA」と表示されたのは、「A」を押したときにe.codeで渡されるコードが「KeyA」だったことを意味しています。
今回、UFOゲームでは、カーソルキーの左右のキーでキャノン砲を移動、スペースキーでビームを発射します。それぞれのキーを押したときに渡されるコードを上のプログラムで確認すると、表1のようになっていることがわかります。
キー | コード |
---|---|
左カーソルキー | ArrowLeft |
右カーソルキー | ArrowRight |
スペースキー | Space |
キーを押した時と離した時のイベントが取得できるようになりましたが、こと「UFOゲームのキャノン砲を動かす」には、このままでは使えません。
キーを押しっぱなしにすると、keyDown()関数は、「タッ、タタタタタタタタタ……」という感じで呼び出されていますが、キャノン砲を左右に動かすのに、そんな感じで動くのは変でしょう。また、キーをすごい勢いで連打すれば、keyDown()関数は連打した回数だけ呼び出されますが、この手のゲームでは、だからといってキャノン砲が速く動くわけではありません。ハイパーオリンピック※1なら連打すれば早く走れるでしょうが。
この手のゲームのプログラムは、普通に考えて、以下のような構造になると思いますが、
mainLoop() { ①UFOやキャノン砲を表示する ②弾が当たったかどうかを判定する ③UFOやキャノン砲を動かす }
この「③UFOやキャノン砲を動かす」のところの、「キャノン砲を動かす」という処理を分解すると、以下のようになるはずです。
mainLoop() { ①UFOやキャノン砲を表示する ②弾が当たったかどうかを判定する ③-1 UFOを動かす ③-2 キャノン砲を動かす(この処理を分解) if (今現在、「←」キーが押されていたら) { キャノン砲を左に動かす } if (今現在、「→」キーが押されていたら) { キャノン砲を右に動かす } }
上のように「今現在、このキーが押されていたら」という判定ができれば、左右のカーソルキーを押している間だけ、キャノン砲が一定速度で動きます。キーを押しっぱなしにしたとき、キャノン砲は、mainLoop()関数1回の実行で1回だけ左右のどちらかに動くからです。UFOゲームにおけるキャノン砲の動きとして望ましいのはこういう動きでしょう。
――しかし、JavaScriptでは、「今現在、このキーが押されているかどうか」を判定する方法はありません※2。なので自分で作らなければいけません。
でもまあ、それを実現する方法は、そんなに難しくありません。キーが押されたイベントと離されたイベントは取得できているわけですから、「今現在、このキーが押されているか?」を保持する論理型の変数を用意して、keyDown()でそれをtrueにし、keyUp()でfalseにすればよいのです。
このように、trueかfalseかで、現在の状態を表す変数のことをフラグ(flag)と呼びます。フラグとは旗のことです。なのでフラグをtrueにすることを「フラグを立てる」と言ったりします。
「←」キーが押されている間はフラグleftKeyPressedを立てて、「→」キーが押されている間はrightKeyPressedを立てる、というプログラムは、以下のようになります。
1: let leftKeyPressed = false; 2: let rightKeyPressed = false; 3: 4: keyEventArea.onkeydown = keyDown; 5: keyEventArea.onkeyup = keyUp; 6: 7: function keyDown(e) { 8: if (e.code === "ArrowLeft") { 9: leftKeyPressed = true; 10: } else if (e.code === "ArrowRight") { 11: rightKeyPressed = true; 12: } 13: } 14: 15: function keyUp(e) { 16: if (e.code === "ArrowLeft") { 17: leftKeyPressed = false; 18: } else if (e.code === "ArrowRight") { 19: rightKeyPressed = false; 20: } 21: }
下のエリアで実演します。テキストフィールドをクリックしてフォーカスを当ててから、左右のカーソルキーを押してください。各フラグの状態を表示するようにしました。
左のカーソルキーを「押している間だけ」leftKeyPressedがtrueになり、右のカーソルキーを「押している間だけ」rightKeyPressedがtrueになることがわかると思います。
この下の入力フィールドをクリックしてフォーカスを当ててから、左右のカーソルキーを押してください。
leftKeyPressed..
rightKeyPressed..
準備は整ったのでキャノン砲を動かします。まずキャノン砲の画像を用意します。
これもペイントで描きました。絵心がないのは勘弁してください。ファイル名はcannon.png、画像のサイズは50×25ピクセルです。
そして、前回のlesson04_7.htmlに、キャノン砲の描画と、キャノン砲を動かすコードを埋め込んだのが、リスト3です。
1: <!DOCTYPE html> 2: <html lang="ja"> 3: <head> 4: <meta charset="UTF-8"> 5: <title>キャノン砲を動かす</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: // キャノン砲の画像読み込み 15: const cannonImage = new Image(); 16: cannonImage.src = "./cannon.png"; 17: 18: const canvas = document.getElementById("canvas"); 19: const context = canvas.getContext("2d"); 20: 21: // UFOの初期位置は、とりあえず左上とする 22: let ufoX = 0; 23: let ufoY = 0; 24: // 「目標点」の初期位置も、左上とする 25: let ufoTargetX = 0; 26: let ufoTargetY = 0; 27: 28: // キャノン砲の初期位置は、真ん中あたりとする 29: let cannonX = 375; 30: 31: // 左右のカーソルキーが押されているかどうかを保持するフラグ 32: let leftKeyPressed = false; 33: let rightKeyPressed = false; 34: 35: document.onkeydown = keyDown; 36: document.onkeyup = keyUp; 37: 38: mainLoop(); 39: 40: function mainLoop() { 41: // 画面をクリアする 42: context.clearRect(0, 0, 800, 600); 43: // UFOを描く 44: context.drawImage(ufoImage, ufoX, ufoY); 45: // キャノン砲を描く 46: context.drawImage(cannonImage, cannonX, 570); 47: 48: // UFOを動かす 49: if (ufoX === ufoTargetX && ufoY === ufoTargetY) { 50: // UFOの位置と目標点が一致しているので、新たな目標点を設定 51: ufoTargetX = Math.floor(Math.random() * 73) * 10; 52: ufoTargetY = Math.floor(Math.random() * 51) * 10; 53: } 54: // UFOを目標点に近づける 55: if (ufoX < ufoTargetX) { 56: ufoX += 10; 57: } else if (ufoX > ufoTargetX) { 58: ufoX -= 10; 59: } 60: if (ufoY < ufoTargetY) { 61: ufoY += 10; 62: } else if (ufoY > ufoTargetY) { 63: ufoY -= 10; 64: } 65: 66: // キャノン砲を動かす 67: if (leftKeyPressed && cannonX > 0) { 68: cannonX -= 10; 69: } else if (rightKeyPressed && cannonX < 750) { 70: cannonX += 10; 71: } 72: 73: setTimeout(mainLoop, 20); 74: } 75: 76: function keyDown(e) { 77: if (e.code === "ArrowLeft") { 78: leftKeyPressed = true; 79: } else if (e.code === "ArrowRight") { 80: rightKeyPressed = true; 81: } 82: } 83: 84: function keyUp(e) { 85: if (e.code === "ArrowLeft") { 86: leftKeyPressed = false; 87: } else if (e.code === "ArrowRight") { 88: rightKeyPressed = false; 89: } 90: } 91: </script> 92: </body> 93: </html>
ここをクリックすると、動くページが別タブで開きます。
書き足した部分を、順に見ていきます。
14: // キャノン砲の画像読み込み 15: const cannonImage = new Image(); 16: cannonImage.src = "./cannon.png";
まず、15~16行目で、キャノン砲の画像を読み込んでいます。UFOの画像を読み込んだ時と同じですね。HTMLと同じフォルダに、cannon.pngを配置してください。
28: // キャノン砲の初期位置は、真ん中あたりとする 29: let cannonX = 375;
次。29行目で、キャノン砲のX座標を保持する変数cannonXを宣言しています。キャノン砲は最初は画面の真ん中あたりに表示しようと思います。canvasの幅は800ピクセル、キャノン砲の幅は50ピクセルなので、800の半分の400から50の半分の25を引いて、初期位置を375としました。キャノン砲は上下には動かないので、Y座標を保持する変数は要りません。
31: // 左右のカーソルキーが押されているかどうかを保持するフラグ 32: let leftKeyPressed = false; 33: let rightKeyPressed = false; 34: 35: document.onkeydown = keyDown; 36: document.onkeyup = keyUp;
その続きでは、左右のカーソルキーが押されているかどうかを保持するフラグleftKeyPressedとrightKeyPressedを宣言しています。上のリスト2と同じですね。
キーを押した時と離した時のイベントハンドラの設定も行っています(35~36行目)。これもリスト2と同じですが、ここでは、documentに対してイベントハンドラを割り当てています。documentはこのHTMLページの文章全体を示すので、ブラウザのウインドウにフォーカスが当たっていれば、キー入力を受け付けることができます。
76: function keyDown(e) { 77: if (e.code === "ArrowLeft") { 78: leftKeyPressed = true; 79: } else if (e.code === "ArrowRight") { 80: rightKeyPressed = true; 81: } 82: } 83: 84: function keyUp(e) { 85: if (e.code === "ArrowLeft") { 86: leftKeyPressed = false; 87: } else if (e.code === "ArrowRight") { 88: rightKeyPressed = false; 89: } 90: }
ちょっと飛びますが、キーが押されたときのイベントハンドラkeyUp()、離されたときのイベントハンドラkeyDown()はこう。これもリスト2と同じです。
45: // キャノン砲を描く 46: context.drawImage(cannonImage, cannonX, 570);
46行目まで戻ります。キャノン砲を描画しているのが46行目。X座標は上で宣言したcannonXで、Y座標は固定値で570です。canvasの高さが600で、キャノン砲の高さが25なので、下に5ピクセルくらい隙間を開ける感じで570としました。
66: // キャノン砲を動かす 67: if (leftKeyPressed && cannonX > 0) { 68: cannonX -= 10; 69: } else if (rightKeyPressed && cannonX < 750) { 70: cannonX += 10; 71: }
67~71行目でキャノン砲を動かしています。フラグleftKeyPressedがtrueであり、かつ、cannonXが0より大きければ、cannonXから10引いています。こちらの演算子一覧のNo.18に挙げたように、&&は「かつ」を意味する演算子です。そして、「cannonXが0より大きければ」という条件のおかげで、キャノン砲が画面をはみ出して見えなくなってしまうことを防いでいます。
ただし、68行目ではcannonXから10引いていますから、キャノン砲もUFOと同様一度に10ピクセル動きます。そして、キャノン砲の初期位置は375としましたから、そこからcannonXを10ずつ減らして左端に近づいていくと、左端にぶつかる直前にはcannonXは5となり、5は0より大きいので68行目が実行されて、cannonXは-5になります。つまり、このプログラムではキャノン砲はちょっとだけ左端を突き抜けます。
これがよいかどうかはプログラム次第でしょう。こういったシューティングゲームでは、むしろキャノン砲の中央、ビームの出るところが画面の左端まで行けるのが正しいようにも思います(そうでないと、画面の左端の敵が狙えないので)。今回はこれでよしとしました。
69~71行目では、同様に右への移動を行っています。ここで、「cannonX < 750」という条件の750という値は、canvasの幅800からキャノン砲の幅50を引いたものです。
――こんなふうに375だの570だの750だのの数値をプログラム内に直に書いてしまうことは、あまりよいこととはされません。まず他人が読んだときに意味が分からないですし(一月後の自分なんて他人と同じです)、「canvasの幅を900に変えよう」となったら全部計算しなおす必要があるからです。そのあたりは、この入門が続いたらこの先で直します。
キャノン砲が動いたので、残りのやること一覧は以下です。次はビームを発射します。
UFOやキャノン砲は、それぞれペイントで絵を描いて.pngファイルを作りましたが、ビームはまあ、ただの黄色い線なので、ここでやったように、単に線を引くことにしましょう。この線の長さは20ピクセルです。太さは1ピクセルなのですが、X座標を整数にして線を引いているので、ここに描いた理由により、2ピクセル幅の、ちょっと暗い黄色に見えます。まあ、気にしないことにしましょう。
ここまで覚えた知識があれば、ビームを発射するのに、特に新たなことを覚える必要はありません。そこで、ビームを発射できるバージョンのプログラムをいきなり掲載します。
1: <!DOCTYPE html> 2: <html lang="ja"> 3: <head> 4: <meta charset="UTF-8"> 5: <title>ビームを発射する</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: // キャノン砲の画像読み込み 15: const cannonImage = new Image(); 16: cannonImage.src = "./cannon.png"; 17: 18: const canvas = document.getElementById("canvas"); 19: const context = canvas.getContext("2d"); 20: 21: // UFOの初期位置は、とりあえず左上とする 22: let ufoX = 0; 23: let ufoY = 0; 24: // 「目標点」の初期位置も、左上とする 25: let ufoTargetX = 0; 26: let ufoTargetY = 0; 27: 28: // キャノン砲の初期位置は、真ん中あたりとする 29: let cannonX = 375; 30: 31: // 左右のカーソルキーが押されているかどうかを保持するフラグ 32: let leftKeyPressed = false; 33: let rightKeyPressed = false; 34: 35: // ビームが現在発射中であることを示すフラグ 36: let beamFlag = false; 37: // ビームのX, Y座標 38: let beamX; 39: let beamY; 40: 41: document.onkeydown = keyDown; 42: document.onkeyup = keyUp; 43: 44: mainLoop(); 45: 46: function mainLoop() { 47: // 画面をクリアする 48: context.clearRect(0, 0, 800, 600); 49: // UFOを描く 50: context.drawImage(ufoImage, ufoX, ufoY); 51: // キャノン砲を描く 52: context.drawImage(cannonImage, cannonX, 570); 53: // (発射中なら)ビームを描く 54: if (beamFlag) { 55: context.strokeStyle = "yellow"; 56: context.beginPath(); 57: context.moveTo(beamX, beamY); 58: context.lineTo(beamX, beamY + 20); 59: context.stroke(); 60: } 61: 62: // UFOを動かす 63: if (ufoX === ufoTargetX && ufoY === ufoTargetY) { 64: // UFOの位置と目標点が一致しているので、新たな目標点を設定 65: ufoTargetX = Math.floor(Math.random() * 73) * 10; 66: ufoTargetY = Math.floor(Math.random() * 51) * 10; 67: } 68: // UFOを目標点に近づける 69: if (ufoX < ufoTargetX) { 70: ufoX += 10; 71: } else if (ufoX > ufoTargetX) { 72: ufoX -= 10; 73: } 74: if (ufoY < ufoTargetY) { 75: ufoY += 10; 76: } else if (ufoY > ufoTargetY) { 77: ufoY -= 10; 78: } 79: 80: // キャノン砲を動かす 81: if (leftKeyPressed && cannonX > 0) { 82: cannonX -= 10; 83: } else if (rightKeyPressed && cannonX < 750) { 84: cannonX += 10; 85: } 86: // (発射中なら)ビームを動かす 87: if (beamFlag) { 88: if (beamY > 0) { 89: beamY -= 10; 90: } else { 91: beamFlag = false; 92: } 93: } 94: 95: setTimeout(mainLoop, 20); 96: } 97: 98: function keyDown(e) { 99: if (e.code === "ArrowLeft") { 100: leftKeyPressed = true; 101: } else if (e.code === "ArrowRight") { 102: rightKeyPressed = true; 103: } else if (e.code === "Space" && !beamFlag) { 104: beamFlag = true; 105: beamX = cannonX + 25; 106: beamY = 550; 107: } 108: } 109: 110: function keyUp(e) { 111: if (e.code === "ArrowLeft") { 112: leftKeyPressed = false; 113: } else if (e.code === "ArrowRight") { 114: rightKeyPressed = false; 115: } 116: } 117: </script> 118: </body> 119: </html>
ここをクリックすると、動くページが別タブで開きます。
また、書き足した部分を、順に見ていきます。
35: // ビームが現在発射中であることを示すフラグ 36: let beamFlag = false; 37: // ビームのX, Y座標 38: let beamX; 39: let beamY;
まずは36~39行目から。36行目でbeamFlagというフラグを宣言しています。これは、「ビームが現在発射中」であればtrueになるフラグです。ビームは常に存在しているわけではない(ゲームのプレイヤーがビームを撃たなければ、表示する必要も、動かす必要もない)ので、このようなフラグを用意しています。
38, 39行目で、ビームのX座標、Y座標を保持する変数を宣言しています。UFOのX, Y座標をufoX, ufoYで保持したように、また、キャノン砲のX座標をcannonXで保持したように、ビームのX, Y座標を変数beamX, beamYで保持しています。ビームは縦方向にいくらかの長さ(ここでは20ピクセルです)がありますが、beamYは、ビームの上端を保持することにします。ビームの位置を保持する変数が、X, Yの一組しかないので、このプログラムでは、ビームの座標をひとつしか保持できません。このUFOゲームでビームが連射できない(先に撃ったビームが消えるまで次のビームが撃てない)のはこれが理由です。
そして、スペースキーが押されたら、ビームを発射します。
98: function keyDown(e) { 99: if (e.code === "ArrowLeft") { 100: leftKeyPressed = true; 101: } else if (e.code === "ArrowRight") { 102: rightKeyPressed = true; 103: } else if (e.code === "Space" && !beamFlag) { 104: beamFlag = true; 105: beamX = cannonX + 25; 106: beamY = 550; 107: } 108: }
keyDown()はもともとあった関数ですが、ここにスペースキーを押した時の処理を追加しました(赤字部分)。スペースキーが押されたとき、beamFlagがfalse、つまりビームが発射されていなければ、beamFlagをtrueにしたうえで、ビームのX座標、Y座標を設定します。キャノン砲の(左端の)X座標がcannonXで、キャノン砲の幅が50ピクセルなので、cannonXに50の半分の25を足せば、キャノン砲の真ん中あたりからビームが発射されることになります。また、キャノン砲のY座標は上端が570なので(リスト4の52行目参照)、ビームのY座標は、570からビームの長さの20を引いて、550としています(図3)。
こうしてbeamFlagがtrueになれば、次のmainLoop()の呼び出しからは、ビームが表示されます。
53: // (発射中なら)ビームを描く 54: if (beamFlag) { 55: context.strokeStyle = "yellow"; 56: context.beginPath(); 57: context.moveTo(beamX, beamY); 58: context.lineTo(beamX, beamY + 20); 59: context.stroke(); 60: }
ビームを描画しているのが、この場所です。54行目でbeamFlagをチェックし、ビームが発射中なら、黄色でビームを描いています。
86: // (発射中なら)ビームを動かす 87: if (beamFlag) { 88: if (beamY > 0) { 89: beamY -= 10; 90: } else { 91: beamFlag = false; 92: } 93: }
そして、ビームが発射中なら、ビームを10ドットずつ上に動かします。もし画面の上端に達したら、beamFlagをfalseにします。これによりビームは消えて、再度発射可能になます。
これでビームが発射できるようになりました。でも、まだ、ビームがUFOに当たっても何も起きません。そういう処理を作っていないのだから当たり前です。それが、やること一覧で残っている、最後のひとつです。
次のページで、その部分を作ります。
公開日: 2021/06/20
前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る