#いよいよ敵を表示してみる
と言っても、ただ単にスプライトをランダムに表示するってのではなくて、決まったタイミングで決まった敵を出すようにしたいです。
でも、この辺をどう作るのが定石かって情報はゲーム作成の入門書とかにはあまり載っていないのですよね・・・。
シナリオを作成する
そこで、どのタイミングでどの敵を出すのか、っていうシナリオのオブジェクトを作ることにしました。
と言っても、
・何フレーム後に出すか?
・どの敵を出すか?
という情報を持っている配列を作るだけです。
面ごとの配列の中に、シナリオの配列を持ち、その中にシナリオ情報を連想配列で持っているだけです。
・面の配列
・シナリオの配列
・何フレーム後に出すか?
・どの敵を出すか?
という構造です。
/**
* ゲームシナリオ
*/
// シナリオを格納する配列
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"}
]
];
始まって100フレーム後に enemy01 を出して、その50フレーム後に enemy01 を出して、という感じにしてます。
この形の欠点は、1シナリオで1つの敵を出すことだけを考えているので、同時に複数の敵を出すことを考えていません。
##シナリオを読み込む
シナリオの読み込みは描画単位で行うため、メインシーンの update 部分で行います。
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(option) {
this.superInit(option);
// ~省略
// シナリオ関連の初期化
// 面(0始まり)
this.stage = 0;
// シナリオのフレーム数
this.scenarioFrame = 0;
// 現在のシナリオ番号
this.scenarioNo = 0;
// 現在のシナリオが始まるフレーム数
this.scenarioNowFrame = 0;
// ~省略
update: function () {
// ~省略
// シナリオ関連の処理
// 面数が配列の最大を超えた場合は最初に戻す
if (this.stage >= scenarioArray.length) {
this.stage = 0;
this.scenarioFrame = 0;
this.scenarioNo = 0;
this.scenarioNowFrame = 0;
}
// シナリオ配列から面数のシナリオを取得
var scenarioStageArray = scenarioArray[this.stage];
// シナリオのフレームを進める
this.scenarioFrame++;
// シナリオのフレーム数が次のシナリオフレーム数を超えた場合
if (this.scenarioNowFrame + scenarioStageArray[this.scenarioNo].frame <= this.scenarioFrame) {
// 現在のシナリオフレーム数を更新する
this.scenarioNowFrame += scenarioStageArray[this.scenarioNo].frame;
// シナリオを実行する
doScenario(scenarioStageArray[this.scenarioNo]);
// 次のシナリオに進む
this.scenarioNo++;
// シナリオ数が配列の最大を超えた場合はシナリオの最初に戻す
if (this.scenarioNo >= scenarioStageArray.length) {
this.scenarioNo = 0;
}
}
},
});
描画単位ごとに、scenarioFrame を加算していき、これが次のシナリオフレーム数を超えた場合、シナリオを実行するようにしています。
シナリオの実行は、doScenario という関数に、シナリオの情報を渡すだけです。
で、doScenario の内容が以下です。
/** シナリオを実行する関数
* scenario シナリオ
*/
function doScenario (scenario) {
switch (scenario.scenarioName) {
case "enemy01":
// 敵1を生成する
Enemy01().addChildTo(mainScene.group.enemyGroup);
break;
}
}
シナリオ情報を受け取って、scenarioName ごとに処理を分けて敵オブジェクトを生成し、敵用のグループに追加するだけです。
今は1種類しか敵を作っていないけど、scenarioName が変われば、違う敵が生成される想定です。
##Enemy01 を作る前に
敵なんてものはいろんな種類があっても、「敵」と表せる共通の概念があるはずです。
・自機がぶつかると自機がやられる。
・自機の弾に撃たれて破壊される。その時点数が入る。
みたいな感じです。
これを全ての敵に実装するのはバカバカしいので、敵を作るためのスーパークラスを作ろうと思いました。
また、敵が動くタイミングは全て共通だけど、敵の動きは異なることから、敵のスーパークラスを抽象クラスのように作れないか考えてみました。
// 敵の基本クラス
phina.define('Enemy', {
// Spriteを継承
superClass: 'Sprite',
// 初期化
init: function(image, sizeX, sizeY) {
// 親クラスの初期化
this.superInit(image, sizeX, sizeY);
// プロパティの初期化
this.x = 0;
this.y = 0;
// 硬さ
this.hardness = 1;
// 弾を当てた時のスコア
this.bulletScore = 10;
// 敵のフレームカウント数
this.frameCount = 0;
},
update: function(app) {
// フレームカウントを進める
this.frameCount++;
// 動かす
this.move();
// 画面の外に出たら消す
if (this.right < 0 || this.left > mainScene.width || this.bottom < 0 || this.top > mainScene.height) {
this.remove();
delete this;
}
},
// 敵を動かすメソッド
move: undefined // 継承先で設定
,
});
Spriteクラスを継承したクラスなんですが、敵のイメージとかサイズが敵ごとに異なることを想定して、これは継承先から受け取るようにします。
で、敵全体で必要になるであろうプロパティを適当に宣言しておきます。
キモは、update 処理なんですけど、ここでは this.move(); ってやっているだけです。で、その this.move() ってのはこのクラスでは undefined になっていて、継承先で作る想定にしています。
こうすれば、継承先の敵ごとに全く異なる動きができるんじゃないかと思いました。
敵が画面の外に出たら消すのは敵共通の処理なので、このクラス内で実装しています。
##Enemy01 を実装する
まず、敵の画像を用意しなくては!
適当に作ってこんな感じ。
相変わらず背景が透明だと見にくいので、背景を黒くしてみました。
この画像のフレーム1~3を使って、ジグザグに動く敵を実装しました。
/**
* 敵1クラス
* ジグザグ移動
*/
phina.define('Enemy01', {
// Enemyを継承
superClass: 'Enemy',
// 初期化
init: function() {
// 親クラスの初期化
this.superInit('enemy', 32, 32);
// プロパティの設定
this.frameIndex = 2;
this.score = 100;
this.hardness = 1;
// 移動速度を決定
this.speed = 4;
// 初期値を設定
this.x = Random.randint(0, mainScene.width);
this.y = 0;
// 横方向の速度
this.speedX = this.speed;
// 縦方向の速度
this.speedY = 2 * Random.randint(1, 3);
// モード 1:左方向、-1:右方向
this.mode = 1;
// 出現位置により移動方向を決める
if (this.x > mainScene.width / 2) {
this.speedX = -this.speed;
this.mode = -1;
}
},
// 敵を動かすメソッド
move: function () {
this.x += this.speedX;
this.y += this.speedY;
// スピードを加算する。
this.speedX += this.mode * 2;
// スピードが元の4倍を超えた場合
if (Math.abs(this.speedX) >= this.speed * 4) {
// 方向を逆転する
this.mode *= -1;
}
// 画像の決定
if (Math.abs(this.speedX) < this.speed * 2) {
// 正面
this.frameIndex = 2;
} else if (this.mode == 1) {
// 右向き
this.frameIndex = 3;
} else {
// 左向き
this.frameIndex = 1;
}
},
});
Enemyを継承して、move に動かしたい内容を実装しています。
ここまで作ると、シナリオ内容通りのタイミングで、敵が出てきます。
敵を生成するのは、
Enemy01().addChildTo(mainScene.group.enemyGroup);
ってやっているだけなんですよね。
これだけで、敵は勝手にクラス設計通りに生成されて勝手に動いて勝手に削除されるんですよね。
敵を増やすのはシナリオの数を増やすだけでOKなんです。
オブジェクト指向って本当にすごいなと思います。
#今日の成果
今日の成果をここに上げました。
敵がわらわらと出てきます。
http://hirotyan.my.coocan.jp/phinajs/Shooting/004/index.html
シナリオ関連とか敵関連は、増えてくると見にくくなるため、別ファイルに記載するようにしました。
phina が読み込めていないと phina.define が想定通りに動かないので、enemy.js を読み込むタイミングは main.js の後になります。
index.html
<!doctype html>
<html>
<head>
<meta charset='utf-8' />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Getting started | phina.js</title>
<!-- phina.js を読み込む -->
<script src='http://cdn.rawgit.com/phi-jp/phina.js/v0.2.0/build/phina.js'></script>
<!-- シナリオ -->
<script src="scenario.js"></script>
<!-- メイン処理 -->
<script src='main.js'></script>
<script src="enemy.js"></script>
</head>
<body>
</body>
</html>
scenario.js
/**
* ゲームシナリオ
*/
// シナリオを格納する配列
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"}
]
];
main.js
// phina.js をグローバル領域に展開
phina.globalize();
// 画面サイズ
var SC_WIDTH = 320;
var SC_HEIGHT = 480;
// アセット
var ASSETS = {
// 画像
image: {
'ship': 'ship.gif',
'enemy': 'enemy.gif',
},
sound: {
'shoot': 'shoot.wav',
},
};
// ハイスコア
var hiscore = 0;
// メインシーンを保持する変数
var mainScene;
// MainScene クラスを定義
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(option) {
this.superInit(option);
// メインシーンを変数に格納
mainScene = this;
// 背景色を指定
this.backgroundColor = '#000000';
// グループを生成する
this.group = {
// 背景用
backgroungGroup: DisplayElement().addChildTo(this),
// 自機用
shipGroup: DisplayElement().addChildTo(this),
// 敵用
enemyGroup: DisplayElement().addChildTo(this),
// 自機弾用
myBulletGroup: DisplayElement().addChildTo(this),
// 爆発用
explosionGroup: DisplayElement().addChildTo(this),
// 敵弾用
enemyBulletGroup: DisplayElement().addChildTo(this),
// スコア用
scoreGroup: DisplayElement().addChildTo(this),
};
// グループの大きさをこのシーンに合わせる
for (var groupKey in this.group) {
this.group[groupKey].width = this.width;
this.group[groupKey].height = this.height;
}
// 星を生成
(50).times(function() {
Star(this.group.backgroungGroup, Random.randint(0, this.width), Random.randint(0, this.height));
}, this);
// スコア
this.score = 0;
// 残機数
this.remainingShip = 3;
// スコア表示
this.scoreLabel =
Label({
text: 'SCORE:'+ ('0000000000' + this.score).slice(-10),
fontSize: 15,
x: 80,
y: 15,
fill: "white",
stroke: false,
}).addChildTo(this.group.scoreGroup);
// 残機数表示
this.remainingShipLabel =
Label({
text: 'SHIP:' + ('00' + this.remainingShip).slice(-2),
fontSize: 15,
x: this.width - 50,
y: 15,
fill: "white",
stroke: false,
}).addChildTo(this.group.scoreGroup);
// シナリオ関連の初期化
// 面(0始まり)
this.stage = 0;
// シナリオのフレーム数
this.scenarioFrame = 0;
// 現在のシナリオ番号
this.scenarioNo = 0;
// 現在のシナリオが始まるフレーム数
this.scenarioNowFrame = 0;
// 自機を作成
var ship = Ship().addChildTo(this.group.shipGroup);
// 初期位置
ship.x = this.gridX.center();
ship.y = this.gridY.center(5);
},
update: function () {
// ランダムで星を生成する
if (Random.randint(1, 3) == 1) {
Star(this.group.backgroungGroup, Random.randint(0, this.width), 0);
}
// シナリオ関連の処理
// 面数が配列の最大を超えた場合は最初に戻す
if (this.stage >= scenarioArray.length) {
this.stage = 0;
this.scenarioFrame = 0;
this.scenarioNo = 0;
this.scenarioNowFrame = 0;
}
// シナリオ配列から面数のシナリオを取得
var scenarioStageArray = scenarioArray[this.stage];
// シナリオのフレームを進める
this.scenarioFrame++;
// シナリオのフレーム数が次のシナリオフレーム数を超えた場合
if (this.scenarioNowFrame + scenarioStageArray[this.scenarioNo].frame <= this.scenarioFrame) {
// 現在のシナリオフレーム数を更新する
this.scenarioNowFrame += scenarioStageArray[this.scenarioNo].frame;
// シナリオを実行する
doScenario(scenarioStageArray[this.scenarioNo]);
// 次のシナリオに進む
this.scenarioNo++;
// シナリオ数が配列の最大を超えた場合はシナリオの最初に戻す
if (this.scenarioNo >= scenarioStageArray.length) {
this.scenarioNo = 0;
}
}
},
});
// 自機クラス
phina.define('Ship', {
// Spriteを継承
superClass: 'Sprite',
// 初期化
init: function() {
// 親クラスの初期化
this.superInit("ship", 32, 32);
// 画像フレームの初期値
this.frameIndex = 1;
// 自機の速度
this.speed = 8;
// 連打のフラグ
this.triggerFlag = false;
// 弾を撃ってからのフレーム数
this.trigerFrameCount = 0;
},
update: function(app) {
var key = app.keyboard;
// 上下左右移動
// 画像フレームを一旦初期化する
this.frameIndex = 1;
if (key.getKey('left')) {
this.x -= this.speed;
// 画像フレームを左移動に変更
this.frameIndex = 0;
}
if (key.getKey('right')) {
this.x += this.speed;
// 画像フレームを右移動に変更
this.frameIndex = 2;
}
if (key.getKey('up')) {
this.y -= this.speed;
}
if (key.getKey('down')) {
this.y += this.speed;
}
// 端まで行った場合は動かさない
if (this.left < 0) {
this.left = 0;
} else if (this.right > this.parent.width) {
this.right = this.parent.width;
}
if (this.top < 0) {
this.top = 0;
} else if (this.bottom > this.parent.height) {
this.bottom = this.parent.height;
}
// 弾発射
if (key.getKey('z')) {
// 8フレームに1回、または連打した場合は自弾を発射する
if (this.trigerFrameCount % 8 == 0 || !this.triggerFlag) {
// 自弾を生成
MyBullet(this.x, this.y).addChildTo(mainScene.group.myBulletGroup);
// 発射音を鳴らす
SoundManager.play('shoot');
// 連打のフラグを立てる
this.triggerFlag = true;
}
// 弾のフレーム数をインクリメントする
this.trigerFrameCount++;
} else {
// 連打のフラグを寝かせる
this.triggerFlag = false;
// 弾のフレーム数を0にする
this.trigerFrameCount = 0;
}
},
});
// 自弾クラス
phina.define('MyBullet', {
// Spriteを継承
superClass: 'Sprite',
init: function(x, y) {
// 親クラスの初期化
this.superInit("ship", 16, 16);
// 座標の設定
this.x = x;
this.y = y;
// 弾の速度
this.speed = 16;
// 画像から弾のフレームだけを設定
this.frameIndex = 6;
},
update: function(app) {
// 弾の移動
this.y -= this.speed;
// 弾が上まで行った場合は消す
if (this.bottom < 0) {
this.remove();
delete this;
}
},
});
// 背景の星クラス
phina.define('Star', {
// CircleShapeを継承
superClass: 'CircleShape',
init: function(disp, x, y) {
// 親クラスの初期化
this.superInit({
stroke: false,
});
// 星の半径の設定
this.radius = Random.randint(1, 2);
// 色の明るさ設定
this.light = 240;
// 色を設定
this.fill = Color(this.light, this.light, this.light, 1).toStyleAsRGB();
// 座標の設定
this.x = x;
this.y = y;
// 移動速度
this.speed = Random.randint(1, 4);
// 表示領域に表示する
this.addChildTo(disp);
// フレームカウント
this.frameCounnt = 1;
// 点滅タイミング
this.twinkle = Random.randint(20, 100);
// モード
this.mode = 0; // 0:通常、1:暗く、2:明るく
},
update: function(app) {
// 星の点滅処理
// 星が通常の場合
if (this.mode == 0) {
// 点滅タイミングの場合
if (this.frameCounnt % this.twinkle == 0) {
// 暗くするモードにする
this.mode = 1;
this.frameCounnt = 1;
} else {
// フレーム数をインクリメントする
this.frameCounnt ++;
}
// 星が暗くなっている場合
} else if (this.mode == 1) {
// 暗くする
this.light -= 10;
// 一番暗くなった場合は明るくするモードにする
if (this.light < 0) {
this.mode = 2;
this.light = 0;
}
// 星が明るくなっている場合
} else {
// 明るくする
this.light += 10;
// 一番明るくなった場合は通常モードにする
if (this.light > 240) {
this.mode = 0;
this.light = 240;
}
}
// 星の色を変える
this.fill = Color(this.light, this.light, this.light, 1).toStyleAsRGB();
// 移動
this.y += this.speed;
// 下まで行った場合は消す
if (this.top > this.parent.height) {
this.remove();
delete this;
}
},
});
/** シナリオを実行する関数
* scenario シナリオ
*/
function doScenario (scenario) {
switch (scenario.scenarioName) {
case "enemy01":
// 敵1を生成する
Enemy01().addChildTo(mainScene.group.enemyGroup);
break;
}
}
// メイン処理
phina.main(function() {
// アプリケーション生成
var app = GameApp({
startLabel: 'main', // メインシーンから開始する
// アセット読み込み
assets: ASSETS,
width: SC_WIDTH,
height: SC_HEIGHT,
});
// アプリケーション実行
app.run();
});
enemy.js
// 敵の基本クラス
phina.define('Enemy', {
// Spriteを継承
superClass: 'Sprite',
// 初期化
init: function(image, sizeX, sizeY) {
// 親クラスの初期化
this.superInit(image, sizeX, sizeY);
// プロパティの初期化
this.x = 0;
this.y = 0;
// 硬さ
this.hardness = 1;
// 弾を当てた時のスコア
this.bulletScore = 10;
// 敵のフレームカウント数
this.frameCount = 0;
},
update: function(app) {
// フレームカウントを進める
this.frameCount++;
// 動かす
this.move();
// 画面の外に出たら消す
if (this.right < 0 || this.left > mainScene.width || this.bottom < 0 || this.top > mainScene.height) {
this.remove();
delete this;
}
},
// 敵を動かすメソッド
move: undefined // 継承先で設定
,
});
/**
* 敵1クラス
* ジグザグ移動
*/
phina.define('Enemy01', {
// Enemyを継承
superClass: 'Enemy',
// 初期化
init: function() {
// 親クラスの初期化
this.superInit('enemy', 32, 32);
// プロパティの設定
this.frameIndex = 2;
this.score = 100;
this.hardness = 1;
// 移動速度を決定
this.speed = 4;
// 初期値を設定
this.x = Random.randint(0, mainScene.width);
this.y = 0;
// 横方向の速度
this.speedX = this.speed;
// 縦方向の速度
this.speedY = 2 * Random.randint(1, 3);
// モード 1:左方向、-1:右方向
this.mode = 1;
// 出現位置により移動方向を決める
if (this.x > mainScene.width / 2) {
this.speedX = -this.speed;
this.mode = -1;
}
},
// 敵を動かすメソッド
move: function () {
this.x += this.speedX;
this.y += this.speedY;
// スピードを加算する。
this.speedX += this.mode * 2;
// スピードが元の4倍を超えた場合
if (Math.abs(this.speedX) >= this.speed * 4) {
// 方向を逆転する
this.mode *= -1;
}
// 画像の決定
if (Math.abs(this.speedX) < this.speed * 2) {
// 正面
this.frameIndex = 2;
} else if (this.mode == 1) {
// 右向き
this.frameIndex = 3;
} else {
// 左向き
this.frameIndex = 1;
}
},
});