#ボス敵を作る
各面の最後にボス敵を出したいなあ。
って訳で、ボス敵を作ってみます。
まず、普通の敵とボス敵の相違点を出してみました。
##ボス敵と普通の敵と違う所
・シナリオが進まない
・倒した後、面クリアする
・硬い
・大きい
・ある程度攻撃すると攻撃方法を変えてくる
・やっつけた時にたくさん爆発する
こんな所でしょうか。
##ボス敵の画像を用意する
ボス敵っぽいものを GIMP使ってちゃちゃっと描きました。
背景が透過だと見にくいので、黒くしてみました。
この機会に、他の敵のグラフィックとかも少々手直ししたのですが、こういう絵って、後からディテールを細かくするのってむっちゃ難しくて全然ダメダメでした。
紙に描いたものをスキャンするなりディテールを細かく作っておいて、そこから無駄な所を省く、ってやった方が良かったのかも。
また、縦シューティング用の影の付け方がよく解らず横から光を当てたようにしてしまったのだけど、こうすると左右対称の形をキープするのがむっちゃ難しい。
で、他のシューティングはどうしているのか調べたところ、結構上から光当てて左右対称に影を付けているものが多いですね。
ゼビウスのように斜めに影を付けるのはすごく難しいです。
ASSETS に、BOSS.gif を追加しました。
var ASSETS = {
// 画像
image: {
'ship': 'ship.gif',
'enemy': 'enemy.gif',
'boss': 'BOSS.gif',
'bullet': 'bullet.gif',
'explosion': 'EXPLOSION.gif',
'gameover' : 'GAMEOVER.gif',
'title' : 'title.gif',
},
##シナリオを進まないようにする
まず、ボス敵用にシナリオの動きを修正します。
MainScene の UPDATE のシナリオ関連の処理です。
// シナリオ関連の処理
// 面数が配列の最大を超えた場合は最初に戻す
if (this.stage >= scenarioArray.length) {
this.stage = 0;
this.scenarioFrame = 0;
this.scenarioNo = 0;
this.scenarioNowFrame = 0;
}
// シナリオ配列から面数のシナリオを取得
var scenarioStageArray = scenarioArray[this.stage];
// シナリオ数が配列の最大を超えた場合はシナリオの処理を行わない
if (this.scenarioNo >= scenarioStageArray.length) {
return;
}
// シナリオのフレームを進める
this.scenarioFrame++;
// シナリオのフレーム数が次のシナリオフレーム数を超えた場合
if (this.scenarioNowFrame + scenarioStageArray[this.scenarioNo].frame <= this.scenarioFrame) {
// 現在のシナリオフレーム数を更新する
this.scenarioNowFrame += scenarioStageArray[this.scenarioNo].frame;
// シナリオを実行する
doScenario(scenarioStageArray[this.scenarioNo]);
// 次のシナリオに進む
this.scenarioNo++;
}
修正した所は、
// シナリオ数が配列の最大を超えた場合はシナリオの処理を行わない
って所です。修正前は、次の面のシナリオに移動していました。
それを、シナリオの処理を行わないようにしました。
##ボス敵の基本クラスを作成する
ボス敵は面ごとに異なる攻撃をするんだろうけど、ボス敵として同じ処理があるはずなので、敵の基本クラスを作った時みたいにボス敵の基本クラスを作ります。
/**
* ボス敵の基本クラス
*/
phina.define('Boss', {
// Spriteを継承
superClass: 'Sprite',
// 初期化
init: function(image, sizeX, sizeY) {
// 親クラスの初期化
this.superInit(image, sizeX, sizeY);
// プロパティの初期化
this.x = 0;
this.y = 0;
// 硬さ
this.hardness = 200;
// 敵を倒した時のスコア
this.score = 10000;
// 弾を当てた時のスコア
this.bulletScore = 20;
// 爆発処理かどうかのフラグ
this.isExplosion = false;
// ボス用のカウンター
this.count = 0;
},
update: function(app) {
// 硬さが0の場合
if (!this.isExplosion && this.hardness <= 0) {
// 爆発処理
this.isExplosion = true;
// カウントをリセット
this.count = 0;
// スコアを加算
mainScene.score += this.score;
mainScene.scoreLabel.setValue(mainScene.score);
}
// 爆発処理
if (this.isExplosion == true) {
// 爆破処理
this.count++;
// 爆発を起こす
if (this.count % 10 == 1) {
var tmpX = Random.randint(0, this.width);
var tmpY = Random.randint(0, this.height);
// 爆発を表示
Explosion(this.left + tmpX, this.top + tmpY).addChildTo(mainScene.group.explosionGroup);
// 爆発音を出す
SoundManager.play('bom');
}
// 爆発終了処理
if (this.count >= 100) {
// 自機を1機追加
mainScene.remainingShip++;
mainScene.remainingShipLabel.setValue(mainScene.remainingShip);
// 次の面へ
mainScene.stage++;
// シナリオを初期化
mainScene.scenarioFrame = 0;
mainScene.scenarioNo = 0;
mainScene.scenarioNowFrame = 0;
// 削除する
this.remove();
delete this;
return;
}
return;
}
// 自機との当たり判定
collisionShip(this);
// 動かす
this.move();
},
});
ここで行っている処理の主なものは、爆発処理ですね。
ボスはみんな同じような爆発をすることにして、次の面へ移動します。
まず、
// 爆発処理かどうかのフラグ
this.isExplosion = false;
ってフラグを持たせておいて、ボス敵の硬さが0になったらこのフラグを立てます。
で、このフラグが立っている場合、カウンターが0~100の間、かつカウンター10間隔でボス敵内のランダムな場所に爆発のオブジェクトを生成しています。
爆発処理が終ったら、シナリオを次の面に移動して初期化しています。
##ボス1クラスを作る
さて、ボス敵の基本クラス を継承した、ボス1クラスを作ります。
まず、全体的な流れを作ります。
/**
* ボス1クラス
*/
phina.define('Boss01', {
// Bossを継承
superClass: 'Boss',
// 初期化
init: function() {
// 親クラスの初期化
this.superInit('boss', 96, 96);
// プロパティの設定
this.frameIndex = 0;
this.score = 10000;
this.bulletScore = 20; // 弾を当てた時のスコア
this.hardness = 200;
this.x = mainScene.gridX.center();
this.bottom = 0;
// 初期値を設定
this.baseHardness = this.hardness; // 硬さの初期値を保持しておく
this.speedX = 0;
this.speedY = 2;
this.count = 0;
this.mode = 1;
this.direction = 1; // 移動方向 -1:左、1:右
},
// 敵を動かすメソッド
move: function () {
// モードごとの処理
switch (this.mode) {
case 1:
// 下降する
break;
case 2:
// 左右移動で弾を撃つ
break;
case 3:
// 左右移動、一定間隔で3方向に弾発射
break;
case 4:
// 左右移動、両端に行ったら自機方向に5方向弾を発射
break;
}
},
});
switch 文の中に、実際のボスの動きを作っていくのですが、とりあえずモードが4つあって、動きが4段階に変わる感じです。
では、実際にモードを実装してみます。
###モード1
モード1は、上からボス敵がやってきて、画面4/16の所(つまり1/4の所)まで進むだけです。
画面1/4の所まで来たら、モード2になります。
case 1:
// 下降する
this.y += this.speedY;
if (this.y > mainScene.gridY.span(4)) {
// モード2へ
this.speedX = 2;
this.speedY = 0;
this.mode = 2;
}
break;
###モード2
モード2は、左右に移動しながら、ランダムで自機に向かって弾を撃ちます。
自機に向かって弾を撃つ方法は、敵が弾を撃つ処理を作った時に作成した createAngle と EnemyBullet を使っています。
case 2:
// 左右移動で弾を撃つ
this.x += this.speedX * this.direction;
if (this.right >= mainScene.width) {
// 左方向へ
this.direction = -1;
} else if (this.left < 0) {
// 右方向へ
this.direction = 1;
}
// ランダムに自機方向へ弾を撃つ
if (Random.randint(0, 15) == 0) {
// 自機までの角度を算出
var angle = createAngle(this, ship);
// 弾を発射
EnemyBullet(this.x, this.y, angle, 4).addChildTo(mainScene.group.enemyBulletGroup);
}
// 硬さが減った場合
if (this.hardness < this.baseHardness * 2 / 3) {
// モード3へ
this.speedX = 4;
this.speedY = 0;
this.mode = 3;
}
break;
ボス敵の硬さが2/3になったら、モード3になります。
###モード3
モード3は、左右に移動しながら一定間隔で3方向に弾を発射します。
case 3:
// 左右移動、一定間隔で3方向に弾発射
this.x += this.speedX * this.direction;
if (this.right >= mainScene.width) {
// 左方向へ
this.direction = -1;
} else if (this.left < 0) {
// 右方向へ
this.direction = 1;
}
// フレーム数をカウント
this.count++;
// フレーム数が一定数を超えたら弾発射
if (this.count > 20) {
// カウントを戻す
this.count = 0;
// 3方向に弾発射
var angleArray = new Array(3);
angleArray[0] = Math.PI * 3 / 2;
angleArray[1] = angleArray[0] - Math.PI / 6;
angleArray[2] = angleArray[0] + Math.PI / 6;
angleArray.forEach((angle) => {
// 弾を発射
EnemyBullet(this.x, this.y, angle, 4).addChildTo(mainScene.group.enemyBulletGroup);
});
}
// 硬さが減った場合
if (this.hardness < this.baseHardness * 1 / 3) {
// モード4へ
this.speedX = 8;
this.speedY = 0;
this.mode = 4;
}
break;
ここでのポイントは、3方向に弾を発射する所かと思います。
弾を発射する時のアングルはラジアンなんですが、EnemyBullet のアングルは 分度器と同じ向きで考えているので、下向きは 3/2π(パイ) になります。
その下向きを基準に、1/6π(つまり30°)足したものと引いたものを同時に発射すれば、3方向弾になります。
この3つを配列に入れて、forEach で配列分 EnemyBullet を呼び出しています。
ボス敵の硬さが1/3になったら、モード4になります。
###モード4
モード4は、左右に移動して、両端に行ったら自機方向に5方向に弾を発射します。
case 4:
// 弾を発射するかどうかのフラグ
var isShoot = false;
// 左右移動、両端に行ったら自機方向に5方向弾を発射
this.x += this.speedX * this.direction;
if (this.right >= mainScene.width) {
// 左方向へ
this.direction = -1;
// 弾発射
isShoot = true;
} else if (this.left < 0) {
// 右方向へ
this.direction = 1;
// 弾発射
isShoot = true;
}
// 弾発射処理
if (isShoot) {
// 自機方向に5つ弾発射
var angleArray = new Array(5);
angleArray[0] = createAngle(this, ship);
angleArray[1] = angleArray[0] - Math.PI / 12;
angleArray[2] = angleArray[0] - Math.PI / 6;
angleArray[3] = angleArray[0] + Math.PI / 12;
angleArray[4] = angleArray[0] + Math.PI / 6;
angleArray.forEach((angle) => {
// 弾を発射
EnemyBullet(this.x, this.y, angle, 4).addChildTo(mainScene.group.enemyBulletGroup);
});
}
break;
自機方向に5方向弾を発射、って所は、モード2の自機方向に弾を発射と、モード3の3方向弾の応用です。
自機方向を基準に、30°ごとに5方向に弾を発射します。
こう考えると、円形に弾を発射するとかも簡単にできそうですね。
##シナリオを設定して完成!
ボス1クラスが完成したので、あとはシナリオに入れるだけです。
/**
* ゲームシナリオ
*/
// シナリオを格納する配列
var scenarioArray = [
// 1面目
[
{frame:100, scenarioName: "enemy01"}
,{frame:50, scenarioName: "enemy01"}
,{frame:40, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:30, scenarioName: "enemy01"}
,{frame:100, scenarioName: "BOSS01"}
]
];
敵が10機出てきたらボス敵が出てくるようにしました。
あとは、BOSS01 ってシナリオになったらボス1クラスを生成するようにすれば完成です。
/** シナリオを実行する関数
* scenario シナリオ
*/
function doScenario (scenario) {
switch (scenario.scenarioName) {
case "enemy01":
// 敵1を生成する
Enemy01().addChildTo(mainScene.group.enemyGroup);
break;
case "BOSS01":
// ボス1を生成する
Boss01().addChildTo(mainScene.group.enemyGroup);
break;
}
}
##今日の成果
今日の成果をここに上げました。
敵が10機出た後、ボス敵が出ます。
http://hirotyan.my.coocan.jp/phinajs/Shooting/010/index.html