tmlib.js
bulletml.js
bulletml.js-tutorial

tmlib.jsとbulletml.jsを使って弾幕STGを作る(4)

More than 3 years have passed since last update.

弾のカスタマイズ

前回までで、bulletml.jsを使った弾幕パターンの記述についてはお分かり頂けたと思います。
今回は、実際にゲームを作成する際に必要となる処理の書き方について紹介します。

今回作成するソースコードはこちら

当たり判定

STGを作る上で絶対に必要なのが当たり判定です。
tmlib.jsではエレメント同士の衝突判定をするために便利なメソッドが用意されていますので、これを利用していきます。

まずは自機の当たり判定について、以下のように設定します。

自機クラス
tm.define("Player", {
...
    init: function() {
...
        this.boundingType = "circle"; // <-- 円形の当たり判定を持つ
        this.radius = 2; // <-- そしてその半径は2
...

自機は半径2の円形当たり判定を持つことにします。

次に敵弾の当たり判定について設定します。
createNewBullet関数の中で、生成した弾に対して以下のように設定します。

弾の当たり判定
var params = {
...
createNewBullet: function(runner) {
    var bullet = tm.bulletml.Bullet(runner).addChildTo(this);
    bullet.boundingType = "circle"; // <-- 円形の当たり判定を持つ
    bullet.radius = 2; // <-- そしてその半径は2
    bullet.onenterframe = function() { // 毎フレームの処理
        if (this.isHitElement(Player.SINGLETON)) { // 自機と衝突していれば
            this.remove(); // 弾を削除
        }
    });
...
};

弾も半径2の円形当たり判定を持つことにします。
また、onenterframeメソッドを追加し、自機との衝突をisHitElementメソッドで判定しています。
今回は、自機に当たった弾は単にシーンから削除しています。

サンプル実行

画面外に出た弾を消す

弾を次々に発射していると、ゲームは次第に遅くなっていき、最後にはフリーズしてしまいます。
これは、弾を毎回生成してはシーンに追加し、そのまま削除せずに放置しているからです。

画面外に出た弾は忘れずに消すようにしましょう。

この処理も、上記の当たり判定と同様、弾のenterframeイベント内で行います。

画面外に出た弾を消す
bullet.onenterframe = function() { // 毎フレームの処理
...
    if (this.x < 0 || 320 < this.x || this.y < 0 || 320 < this.y) { // 画面外に出た
        this.remove(); // 弾を削除
        return;
    }
};

サンプル実行

弾の見た目を変える

bulletml.jsでは、デフォルトでは赤い円形弾が発射されますが、色や形を変えて画面を派手にしたいこともあると思います。
そういう場合は、BulletML内に弾の属性値を記述し、JavaScript側で属性に応じた処理を書くことで対応します。

デフォルトの弾クラスを継承し、オリジナルの弾クラスを作る

まずは、createNewBullet関数の中で生成しているtm.bulletml.Bulletを独立したクラスにしてみましょう。

デフォルトの弾クラスを継承し、オリジナルの弾クラスを作る
createNewBullet: function(runner) {
    MyBullet(runner).addChildTo(this);
}.bind(this)

...

tm.define("MyBullet", {
    superClass: "tm.bulletml.Bullet",

    init: function(runner) {
        this.superInit(runner);

        this.boundingType = "circle"; // <-- 円形の当たり判定を持つ
        this.radius = 2; // <-- そしてその半径は2
    },

    onenterframe: function() { // 毎フレームの処理
        if (this.isHitElement(Player.SINGLETON)) { // 自機と衝突していれば
            this.remove(); // 弾を削除
            return;
        }

        if (this.x < 0 || 320 < this.x || this.y < 0 || 320 < this.y) { // 画面外に出た
            this.remove(); // 弾を削除
            return;
        }
    }
});

tm.bulletml.Bulletクラスを継承して、新しくMyBulletクラスを作りました。
当たり判定処理や画面外判定処理も新しいクラスへ移動しています。

弾のテクスチャを読み込む

次に弾の見た目をリッチにするため、以下の様な画像を用意し、これをtm.ui.LoadingSceneを使って読み込みます。
tex0.png

tm.ui.LoadingSceneで画像をロード
app.replaceScene(tm.ui.LoadingScene({
    width: 320,
    height: 320,
    assets: {
        "bullet": "tex0.png",
    },
    nextScene: MainScene
}));

弾に属性値を記述する

さて、今回は攻撃パターンを以下のようにします。

攻撃パターン
var DANMAKU0 = new bulletml.Root({
    top: action([
        repeat(999, [
            fire(speed(2.1), bullet({ color: "red" })),
            wait("$rand * 20"),
            fire(direction(180, "absolute"), speed(2.1), bullet({ color: "blue" })),
            wait("$rand * 20"),
        ]),
    ]),
});

自機狙い弾と固定弾を交互に撃つ、単純な攻撃パターンですね。
しかし、今までと違う点があります。
bullet関数の引数にオブジェクトが指定されています。

bullet({ color: "red" })
bullet({ color: "blue" })

bulletml.js DSLでは、弾に対して属性値を記述することが出来ます。
ここでは、colorプロパティとして、自機狙い弾には"red"、固定弾には"blue"という文字列を指定しました。

これまでcreateNewBullet関数では第1引数runnerのみを受け取っていましたが、BulletMLで記述した属性値は第2引数で受け取ることが出来ます。

属性値を受け取る
createNewBullet: function(runner, attr) { // <-- 第2引数attrを追加
    MyBullet(runner, attr).addChildTo(this); // <-- attrをMyBulletコンストラクタに渡す
}.bind(this)

...

tm.define("MyBullet", {
    superClass: "tm.bulletml.Bullet",

    init: function(runner, attr) { // <-- attrを受け取る
...

属性値に応じて弾の振る舞いを変える

受け取ったattrにはbullet関数の中で定義した属性値がそのまま入っています。
これを参照し、弾の振る舞いを定義します。

属性値に応じて弾の振る舞いを変える
tm.define("MyBullet", {
    superClass: "tm.bulletml.Bullet",

    init: function(runner, attr) {
        this.superInit(runner);

        this.color = attr.color;

        this.removeChildren(); // デフォルトの赤弾を削除(1)

        // colorプロパティに応じて初期化処理(2)
        switch (attr.color) {
        case "blue":
            tm.display.Sprite("bullet", 20, 20)
                .setFrameIndex(0)
                .addChildTo(this);
            break;
        case "red":
            tm.display.Sprite("bullet", 20, 20)
                .setFrameIndex(1)
                .setScale(1.5, 0.5)
                .addChildTo(this);
            break;
        }

        this.beforeX = this.x;
        this.beforeY = this.y;
...
    },

    onenterframe: function() { // 毎フレームの処理 (3)
...
        if (this.color === "blue") {
            this.rotation += 10;
        } else if (this.color === "red") {
            this.rotation = Math.atan2(this.y - this.beforeY, this.x - this.beforeX) * Math.RAD_TO_DEG;
        }

        this.beforeX = this.x;
        this.beforeY = this.y;
    },
...

まず、tm.bulletml.Bulletにはデフォルトの赤い円形弾のスプライトが入っているので、removeChildrenメソッドで子要素を削除します。(1)
次に、attr.colorの内容に応じてスプライトを生成し、子要素として追加します。(2)
onenterframeメソッドでは、青弾の場合は毎フレーム10度ずつ回転するように、赤弾の場合は直前のフレームからの移動方向により弾の姿勢が変わるように処理を記述しています。(3)

このように、BulletML内で定義した属性値によって弾の見た目や振る舞いを変えることで、カラフルで個性豊かな弾を作成することが出来るようになっています。

サンプル実行

応用例:大型戦闘機「衛掃」っぽい弾幕

「怒首領蜂最大往生」5面に登場する大型機「衛掃」の弾幕をBulletMLで再現してみましょう。

http://youtu.be/cujHIkGRT7U?t=19m40s
eiso.png

衛掃の弾幕は単なる全方位弾に見えますが、よく発射位置を見ると、なんだか回転しているような錯覚を受けます。
図解すると以下のようになっていて、これをBulletMLで再現するにはすこし工夫が必要です。

  • 砲口を中心とした円周上に発射点が並んでいる
  • それぞれの発射点からは、砲口からの直線と垂直な方向へ弾を発射する

eiso2.png

DSLはこのような仕様にしてみましょう。

  1. 砲口から全方位弾を発射。ただし、撃つ弾は透明で当たり判定も存在しない
  2. 発射された透明弾は一定時間後、進行方向に対して90度の方向に弾を発射して消える

実際に書くとこうなります。

攻撃パターン
var DANMAKU0 = new bulletml.Root({
    top: action([
        repeat(999, [
            fire(direction("$loop.index * 5", "absolute"), speed(10), bullet(actionRef("inv"), { color: "invisible" })),
            repeat(16, [
                fire(direction(360 / 16, "sequence"), speed(0, "sequence"), bullet(actionRef("inv"), { color: "invisible" })),
            ]),
            wait(25),
        ]),
    ]),

    inv: action([
        wait(1),
        fire(direction(90, "relative"), speed(1.2), bullet({ color: "red" })),
        vanish(),
    ]),
});
弾クラス
tm.define("MyBullet", {
    superClass: "tm.bulletml.Bullet",

    init: function(runner, attr) {
        this.superInit(runner);

        this.color = attr.color;

        this.removeChildren(); // デフォルトの赤弾を削除

        // colorプロパティに応じて初期化処理
        switch (attr.color) {
        case "invisible":
            // なにもしない
            break;
        case "red":
            tm.display.Sprite("bullet", 20, 20)
                .setFrameIndex(1)
                .addChildTo(this);
            break;
        }

        this.boundingType = "circle"; // <-- 円形の当たり判定を持つ
        this.radius = 7; // <-- そしてその半径は2
    },

    onenterframe: function() { // 毎フレームの処理

        if (this.color !== "invisible") { // 見えない弾は自機と衝突しない

            if (this.isHitElement(Player.SINGLETON)) { // 自機と衝突していれば
                this.remove(); // 弾を削除
                return;
            }

        }

        if (this.x < 0 || 320 < this.x || this.y < 0 || 320 < this.y) { // 画面外に出た
            this.remove(); // 弾を削除
            return;
        }

        this.rotation += 10;
    }
});

サンプル実行

まとめ

今回は最低限STGを作る上で必要になる処理の書き方について紹介しました。

次回はもう一歩踏み込んだ内容について解説したいと思います。

今回作成したソースコードはこちら

JSSTG 夏のJavaScriptシューティングゲーム祭り 開催中!

突然ですが、宣伝です。

JSSTG 夏のJavaScriptシューティングゲーム祭り 開催中です!

応募要項は至って単純。「JavaScript製のシューティングゲームであればなんでもOK!」です。
締め切りは2014年8月31日を予定。
応募方法などは公式サイトをご覧になるか、Twitterにてハッシュタグ「#jsstg」をつけて質問してください。

本シリーズを読んでtmlib.js&bulletml.js製の弾幕STGを作ってみようと思った方、ぜひご応募を!
熱いSTG 待ってます!!!