インベーダーの残りのミサイル編
『弾幕インベーダー』のリファクタリング解説シリーズの第四段です。前回はインベーダーの3Wayミサイルについてリファクタリングしました。インベーダーの攻撃は3Wayミサイル以外にも誘導弾ミサイル、ビーム、弾幕ミサイルがありますが、どのミサイルもほぼ同じ手順でリファクタリングしていきますので、今回はそれらのミサイルについて一気に作業を進めていきたいと思います。
『弾幕インベーダーゲーム』で遊びたい方は以下よりアクセスしてください。
ソースコードはGitHub上にアップしています。
誘導弾ミサイル、ビーム、弾幕ミサイルクラスの見直し
前回の3Wayミサイルのときと同様に関連する処理の洗い出しを行いますが、他のミサイルも3Wayミサイルと同じ作りにしていますので、洗い出しは簡単で、リファクタリングの方針も全く同じですので、ほぼ機械的な作業になりますね。
誘導弾ミサイル、ビーム、弾幕ミサイルに関連する関数の洗い出し
3Wayミサイルのときに洗い出した箇所とリファクタリングの方針もそのまま流用できます。
関連する関数 | 内容 | リファクタリングの方針 |
---|---|---|
shotchance() | ミサイル発射の抽選と発射の準備を行う | 各ミサイルの対象となるインベーダーの処理において、ミサイルの抽選やfor文で配列を走査している処理は残し、for文の中の個別のオブジェクトの処理をクラスのメソッドに移す。 |
tamamovea() | ミサイルの描画と移動および当たり判定 | for文で配列を走査している処理は残し、for文の中の個別のオブジェクトの処理をクラスのメソッドに移す。 |
respone() | ドロップアイテムオブジェクトの一括無効化 | aliveフィールドの更新になるため、更新用のアクセサを用意し、それを呼び出す形に修正する |
誘導弾ミサイル、ビーム、弾幕ミサイル関連のリファクタリング
今回のリファクタリングは3Wayミサイルと全く同じ内容となりますので、特に解説したいポイントやコメントはありませんので、リファクタリング前後のコードだけ掲載しておきます。
誘導弾ミサイルクラスのリファクタリング
class BombB {
constructor(_alive, _image) {
this.image = _image;
this.alive = _alive;
this.x = 0;
this.y = 0;
this.mx = 0;
this.my = 0;
}
}
ビームクラスのリファクタリング
class BombC {
constructor(_alive, _image1, _image2) {
this.imagea = _image1;
this.imageb = _image2;
this.alive = _alive;
this.x = 0;
this.chage = 0;
this.limit = 0;
}
}
弾幕ミサイルクラスのリファクタリング
class BombD {
constructor(_alive, _image) {
this.image = _image;
this.alive = _alive;
this.x = 0;
this.y = 0;
this.mx = 0;
this.my = 0;
this.ra = 0;
this.ra_now = 0;
this.range = 0;
this.range_mv = 0;
}
}
インスタンス生成処理のリファクタリング
//インベーダーの誘導弾ミサイルのインスタンス生成
for (let j = 0; j <= 5; j++) {
tama[1][j] = new BombB(false, "res/tamat_16x12.png");
}
//インベーダーのビームのインスタンス生成
for (let j = 0; j <= 3; j++) {
tama[2][j] = new BombC(false, "res/tamaL4_32x24.png", "res/tamaL5_32x24.png");
}
//インベーダーの弾幕ミサイルのインスタンス生成
for (let j = 0; j <= 4; j++) {
tama[3][j] = new BombD(false, "res/tamah_16x12.png");
}
shotchance()のリファクタリング
function shotchance(_i, _j){
let ratio;
switch (_i) {
~中略~
//上から3段目のインベーダーのとき
case 2:
ratio = Math.random()*800000; //抽選
if (ratio < stage*10 + 500 - tcount*10) { //ステージ数と生き残っているインベーダーの数で抽選
for (let k = 0; k < tama[1].length; k++) {
if (!tama[1][k].alive) { //ミサイルが未発射のとき
tama[1][k].alive = true; //ミサイルを発射状態にセット
tama[1][k].x = teki[_i][_j].x + 12 + tx; //ミサイルのx座標を対象となるインベーダーのx座標にセット
tama[1][k].y = teki[_i][_j].y + 16; //ミサイルのy座標を対象となるインベーダーのy座標にセット
break;
}
}
}
break;
//上から2段目のインベーダーのとき
case 1:
ratio = Math.random()*200000; //抽選
if(ratio < stage*10 + 500 - tcount*10) { //ステージ数と生き残っているインベーダーの数で抽選
for(k = 0; k < tama[2].length; k++) {
let beemnow = false; //ビーム発射中でない状態をセット
if (!tama[2][k].alive) { //ミサイルが未発射のとき
tama[2][k].alive = true; //ビーム発射状態にセット
tama[2][k].x = _j; //ビームを発射するx座標をセット
tama[2][k].chage = 0; //チャージ中をクリア
tama[2][k].limit = 0; //リミットをクリア
break;
}
}
}
break;
//最上段のインベーダーのとき
case 0:
ratio = Math.random()*8000000; //抽選
if(ratio < stage*10 + 5000 - tcount*100) { //ステージ数と生き残っているインベーダーの数で抽選
for(let k = 0; k < tama[3].length; k++) {
if (!tama[3][k].alive) { //ミサイルが未発射のとき
tama[3][k].alive = true; //ミサイル発射状態にセット
tama[3][k].x = teki[_i][_j].x + 12 + tx; //ミサイルのx座標を対象となるインベーダーのx座標にセット
tama[3][k].y = teki[_i][_j].y + 16; //ミサイルのy座標を対象となるインベーダーのy座標にセット
tama[3][k].ra = Math.random()*0.02-0.04; //回転速度をセット
tama[3][k].ra_now = 0;
tama[3][k].range_mv = 0.4 - Math.random()*0.2; //中心からの移動量をセット
tama[3][k].range = -20; //円の半径の初期値をセット
let x_renge = x + 20-tama[3][k].x; //自機と弾幕とのx座標の距離を取得
let y_renge = 470 - tama[3][k].y; //自機と弾幕とのy座標の距離を取得
let angle = Math.atan2(y_renge, x_renge); //自機と弾幕とのラジアン角の取得
tama[3][k].mx = Math.cos(angle)*1; //弾幕のx座標の移動量をセット
tama[3][k].my = Math.sin(angle)*1; //弾幕のy座標の移動量をセット
break;
}
}
}
break;
}
}
tamamoveb()のリファクタリング
function tamamoveb(){
for (let k = 0; k < tama[1].length; k++) {
if (tama[1][k].alive) { //誘導弾が有効のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
if (tama[1][k].y < 400) { //誘導弾のy座標が400pxより小さいとき
let x_range = x + 20 - tama[1][k].x; //自機のx座標との距離を取得
let y_range = 470 - tama[1][k].y; //自機のy座標との距離を取得
let angle = Math.atan2(y_range, x_range); //自機と誘導弾とのラジアン角の取得
tama[1][k].mx = Math.cos(angle)*2; //自機に向かう角度でx座標の移動量をセット
tama[1][k].my = Math.sin(angle)*2; //自機に向かう角度でy座標の移動量をセット
}
//誘導弾の移動
tama[1][k].y = tama[1][k].y + tama[1][k].my;
tama[1][k].x = tama[1][k].x + tama[1][k].mx;
}
//誘導弾の描画
tamaimg = new Image();
tamaimg.src = tama[1][k].image;
ctx.drawImage(tamaimg, tama[1][k].x, tama[1][k].y, 12, 12);
//誘導弾の当たり判定
if (tama[1][k].y >= 460 && tama[1][k].y <= 474) {
if (tama[1][k].x >= x+14 && tama[1][k].x <= x+20) {
hit(); //被弾処理
}
}
//誘導弾が流れていったときの処理
if (tama[1][k].y > 500) { //誘導弾のy座標が500pxを超えたとき
tama[1][k].alive = false; //誘導弾を無効にする
}
}
}
}
tamamovec()のリファクタリング
function tamamovec(){
let xx = 0;
let yy = 0;
let body = 0;
for (let k = 0; k < tama[2].length; k++) {
if (tama[2][k].alive) { //ビーム有効のとき
body = tama[2][k].x; //ビームのx座標を取得
xx = teki[1][body].x + tx; //ビームを発射するインベーダーのx座標をセット
yy = teki[1][body].y; //ビームを発射するインベーダーのy座標をセット
if (!teki[1][body].alive) { //インベーダーが倒されたとき
tama[2][k].alive = false; //ビームの発射を止める
}
if (tama[2][k].chage < 40) { //ビームのチャージ中のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
tama[2][k].chage = tama[2][k].chage + 0.05 * stage; //ステージ数に応じたチャージを行う
}
//チャージ量に応じて大きさを変えて描画
tamaaimg = new Image();
tamaaimg.src = tama[2][k].imagea;
ctx.drawImage(tamaaimg, xx+16-tama[2][k].chage/2, yy+28-tama[2][k].chage/2, tama[2][k].chage, tama[2][k].chage);
} else if (tama[2][k].limit < 120 + stage * 20) { //制限時間を超えていないとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
tama[2][k].limit++; //制限時間をカウントアップ
}
//ビームの描画
tamabimg = new Image();
tamabimg.src = tama[2][k].imageb;
ctx.drawImage(tamabimg, xx, yy+28, 24, 500);
if (xx >= x-4 && xx <= x + 20) { //自機に当たったとき
hit(); //被弾処理
}
} else {
tama[2][k].alive = false; //ビームを無効にする
}
}
}
}
tamamoved()のリファクタリング
function tamamoved(){
let rg = Math.PI*0.2;
let xx = 0;
let yy = 0;
for (let k = 0; k < tama[3].length; k++) {
if (tama[3][k].alive) { //弾幕が有効のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
tama[3][k].x = tama[3][k].x + tama[3][k].mx; //弾幕のx座標の更新(自機に向かう角度)
tama[3][k].y = tama[3][k].y + tama[3][k].my; //弾幕のy座標の更新(自機に向かう角度)
tama[3][k].ra_now = tama[3][k].ra_now + tama[3][k].ra; //-0.02~-0.04のいずれかの値で毎回更新(反時計回りにするための移動量)
tama[3][k].range = tama[3][k].range + tama[3][k].range_mv; // //-0.2~-0.4のいずれかの値で更新(円の中心からの半径に相当)
}
//弾幕の画像をセット
tamaimg = new Image();
tamaimg.src = tama[3][k].image;
//スター表示
for (let i = 1; i <= 10; i++) {
if (i%2 == 1) { //奇数番号のとき
//内側の弾幕の座標をセット
xx = Math.cos(tama[3][k].ra_now+rg*i) * tama[3][k].range / 2;
yy = Math.sin(tama[3][k].ra_now+rg*i) * tama[3][k].range / 2;
} else { //偶数番号のとき
//外側の弾幕の座標をセット
xx = Math.cos(tama[3][k].ra_now+rg*i) * tama[3][k].range;
yy = Math.sin(tama[3][k].ra_now+rg*i) * tama[3][k].range;
}
ctx.drawImage(tamaimg, tama[3][k].x+xx, tama[3][k].y+yy, 8, 8);
//自機との当たり判定
if (tama[3][k].y+yy >= 460 && tama[3][k].y + yy <= 474) {
if (tama[3][k].x + xx >= x + 14 && tama[3][k].x + xx <= x + 20) {
hit(); //被弾処理
}
}
}
//弾幕が流れていったときの処理
if (tama[3][k].y > 1000) { //誘導弾のy座標が1000pxを超えたとき
tama[3][k].alive = false; //弾幕を無効にする
}
}
}
}
function game() {
~中略~
tamamoveb();
tamamovec();
tamamoved();
~中略~
}
respone()のリファクタリング
function respone() {
~中略~
//インベーダーの誘導弾ミサイルの初期化
for (let j = 0; j < tama[1].length; j++) {
tama[1][j].alive = false;
}
//インベーダーのレーザービームの初期化
for (let j = 0; j < tama[2].length; j++) {
tama[2][j].alive = false;
}
//インベーダーの弾幕ミサイルの初期化
for (j = 0; j < tama[3].length; j++) {
tama[3][j].alive = false;
}
~中略~
}
全コードを掲載
ドロップアイテムクラスをリファクタリングしたコードの全文を掲載しておきます。
/*
======================================================================
Project Name : 弾幕インベーダー
Creation Date : 2024年6月
Copyright © 2024 大阪ヒューマンネット All Rights Reserved.
This source code or any portion thereof must not be
reproduced or used in any manner whatsoever.
======================================================================
*/
/**********************************************
グローバル変数宣言
***********************************************/
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var x = 0; //自機のx座標
var stage = 0; //ステージ数
var score = 0; //スコア
var keyr = false; //右移動キー押下状態クリア
var keyl = false; //左移動キー押下状態クリア
var power = 0; //パワーアップアイテム数
var bomb = 2; //ボムアイテム数
var bombwait = 50; //ボム効果用のカウンタ
var zanki = 3; //残機数
var clear = true; //クリア状態(クリア時、一旦動作を止めている)
var miss = false; //被弾状態(被弾時、一旦動作を止めている)
var gameover = false; //ゲームオーバー状態クリア
var extend = 0; //ボーナス得点(1機アップ)のカウンタ
var shield = false; //シールド状態クリア
var invincible = 0; //無敵状態
var tx = 0; //インベーダーのx座標
var tmove = 2; //インベーダーの移動量
var tcount = 50; //インベーダーの生存数
var tact = 0; //インベーダーを移動判定用カウンタ(インベーダーの数が減ってきたら移動速度を上げるため)
var teki = [[], [], [], [], []]; //インベーダーの管理用配列
var tama = [[], [], [], [], []]; //インベーダーのミサイルの管理用配列
var jtama = []; //自機のミサイルの管理用配列
var item = []; //アイテムの管理用配列
/**********************************************
イベントハンドラーの登録
***********************************************/
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
/****************************************************
クラス宣言
*****************************************************/
//自弾クラス
class SpaceshipMissile {
static image = null;
static shotwait = 0; //次のミサイル発射までの待ち時間
static shotNum = 0; //次のミサイル数
//コンストラクタ宣言
constructor(alive, x, y) {
this.x = x;
this.y = y;
this.alive = alive;
SpaceshipMissile.image = new Image();
SpaceshipMissile.image.src = "res/tamaji_16x12.png";
}
//ミサイル待ち時間の取得
static getShotwait() {
return SpaceshipMissile.shotwait;
}
//ミサイル待ち時間の更新
static countShotwait() {
SpaceshipMissile.shotwait++;
}
//ミサイルの数を減らす
static reduceShotNum() {
SpaceshipMissile.shotNum--;
}
//ミサイル数のクリア
static clearShotNum() {
SpaceshipMissile.shotNum = 0;
}
//ミサイル数の取得
static getShotNum() {
return SpaceshipMissile.shotNum;
}
//ミサイルの有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//ミサイルの状態取得
getAlive() {
return this.alive;
}
//ミサイルを発射する
fire() {
this.alive = true; //自機のミサイルを発射状態にセット
this.x = x + 20; //自機の中心のx座標をセット
this.y = 450; //y座標をセット
SpaceshipMissile.shotwait = 0; //ショット待ちカウンタをクリア(カウントスタート)
SpaceshipMissile.shotNum++; //ショット数を1つ増やす
}
//自機のミサイル処理
move() {
if(this.alive){
//ミサイルの移動と表示
if(!miss && !clear){
this.y--;
}
ctx.drawImage(SpaceshipMissile.image, this.x, this.y, 1, 10);
//ミサイルと敵機との当たり判定
for (let i = 0; i < teki.length; i++){
for (let j = 0; j < teki[i].length; j++){
if(teki[i][j].alive){ //生きている敵がいたら
if (this.y <= i*50+74 && this.y > i*50+40){ //ミサイルの先頭の位置が敵の高さの範囲に入っているとき
if (this.x >= teki[i][j].x+tx && this.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++;
}
this.alive = false; //ミサイルを消す
SpaceshipMissile.reduceShotNum(); //ミサイルの数を1つ減らす
tcount--; //敵のカウント数を1つ減らす
dropchance(i, j); //ドロップアイテムの抽選
if (tcount == 0){ //敵の数がゼロになったとき
clear = true; //クリア状態にセット
}
}
}
}
}
}
if (this.y<0){ //ミサイルのy座標が上限を超えた(弾が敵に当たらなかった)
SpaceshipMissile.reduceShotNum(); //ミサイルの数を1つ減らす
this.alive=false; //ミサイルを消す
}
}
}
}
//アイテムクラス
class Item {
//クラスフィールド宣言
static imageSrcScore = "res/item_s.png"; //スコアアップアイテムの画像
static imageSrcPower = "res/item_p.png"; //パワーアップアイテムの画像
static imageSrcBomb = "res/item_b.png"; //ボムアイテムの画像
static imageSrcShield = "res/item_sh.png"; //シールドアイテムの画像
static imageSrcHeart = "res/item_ex.png"; //1機アップアイテムの画像
//コンストラクタ宣言
constructor(alive, x, y, type) {
this.x = x;
this.y = y;
this.alive = alive;
this.type = type;
}
//有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//有効無効の取得
getAlive() {
return this.alive;
}
//ドロップアイテムをランダムに抽選する
drawRandomly(i, j){
let ratio = Math.random()*100; //アイテムの抽選
if (ratio < 40) { //抽選結果が40より小さいとき(40%の確率でアイテムを落とす)
this.alive = true; //アイテムを有効とする
this.x = teki[i][j].x + tx - 4; //アイテムを落とすインベーダーのx座標をセット
this.y = teki[i][j].y + 16; //アイテムを落とすインベーダーのy座標をセット
if (ratio < 1) { //抽選結果が1より小さいとき
this.type = 5; //1機アップアイテムをセット
} else if (ratio < 5) { //抽選結果が5より小さいとき
this.type = 4; //シールドアイテムをセット
} else if (ratio < 10) { //抽選結果が10より小さいとき
this.type = 3; //ボムアイテムをセット
} else if (ratio < 20) { //抽選結果が20より小さいとき
this.type = 2; //パワーアップアイテムをセット
} else { //抽選結果が上記以外のとき
this.type = 1; //スコアアップアイテムをセット
}
}
}
//アイテムの落下
move(){
if (this.alive) { //アイテム有効のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
this.y++; //アイテムのy座標を更新
}
//アイテム画像の描画
let image = new Image();
switch (this.type){
case 1:
image.src = Item.imageSrcScore;
break;
case 2:
image.src = Item.imageSrcPower;
break;
case 3:
image.src = Item.imageSrcBomb;
break;
case 4:
image.src = Item.imageSrcShield;
break;
case 5:
image.src = Item.imageSrcHeart;
break;
}
ctx.drawImage(image, this.x, this.y, 40, 40);
//アイテムがゲットされたときの処理
if (this.y >= 400 && this.y <= 499) { //アイテムのy座標が400px~499pxのとき
if (this.x >= x-14 && this.x <= x+40) { //アイテムのx座標が自機のx座標と重なったとき
if (this.type == 1) { //ゲットしたアイテムがスコアアップアイテムのとき
score = score + 500; //スコアに500点追加
} else if (this.type == 2) { //ゲットしたアイテムがパワーアップアイテムのとき
if (power < 5){ //アイテム数が5個より小さいとき
power++; //アイテム数を増やす
}else{ //アイテム数が5つ以上のとき
score = score + 500; //スコアに500点追加
}
} else if (this.type == 3) { //ゲットしたアイテムがボムアイテムのとき
if (bomb < 3) { //アイテム数が3個より小さいとき
bomb++; //アイテム数を増やす
}else{ //アイテム数が3つ以上のとき
score = score + 500; //スコアに500点追加
}
} else if (this.type == 4) { //ゲットしたアイテムがシールドアイテムのとき
if (!shield) { //シールド中でないとき
shield = true; //シールド状態にセット
} else { //シールド中のとき
score = score + 500; //スコアに500点追加
}
}else if (this.type == 5) { //ゲットしたアイテムが1機アップアイテムのとき
zanki++; //残機数を増やす
}
this.alive = false; //アイテムを無効にする
}
}
//アイテムが流れた時の処理
if (this.y > 500) { //アイテムのy座標が500pxを超えたとき
this.alive = false; //アイテムを無効にする
}
}
}
//アイテム表示
static drawStatus() {
let image = new Image();
image.src = Item.imageSrcBomb;
for (let i = 0; i < bomb; i++){
ctx.drawImage(image, 900 + i*100, 250, 60, 60);
}
image = new Image();
image.src = Item.imageSrcPower;
for (let i = 0; i < power; i++){
ctx.drawImage(image, 830 + i*70, 180, 60, 60);
}
}
}
//敵クラス
class Enemy {
constructor(_alive, _x, _y, _image) {
this.x = _x;
this.y = _y;
this.image = _image;
this.alive = _alive;
}
}
//3Way弾クラス
class ThreeWayMissile {
//クラスフィールド宣言
static imageCenter = "res/tama1a_16x12.png"; //真ん中のミサイルの画像
static imageLeft = "res/tama1b_16x12.png"; //左のミサイルの画像
static imageRight = "res/tama1c_16x12.png"; //右のミサイルの画像
//コンストラクタ宣言
constructor(alive) {
this.alive = alive;
this.x = 0;
this.y = 0;
this.l = 0;
}
//ミサイルの有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//ミサイルの状態取得
getAlive() {
return this.alive;
}
//ミサイル発射を行う
shot(_i, _j){
this.alive = true; //ミサイルを発射状態にセット
this.x = teki[_i][_j].x + 12 + tx; //ミサイルのx座標を対象となるインベーダーのx座標にセット
this.y = teki[_i][_j].y + 16; //ミサイルのy座標を対象となるインベーダーのy座標にセット
this.l = 0; //左右方向の移動量を0にセット
}
//移動と描画
move(){
if (this.getAlive()) { //3Way弾が有効なとき
if(!miss && !clear){ //自機が被弾状態でない、かつクリア状態でないとき
this.y = this.y+1; //3Way弾のy座標を1px増やす
this.l = this.l+0.3; //3Way弾の左右ミサイルのx座標を0.3増やす
}
//3Way弾の描画
let imgCenter = new Image();
let imgLeft = new Image();
let imgRight = new Image();
imgCenter.src = ThreeWayMissile.imageCenter;
imgLeft.src = ThreeWayMissile.imageLeft;
imgRight.src = ThreeWayMissile.imageRight;
ctx.drawImage(imgCenter, this.x, this.y, 8, 12);
ctx.drawImage(imgLeft, this.x-this.l, this.y, 8, 6);
ctx.drawImage(imgRight, this.x+this.l, this.y, 8, 6);
//3Way弾と自機の当たり判定
if(this.y >= 460 && this.y <= 474) {
if (this.x >= x+14 && this.x <= x+20) {
hit(); //被弾処理
} else if (this.x-this.l >= x+18 && this.x-this.l <= x+24) {
hit(); //被弾処理
} else if (this.x+this.l >= x+18 && this.x+this.l <= x+24) {
hit(); //被弾処理
}
}
//3Way弾が流れていったときの処理
if (this.y > 500) { //3Way弾のy座標が500pxを超えたとき
this.alive = false; //3Way弾を無効にする
}
}
}
}
//誘導弾クラス
class GuidedMissile {
//クラスフィールド宣言
static imagePath = "res/tamat_16x12.png"; //ミサイルの画像パス
//コンストラクタ宣言
constructor(alive) {
this.alive = alive;
this.x = 0;
this.y = 0;
this.mx = 0;
this.my = 0;
}
//ミサイルの有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//ミサイルの状態取得
getAlive() {
return this.alive;
}
//ミサイル発射を行う
shot(_i, _j){
this.alive = true; //ミサイルを発射状態にセット
this.x = teki[_i][_j].x + 12 + tx; //ミサイルのx座標を対象となるインベーダーのx座標にセット
this.y = teki[_i][_j].y + 16; //ミサイルのy座標を対象となるインベーダーのy座標にセット
}
//移動と描画
move(){
if (this.alive) { //誘導弾が有効のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
if (this.y < 400) { //誘導弾のy座標が400pxより小さいとき
let x_range = x + 20 - this.x; //自機のx座標との距離を取得
let y_range = 470 - this.y; //自機のy座標との距離を取得
let angle = Math.atan2(y_range, x_range); //自機と誘導弾とのラジアン角の取得
this.mx = Math.cos(angle)*2; //自機に向かう角度でx座標の移動量をセット
this.my = Math.sin(angle)*2; //自機に向かう角度でy座標の移動量をセット
}
//誘導弾の移動
this.y = this.y + this.my;
this.x = this.x + this.mx;
}
//誘導弾の描画
let image = new Image();
image.src = GuidedMissile.imagePath;
ctx.drawImage(image, this.x, this.y, 12, 12);
//誘導弾の当たり判定
if (this.y >= 460 && this.y <= 474) {
if (this.x >= x+14 && this.x <= x+20) {
hit(); //被弾処理
}
}
//誘導弾が流れていったときの処理
if (this.y > 500) { //誘導弾のy座標が500pxを超えたとき
this.alive = false; //誘導弾を無効にする
}
}
}
}
//ビームクラス
class Beam {
//クラスフィールド宣言
static imageCharge = "res/tamaL4_32x24.png"; //チャージ中の画像
static imageBeam = "res/tamaL5_32x24.png"; //ビームの画像
//コンストラクタ宣言
constructor(alive) {
this.alive = alive;
this.x = 0;
this.chage = 0;
this.limit = 0;
}
//ミサイルの有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//ミサイルの状態取得
getAlive() {
return this.alive;
}
//ミサイル発射を行う
shot(x){
this.alive = true; //ビーム発射状態にセット
this.x = x; //ビームを発射するx座標をセット
this.chage = 0; //チャージ中をクリア
this.limit = 0; //リミットをクリア
}
//移動と描画
move(){
if (this.alive) { //ビーム有効のとき
let body = this.x; //ビームのx座標を取得
let xx = teki[1][body].x + tx; //ビームを発射するインベーダーのx座標をセット
let yy = teki[1][body].y; //ビームを発射するインベーダーのy座標をセット
if (!teki[1][body].alive) { //インベーダーが倒されたとき
this.alive = false; //ビームの発射を止める
}
if (this.chage < 40) { //ビームのチャージ中のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
this.chage = this.chage + 0.05 * stage; //ステージ数に応じたチャージを行う
}
//チャージ量に応じて大きさを変えて描画
let image = new Image();
image.src = Beam.imageCharge;
ctx.drawImage(image, xx+16-this.chage/2, yy+28-this.chage/2, this.chage, this.chage);
} else if (this.limit < 120 + stage * 20) { //制限時間を超えていないとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
this.limit++; //制限時間をカウントアップ
}
//ビームの描画
let image = new Image();
image.src = Beam.imageBeam;
ctx.drawImage(image, xx, yy+28, 24, 500);
if (xx >= x-4 && xx <= x + 20) { //自機に当たったとき
hit(); //被弾処理
}
} else {
this.alive = false; //ビームを無効にする
}
}
}
}
//スター弾幕クラス
class BarrageMissile {
//クラスフィールド宣言
static imageBarrage = "res/tamah_16x12.png"; //弾幕の画像
//コンストラクタ宣言
constructor(alive) {
this.alive = alive;
this.x = 0;
this.y = 0;
this.mx = 0;
this.my = 0;
this.ra = 0;
this.ra_now = 0;
this.range = 0;
this.range_mv = 0;
}
//ミサイルの有効無効の設定
setAlive(alive) {
this.alive = alive;
}
//ミサイルの状態取得
getAlive() {
return this.alive;
}
//ミサイル発射を行う
shot(i, j){
this.alive = true; //ミサイル発射状態にセット
this.x = teki[i][j].x + 12 + tx; //ミサイルのx座標を対象となるインベーダーのx座標にセット
this.y = teki[i][j].y + 16; //ミサイルのy座標を対象となるインベーダーのy座標にセット
this.ra = Math.random()*0.02-0.04; //回転速度をセット
this.ra_now = 0;
this.range_mv = 0.4 - Math.random()*0.2; //中心からの移動量をセット
this.range = -20; //円の半径の初期値をセット
let x_renge = x + 20-this.x; //自機と弾幕とのx座標の距離を取得
let y_renge = 470 - this.y; //自機と弾幕とのy座標の距離を取得
let angle = Math.atan2(y_renge, x_renge); //自機と弾幕とのラジアン角の取得
this.mx = Math.cos(angle)*1; //弾幕のx座標の移動量をセット
this.my = Math.sin(angle)*1; //弾幕のy座標の移動量をセット
}
//移動と描画
move(){
let rg = Math.PI*0.2;
let xx = 0;
let yy = 0;
if (this.alive) { //弾幕が有効のとき
if (!miss && !clear) { //自機が被弾状態でない、かつ、クリア状態でない
this.x = this.x + this.mx; //弾幕のx座標の更新(自機に向かう角度)
this.y = this.y + this.my; //弾幕のy座標の更新(自機に向かう角度)
this.ra_now = this.ra_now + this.ra; //-0.02~-0.04のいずれかの値で毎回更新(反時計回りにするための移動量)
this.range = this.range + this.range_mv; // //-0.2~-0.4のいずれかの値で更新(円の中心からの半径に相当)
}
//弾幕の画像をセット
let image = new Image();
image.src = BarrageMissile.imageBarrage;
//スター表示
for (let i = 1; i <= 10; i++) {
if (i%2 == 1) { //奇数番号のとき
//内側の弾幕の座標をセット
xx = Math.cos(this.ra_now+rg*i) * this.range / 2;
yy = Math.sin(this.ra_now+rg*i) * this.range / 2;
} else { //偶数番号のとき
//外側の弾幕の座標をセット
xx = Math.cos(this.ra_now+rg*i) * this.range;
yy = Math.sin(this.ra_now+rg*i) * this.range;
}
ctx.drawImage(image, this.x+xx, this.y+yy, 8, 8);
//自機との当たり判定
if (this.y+yy >= 460 && this.y + yy <= 474) {
if (this.x + xx >= x + 14 && this.x + xx <= x + 20) {
hit(); //被弾処理
}
}
}
//弾幕が流れていったときの処理
if (this.y > 1000) { //誘導弾のy座標が1000pxを超えたとき
this.alive = false; //弾幕を無効にする
}
}
}
}
/****************************************************
アイテム、インベーダー、各ミサイルのインスタンス生成
*****************************************************/
//4アイテムのインスタンス生成
for (let i = 0; i <= 4; i++) {
item[i]=new Item(false, 0, 0, 0);
}
//インベーダのインスタンス生成
for (let i = 0; i <= 4; i++) {
for (let j = 0; j <= 9; j++) {
switch (i) {
case 0:
teki[i][j] = new Enemy(false, j * 50, i * 50 + 50, "res/inv5_32x24.png");
break;
case 1:
teki[i][j] = new Enemy(false, j * 50, i * 50 + 50, "res/inv4_32x24.png");
break;
case 2:
teki[i][j] = new Enemy(false, j * 50, i * 50 + 50, "res/inv3_32x24.png");
break;
case 3:
teki[i][j] = new Enemy(false, j * 50, i * 50 + 50, "res/inv2_32x24.png");
break;
case 4:
teki[i][j] = new Enemy(false, j * 50, i * 50 + 50, "res/inv1_32x24.png");
break;
}
}
}
//インベーダーの3Wayミサイルのインスタンス生成
for (let j = 0; j <= 30; j++) {
tama[0][j] = new ThreeWayMissile(false);
}
//インベーダーの誘導弾ミサイルのインスタンス生成
for (let j = 0; j <= 5; j++) {
tama[1][j] = new GuidedMissile(false);
}
//インベーダーのビームのインスタンス生成
for (let j = 0; j <= 3; j++) {
tama[2][j] = new Beam(false);
}
//インベーダーの弾幕ミサイルのインスタンス生成
for (let j = 0; j <= 4; j++) {
tama[3][j] = new BarrageMissile(false);
}
//自機のミサイルのインスタンス生成
for (let i = 0; i <= 6; i++) {
jtama[i] = new SpaceshipMissile(false, 0, 0);
}
/****************************************************
コールバック設定
*****************************************************/
setInterval(game, 10);
/****************************************************
関数宣言
*****************************************************/
//自機表示・枠表示
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);
}
//ゲーム枠の表示
ctx.beginPath();
ctx.rect(0, 0, 800, 500);
ctx.strokeStyle = "rgba(0, 0, 255)";
ctx.stroke();
ctx.closePath();
//残機、アイテム、スコアの枠表示
ctx.beginPath();
ctx.rect(800, 0, 20, 500); //左枠
ctx.rect(800, 0, 400, 20); //上の枠
ctx.rect(800, 300, 400, 20); //真ん中の枠
ctx.rect(800, 480, 400, 20); //下の枠
ctx.rect(1180, 0, 20, 500); //右枠
ctx.strokeStyle = "rgba(0, 0, 255)"; //輪郭を青色で指定
ctx.stroke(); //枠線の描画
ctx.fillStyle = "rgba(0, 0, 255)"; //塗りつぶす色を青色で指定
ctx.fill(); //塗りつぶし
ctx.closePath();
//残機の表示
if (zanki>=1&&zanki<=3){
for (i=0;i<zanki;i++){
ctx.drawImage(jikiimg, 900 + i*100, 50, 60, 60);
}
}else if (zanki>3&&zanki<=6){
for (i=0;i<3;i++){
ctx.drawImage(jikiimg, 900 + i*100, 30, 60, 60);
}
for (i=0;i<zanki-3;i++){
ctx.drawImage(jikiimg, 900 + i*100, 100, 60, 60);
}
}else if (zanki>=7){
ctx.drawImage(jikiimg, 900, 50, 60, 60);
ctx.font = "48px serif";
ctx.fillText("✖", 1000, 100);
ctx.fillText(String(zanki), 1050, 100);
}
//ステージ数、スコアの表示
ctx.font = "48px serif";
ctx.fillText("STAGE", 850, 380);
ctx.fillText(String(stage), 1020, 380);
ctx.font = "48px serif";
ctx.fillText("Score", 850, 430);
ctx.fillText(String(score), 980, 430);
//アイテムの表示
Item.drawStatus();
}
//自機のミサイル処理
function shotmove() {
for (h = 0; h < jtama.length; h++){
jtama[h].move();
}
}
//敵・弾リセット処理
function nextstage() {
clear = false; //クリア状態を戻す
//敵をすべて復活させる
for (i = 0; i < teki.length; i++) {
for (j = 0; j < teki[i].length; j++) {
teki[i][j].alive = true;
}
}
//各変数のクリア
stage++; //ステージ数を1つアップする
SpaceshipMissile.clearShotNum(); //ミサイルの数をクリア
tcount = 50; //敵のカウント数をクリア
x = 0; //自機のx座標をクリア
tx = 0; //インベーダの移動用のx座標
tmove = 2; //インベーダの移動量
miss = false; //ミス状態のクリア
if (bomb == 0){ //インベーダのミサイルを破壊する爆弾が0個のとき
bomb++; //インベーダのミサイルを破壊する爆弾を1つ増やす
}
//自機のミサイルをすべて無効にする
for (i = 0; i <= 6; i++) {
jtama[i].setAlive(false);
}
//アイテムをすべて無効にする
for (j = 0; j <= 4; j++) {
item[j].setAlive(false);
}
tamareset(); //インベーダのミサイルをすべて無効にする
}
//インベーダのミサイルをすべて無効にする
function tamareset() {
for (let i = 0; i < tama.length; i++) {
for (let j = 0; j < tama[i].length; j++) {
tama[i][j].alive = false;
}
}
}
//敵描写
function tdrow() {
for (let i = 0; i < teki.length; i++) {
for (let j = 0; j < teki[i].length; j++) {
if (teki[i][j].alive) { //インベーダーが生存しているとき
//インベーダーの描画
let tekiimg = new Image();
tekiimg.src = teki[i][j].image;
ctx.drawImage(tekiimg, teki[i][j].x+tx, teki[i][j].y, 32, 24);
if (!miss) { //自機が被弾状態でないとき
if (teki[i][j].x+tx > 800-32){ //インベーダーが右端に到達したとき
tmove=-2; //左方向への移動にセット
}else if (teki[i][j].x+tx < 0){ //インベーダーが左端に到達したとき
tmove=2; //右方向への移動にセット
}
shotchance(i, j); //インベーダーの攻撃の抽選
}
}
}
}
if (!miss) { //自機が被弾状態でないとき
//インベーダーの数に応じて移動速度を変える
tact = tact + 1; //カウントアップ
if (tact >= tcount) { //カウンタが生存しているインベーダーの数を超えたとき
tx = tx + tmove; // //インベーダーのx座標の更新
tact = 0; //カウンタをクリア
}
}
}
//ドロップアイテム抽選
function dropchance(i, j){
for (let k = 0; k < item.length; k++) {
if (!item[i].getAlive()) { //アイテムが有効でないとき
item[k].drawRandomly(i, j);
break;
}
}
}
//敵が弾を撃つかどうかの抽選
function shotchance(_i, _j){
let ratio;
switch (_i) {
//上から5段目と4段目のインベーダーのとき
case 4:
case 3:
ratio = Math.random()*120000; //抽選
if(ratio < stage*10 + 500 - tcount*10) { //ステージ数と生き残っているインベーダーの数で抽選
for(let k = 0; k < tama[0].length; k++) { //全3Wayミサイルを走査
if(!tama[0][k].getAlive()){ //ミサイルが未発射のとき
tama[0][k].shot(_i, _j); //ミサイルを発射する
break;
}
}
}
break;
//上から3段目のインベーダーのとき
case 2:
ratio = Math.random()*800000; //抽選
if (ratio < stage*10 + 500 - tcount*10) { //ステージ数と生き残っているインベーダーの数で抽選
for (let k = 0; k < tama[1].length; k++) {
if (!tama[1][k].getAlive()) { //ミサイルが未発射のとき
tama[1][k].shot(_i, _j); //ミサイルを発射する
break;
}
}
}
break;
//上から2段目のインベーダーのとき
case 1:
ratio = Math.random()*200000; //抽選
if(ratio < stage*10 + 500 - tcount*10) { //ステージ数と生き残っているインベーダーの数で抽選
for(k = 0; k < tama[2].length; k++) {
if (!tama[2][k].getAlive()) { //ミサイルが未発射のとき
tama[2][k].shot(_j);
break;
}
}
}
break;
//最上段のインベーダーのとき
case 0:
ratio = Math.random()*8000000; //抽選
if(ratio < stage*10 + 5000 - tcount*100) { //ステージ数と生き残っているインベーダーの数で抽選
for(let k = 0; k < tama[3].length; k++) {
if (!tama[3][k].getAlive()) { //ミサイルが未発射のとき
tama[3][k].shot(_i, _j);
break;
}
}
}
break;
}
}
//アイテム落下処理
function itemmove(){
for (k = 0; k < item.length; k++) {
item[k].move();
}
}
//被弾処理
function hit(){
if (!miss && !shield && invincible <= 0) { //被弾状態でない、かつ、シールド中でない、かつ、無敵状態でないとき
miss = true; //被弾状態にセット
} else if (shield) { //シールド中のとき
shield = false; //シールド状態解除
}
invincible = 300; //無敵期間をセット
}
//3Way弾の移動と描画
function threeWayMissileMove(){
for (k = 0; k < tama[0].length; k++) {
tama[0][k].move();
}
}
//誘導弾
function guidedMissileMove(){
for (let k = 0; k < tama[1].length; k++) {
tama[1][k].move();
}
}
//ビーーーーーーーーーーーーーーーーーーーーム
function beamMove(){
for (let k = 0; k < tama[2].length; k++) {
tama[2][k].move();
}
}
//スター弾幕
function barrageMissileMove(){
for (let k = 0; k < tama[3].length; k++) {
tama[3][k].move();
}
}
//リスポーン処理
function respone(){
if (zanki >= 1) { //自機の残数が1以上のとき
zanki--; //自機の残数を1つ減らす
bomb = 2; //ボムのアイテム数を2にセット
power = Math.ceil(power/2); //パワーアップアイテムの数を半分に減らす
x = 0; //x座標の初期化
SpaceshipMissile.clearShotNum(); //ショット数の初期化
//自機のミサイルの初期化
for (let i = 0; i < jtama.length; i++) {
jtama[i].setAlive(false);
}
miss=false; //被弾状態のクリア
//インベーダーの3Wayミサイルの初期化
for (let j = 0; j < tama[0]/length; j++) {
tama[0][j].setAlive(false);
}
//インベーダーの誘導弾ミサイルの初期化
for (let j = 0; j < tama[1].length; j++) {
tama[1][j].setAlive(false);
}
//インベーダーのレーザービームの初期化
for (let j = 0; j < tama[2].length; j++) {
tama[2][j].setAlive(false);
}
//インベーダーの弾幕ミサイルの初期化
for (j = 0; j < tama[3].length; j++) {
tama[3][j].setAlive(false);
}
//アイテムの初期化
for (j = 0; j < item.length; j++) {
item[j].setAlive(false);
}
} else if (!gameover) { //残機がなく、かつ、ゲームオーバーでないとき
gameover = true; //ゲームオーバー状態にセット
} else { //上記以外の状態でもう一度キーが押されたとき
gameover = false; //ゲームオーバー状態を初期化
stage = 0; //ステージ数を初期化
zanki = 3; //残機数を初期化
score = 0; //スコアを初期化
extend = 0; //自機を増やすときの拡張数を初期化
power = 0; //パワーアップアイテム数を初期化
nextstage(); //
}
}
//クリア表示
function cleareffect() {
clrimg = new Image();
clrimg.src = "res/text_gameclear_e.png";
if (stage != 0) { //初期表示中でないとき
ctx.drawImage(clrimg, 100,200, 500, 100);
}
}
//ゲームオーバー表示
function gameovereffect() {
clrimg = new Image();
clrimg.src = "res/text_gameover_e.png";
ctx.drawImage(clrimg, 100,200, 500, 100);
}
//ボムエフェクト
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() {
//キャンバスのクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
//自機関連の制御関数の呼び出し
draw();
shotmove();
//インベーダー関連の制御関数の呼び出し
tdrow();
itemmove();
threeWayMissileMove();
guidedMissileMove();
beamMove();
barrageMissileMove();
//ボム爆発表示
if (bombwait<20){
bombeffect();
}
//クリア時の表示
if (clear) {
cleareffect();
}
//ゲームオーバー表示
if (gameover) {
gameovereffect();
}
//自機の移動
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をセット
}
}
}
//各種カウンタのカウント
SpaceshipMissile.countShotwait();
bombwait++;
invincible--;
//デバッグ用
debug();
}
//キー入力
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') && SpaceshipMissile.getShotNum() <= power && SpaceshipMissile.getShotwait() > 20) {
for (let i = 0; i < jtama.length; i++) {
if(!jtama[i].getAlive()) {
jtama[i].fire();
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(); //次のステージにセット
}
}
//キー開放
function keyUpHandler(e) {
//右矢印キーまたはDキー押下
if (e.key === 'ArrowRight' || e.key === 'd') {
keyr = false; //右キー押下状態をクリア
}
//左矢印キーまたはAキー押下
if (e.key === 'ArrowLeft' || e.key === 'a') {
keyl = false; //左キー押下状態をクリア
}
}
function debug() {
// console.log(x);
}
さいごに
今回はインベーダーの残りのミサイルに関連して一気にリファクタリングしました。次回はインベーダークラスについてリファクタリングを行う予定です。インベーダーに関する制御についてはグローバル変数で持っている情報も多く、これまでのリファクタリングよりもやることが多そうです。次回もお楽しみに!
※アイキャッチ画像はMicrosoft Designerで作成しています