自機の移動、ミサイル発射、ボムの制御編
『弾幕インベーダー』の解説シリーズの2回目は自機の移動とミサイルの発射、ボムの表示について解説していきたいと思います。
『弾幕インベーダーゲーム』で遊びたい方は以下よりアクセスしてください。
ソースコードはGitHub上にアップしています。
それぞれの制御についてコードを中心に見ていきましょう。
自機制御の定義一覧
自機の制御に関する定義をまとめておきます。
自機制御に使用するグローバル変数一覧
変数名 | 内容 | 変数名 | 内容 |
---|---|---|---|
x | 自機のx座標 | keyr | 右移動キー押下状態 |
keyl | 左移動キー押下状態 | shot | 自機のミサイル数 |
power | パワーアップアイテム数 | shotwait | 次のミサイル発射までの待ち時間 |
bomb | ボムアイテム数 | bombwait | ボム効果用のカウンタ |
zanki | 残機数 | extend | ボーナス得点(1機アップ)のカウンタ |
miss | 被弾状態 | invincible | 無敵状態 |
jtama | 自機のミサイルの管理用配列 | shield | シールド状態 |
自機のミサイル用のクラス定義(Jtamaクラス)
自機のミサイルはパワーアップアイテムの数によって、連射できる数が変わり、最大4連射まで可能です。複数のミサイルを管理することになりますので、クラス定義しています。今回のクラスはメソッドは用意せず、フィールドだけを持つ簡単なクラスにしています。
フィールド名 | 型 | 場所 | 内容 |
---|---|---|---|
image | string | static | ミサイルの画像データのファイルパス |
x | number | インスタンス | ミサイルのX座標 |
y | number | インスタンス | ミサイルのY座標 |
alive | boolean | インスタンス | ミサイルが発射されているか否か |
フィールド
JavaScriptのフィールドは場所(Location)や可視性(Visibility)の指定によって振る舞いが変わります。staticで修飾するとクラス共通で持つ情報となり、インスタンスにする(何も修飾しない)とインスタンス(オブジェクト)ごとに持つ情報となります。
場所 | static | インスタンス |
可視性 | public | praivete |
staticフィールドはクラス共通の情報になりますので、staticフィールドにアクセスする場合は「クラス名.フィールド名」と記載します。インスタンスフィールドは「インスタンス名.フィールド名」とアクセスします。
クラスを設計する場合、通常はフィールドとフィールドを操作するメソッドの両方を実装します。データ(フィールド)と操作(メソッド)を1つのクラスに閉じ込めることで、1つのオブジェクトとして扱うことができ、修正が入ったとしても他のプログラムへの影響範囲を小さくすることができるからです。これをカプセル化といいます。
自機制御の関数一覧
自機を制御を主としている関数一覧は以下となります。
クラス名 | 内容 |
---|---|
draw() | 自機の表示 |
shotmove() | ミサイル制御 |
nextstage() | 次ステージ処理 |
hit() | 被弾処理 |
respone() | リスポーン処理 |
bombeffect() | ボム表示 |
game() | メイン関数 |
keyDownHandler() | キー押下処理 |
keyUpHandler() | キー開放処理 |
自機のキー操作(keyDownHandler/keyUpHandler関数)
自機の操作は以下の通りです。
- 左右矢印キーまたはA、Dキーでの左右への移動
- スペースキーによるミサイル発射
- XまたはBキーによるボム使用
キー操作はEventTarget インターフェイスのメソッドを使用して、コールバックされる関数keyDownHandler()関数とkeyUpHandler()関数を指定しています。
キーが押されたときのイベントハンドラ登録
document.addEventListener("keydown", keyDownHandler, false);
キーが離されたときのイベントハンドラ登録
document.addEventListener("keyup", keyUpHandler, false);
それぞれのハンドラの処理は以下の通りです。
keyDownHandler
function keyDownHandler(e) {
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でないとき
//右矢印キーまたはDキー押下
if (e.key === 'ArrowRight' || e.key === 'd') {
keyr = true;
}
//左矢印キーまたはAキー押下
if (e.key === 'ArrowLeft' || e.key === 'a') {
keyl = true;
}
//スペースキー押下またはZキー押下、かつショットの数がパワーアップアイテム数以下、かつショット待ち中でないとき
if ((e.key === ' ' || e.key === 'z') && shot <= power && shotwait > 20) {
for (let i = 0; i < jtama.length; i++) {
if (!jtama[i].alive) { //自機のミサイルが未発射のとき
jtama[i].alive = true; //自機のミサイルを発射状態にセット
jtama[i].x = x + 20; //自機の中心のx座標をセット
jtama[i].y = 450; //y座標をセット
shotwait = 0; //ショット待ちカウンタをクリア(カウントスタート)
shot++; //ショット数を1つ増やす
break;
}
}
}
//Xキー押下またはBキー押下時、かつ、ボムのアイテム数が1以上、かつボム待ち中でないとき
if ((e.key === 'x' || e.key === 'b') && bomb >= 1 && bombwait > 250) {
bombwait = 0; //ボム待ちカウンタをクリア
bomb--; //ボムのアイテム数を1つ減らす
tamareset(); //敵のミサイルをすべて無効化
}
}else{ //自機が被弾中、またはクリア状態
keyr = false; //右キー押下状態をクリア
keyl = false; //左キー押下状態をクリア
}
if (miss) { //自機が被弾したとき
respone(); //自機の復活処理
} else if (clear) { //クリアしたとき
nextstage(); //次のステージにセット
}
}
keyUpHandler
function keyUpHandler(e) {
//右矢印キーまたはDキー押下
if (e.key === 'ArrowRight' || e.key === 'd') {
keyr = false; //右キー押下状態をクリア
}
//左矢印キーまたはAキー押下
if (e.key === 'ArrowLeft' || e.key === 'a') {
keyl = false; //左キー押下状態をクリア
}
}
自機の移動処理
自機の移動はメイン関数であるgame()関数内で行っています。上述したキー操作時のイベントハンドラ内で移動用のキーが押されると、フラグがセットされるため、そのフラグによって自機を移動しています。自機の移動部分を抜粋した処理を掲載しておきます。
game()関数内の自機移動のコード
function game() {
~中略~
if(!miss&&!clear){ //自機が被弾状態でない、かつ、クリア状態でないとき
if (keyl) { //左移動キー押下時
x = x - 2; //x座標を2pxずつ減らす
if (x < 0) { //左端に到達したとき
x = 0; //x座標に常に0をセット
}
}
if (keyr) { //右移動キー押下時
x = x + 2; //x座標を2pxずつ増やす
if (x > 760) { //右端に到達したとき
x = 760; //x座標に常に760をセット
}
}
}
~中略~
}
自機が被弾したときやクリアしたときは、ゲームを一旦停止していますので、その場合は移動しないように被弾時やクリア時の条件を見ています。
また自機が画面の右端や左端まで移動した場合もそれ以上移動してしまうと自機が画面外に行ってしまいますので、自機が右端や左端まで来ている場合は、それ以上移動しないようにX座標も監視しています。
自機のX座標は自機の左端にしています。よって自機が画面の左端まで来ているかは自機のX座標が「0」より小さくなったかで判定しています。一方、自機が右端まで行ったかを判定しているのは自機のX座標が「760」より大きくなったかで行っています。ゲーム画面の右端は「800px」なのですが、なぜ「760」で判定しているかと言うと、上述したように自機のX座標は自機の左側です。自機の画像は縦横40pxですので、もし右端まで行ったかの判定を「x > 800」としてしまうと、自機が画面外に出てしまいます。よって自機が画面外まで出ないように自機の画像の幅の40px分を調整しています。
自機の表示処理(draw関数)
自機の表示はdraw()関数で行っています。draw()関数では自機の表示以外にもゲーム画面の枠の表示や残機、アイテム、スコア、ステージ数などの表示も行っています。今回は自機の表示処理についてのコードを抜粋して掲載しておきます。
draw()関数内の自機表示のコード
function draw() {
//自機の表示
let jikiimg = new Image();
if (!miss&&!shield) { //ミス状態でない、かつシールド状態でない(通常状態のとき)
jikiimg.src = "res/jiki_32x24.png";
if (invincible>0&&invincible%2==1){ //無敵タイマ起動中、かつ、カウントが奇数のとき
}else{
ctx.globalAlpha = 1;
ctx.drawImage(jikiimg, x, 450, 40, 40);
}
}else if (!miss&&shield) { //ミス状態でない、かつシールド中
jikiimg.src = "res/jikib_32x24.png";
ctx.globalAlpha = 1;
ctx.drawImage(jikiimg, x, 450, 40, 40);
} else { //ミスしたとき
jikiimg.src = "res/jiki_miss_32x24.png";
ctx.globalAlpha = 0.4;
ctx.globalAlpha = 1;
ctx.drawImage(jikiimg, x - 10, 450 - 10, 60, 60);
}
~中略~
}
自機の画像は「通常時」「シールド中」「ミス時」の3種類を用意しています。ミスしたあとで復活したときには一定時間無敵状態になりますが、無敵期間中はブリンク(点滅)表示にしています。各状態は変数で持ち、変数の内容によってどの画像を表示するかを選んでいるのが上記のコードの内容になっています。
自機の被弾処理(hit関数)
自機がインベーダーが発射してくるミサイルに当たると被弾処理を呼び出しています。被弾してしまうと自機が爆発した画像を表示し、一旦ゲームを止めます。その後、何かのキーが押されると自機がリスポーンされ、ゲームが再開します。被弾して残機がなくなればゲームオーバーとなります。またシールドしているときに被弾するとシールドが解除されます。自機が被弾して爆発したり、シールドが壊されたあとは一定時間、無敵状態にもしています。hit()関数では通常時に被弾した場合は、被弾状態を持つmissフラグをtrueにセット、シールド中の被弾であればシールド状態を持つshieldにfalseをセットし、一定時間無敵にするためにinvincibleカウンタを300に初期化しています。
自機の被弾処理のコード
function hit(){
if (!miss && !shield && invincible <= 0) { //被弾状態でない、かつ、シールド中でない、かつ、無敵状態でないとき
miss = true; //被弾状態にセット
} else if (shield) { //シールド中のとき
shield = false; //シールド状態解除
}
invincible = 300; //無敵期間をセット
}
自機のミサイル発射処理(shotmove関数)
自機はミサイルでインベーダーを攻撃します。その処理はshotmove()関数で行っています。自機が発射したミサイルはインベーダーに当たるか、画面の上まで行って消えるまで、次のミサイルを発射できなくしています。通常は1度に1回しかミサイルを発射出来ませんが、パワーアップアイテムを取ると同時に発射出来るミサイルの数を増やすことができます。
自機のミサイル処理のコード
function shotmove() {
tamaimg = new Image();
tamaimg.src = Jtama.image;
for (h = 0; h < jtama.length; h++){
if(jtama[h].alive){
//ミサイルの移動と表示
if(!miss && !clear){
jtama[h].y--;
}
ctx.drawImage(tamaimg, jtama[h].x, jtama[h].y, 1, 10);
//ミサイルと敵機との当たり判定
for (i = 0; i < teki.length; i++){
for (j = 0; j < teki[i].length; j++){
if(teki[i][j].alive){ //生きている敵がいたら
if (jtama[h].y <= i*50+74 && jtama[h].y > i*50+40){ //ミサイルの先頭の位置が敵の高さの範囲に入っているとき
if (jtama[h].x >= teki[i][j].x+tx && jtama[h].x <= teki[i][j].x+tx+32){ //ミサイルのx座標が敵の幅の範囲に入っているとき
teki[i][j].alive = false; //敵を倒した状態にセットする
score = score+(5-i)*100; //スコアを加算(上段の敵ほど得点が高い)
if (score >= (extend+1)*10000){ // //1万点ごとに自機を増やす
zanki++;
extend++;
}
jtama[h].alive = false; //ミサイルを消す
shot--; //ミサイルの数を1つ減らす
tcount--; //敵のカウント数を1つ減らす
dropchance(i, j); //ドロップアイテムの抽選
if (tcount == 0){ //敵の数がゼロになったとき
clear = true; //クリア状態にセット
}
}
}
}
}
}
if (jtama[h].y<0){ //ミサイルのy座標が上限を超えた(弾が敵に当たらなかった)
shot--; //ミサイルの数を1つ減らす
jtama[h].alive=false; //ミサイルを消す
}
}
}
}
自機のミサイルは一次元配列で管理しています。ミサイルがインベーダーに命中したかどうかはすべてのインベーダーに対して判定する必要があります。インベーダーは5行10列の二次元配列で管理しています。すべての自機のミサイルとすべてのインベーダーを総当たりで見ていく必要がありますので、三重ループで回しています。
自機のミサイルはパワーアップアイテム数によって複数持つことが出来るため配列で管理しています。ミサイルのインスタンスはプログラムの起動時に配列にセットしています。ミサイルが画面上に存在するかどうかは自機のミサイルクラスのaliveフィールドで管理しています。aliveがtrueであれば、画面上に存在します。ミサイルがインベーダーに命中するか、命中せずに画面外に出た時に無効(false)にしています。
自機のボム使用時の表示
ボムアイテムをゲットするとボムを発動することができます。ボムは画面上にあるインベーダーのすべてのミサイルを一瞬で無効化出来る強力なアイテムです。ボムを使用すると自機を中心とした同心円状に爆発する描画を行っています。その処理はbombeffect()関数で描画しています。
ボム使用時の描画のコード
function bombeffect() {
ctx.beginPath();
ctx.arc(x+20, 450, 40*bombwait, 0, Math.PI*2, false);
ctx.fillStyle = "rgba(100,200,255,0.5)";
ctx.fill();
ctx.closePath();
}
function game() {
~中略~
if (bombwait<20){
bombeffect();
}
~中略~
bombwait++;
~中略~
}
ボム使用時の描画はarc()メソッドで行っています。中心のX座標は「x+20」、Y座標は「450」とし、自機の画像の中心となるように設定しています。半径を徐々に増やしていくことで、爆風を表現しています。
さいごに
今回は『弾幕インベーダー』の自機の操作や表示、ミサイル制御についてのコードを見てきました。インベーダーゲームの自機は左右にしか動かず、ミサイルも前方に発射するだけですので自機制御はそれほど難しくはありません。とはいえ、すべてのインベーダーに対してミサイルの当たり判定を行ったり、自機が移動する範囲を監視したり等、いろいろと考慮することが多いですので、難易度は中くらいでしょうか。他のシューティングゲームにも応用できるところがたくさんあると思いますので、是非、シューティングゲームの制作にチャレンジしてみてはいかがでしょうか。次回以降もお楽しみに!
ちょっと息抜き
プログラミングを学習していると、脳をフル稼働させますし、じっと座ってコーディングしているときは目も身体も疲れてしまいます。そんな時は一息入れて、Yahoo!トラベルを見て現実逃避しましょう♪
Yahoo!トラベルはお得が一杯
- 取り扱い施設数が約17000施設!
- リーズナブルな価格での宿泊施設から贅沢な宿泊施設まで掲載!
- 日程や希望の条件からプランの検索・比較が簡単!
- 期間限定のクーポンがいっぱい!
- PayPayポイントが使える・貯まる!
- エリア×テーマで最も売れている宿をデイリーで更新中!
具体的に旅行を考えている方はもちろん、旅行の計画がなくてもYahoo!トラベルを見ているだけでほんのちょっと旅行に行った気分を味わえます。良い気分転嫁が出来ると思いますので興味がある方はアクセスしてみてください。
自機の移動に関してはkeyDownHandlerの処理の中でキーが押された状態をフラグにセットし、keyUpHandlerの処理の中でそのフラグをクリアしています。実際の移動はメイン関数であるgame()関数の中で処理しています。