no-image

JavaScriptで弾幕STGをフルスクラッチで作る その3 弾幕編

前々回の記事ゲームエンジンを作り、前回の記事ゲームを作りました。ここで終わって「あとはみなさんの好きなように作ってください!」としてもよかったのですが、少し味気ない気がしたので、あと1つだけ記事を書くことにしました。

今回するのは弾幕の制作です。前回作ったのはお世辞にも弾幕とは言えない何かだったので、いくつか弾幕を作ってみましょう。

対象となるブラウザはChrome51以降です

回転弾幕

danamku-stg-6

よくある回転しながら放射状に射出される弾幕です。これは簡単に作れます。

まずわかりやすくするためにEnemyを動かないようにしておきましょう。移動する部分を削除するだけで結構です。

回転弾幕の作り方は簡単です。前回作った放射状に弾を発射する動作を、少しずつ角度を変えながら行うだけです。Enemyクラスを編集しましょう。

class Enemy extends SpriteActor {
    constructor(x, y) {
        const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 0, 16, 16));
        const hitArea = new Rectangle(0, 0, 16, 16);
        super(x, y, sprite, hitArea, ['enemy']);

        this.maxHp = 50;
        this.currentHp = this.maxHp;

        this._interval = 10;
        this._timeCount = 0;
        this._count = 0;

        // プレイヤーの弾に当たったらHPを減らす
        this.addEventListener('hit', (e) => {
           if(e.target.hasTag('playerBullet')) {
               this.currentHp--;
               this.dispatchEvent('changehp', new GameEvent(this));
           }
        });
    }

    // degree度の方向にspeedの速さで弾を発射する
    shootBullet(degree, speed) {
        const rad = degree / 180 * Math.PI;
        const velocityX = Math.cos(rad) * speed;
        const velocityY = Math.sin(rad) * speed;
        
        const bullet = new EnemyBullet(this.x, this.y, velocityX, velocityY);
        this.spawnActor(bullet);
    }

    // num個の弾を円形に発射する
    shootCircularBullets(num, speed, initialDegree) {
        const degree = 360 / num;
        for(let i = 0; i < num; i++) {
            this.shootBullet(initialDegree + degree * i, speed);
        }
    }

    update(gameInfo, input) {
        // インターバルを経過していたら弾を撃つ
        this._timeCount++;
        if(this._timeCount > this._interval) {
            this._count += 10;
            this.shootCircularBullets(10, 1, this._count);
            this._timeCount = 0;
        }

        // HPがゼロになったらdestroyする
        if(this.currentHp <= 0) {
            this.destroy();
        }
    }
}

次にshootCicularBulletsメソッドに少し変更を加えて、新たな引数initialDegreeを受け取るようにします。そしてinitialDegreeだけ傾けて発射するように変更します。

updateメソッド内では、新たに追加した変数_countを弾を発射するごとに加算するようにします。これを角度としてshootCircularBulletsメソッドに渡します。すると少しずつ角度を変えながら弾が発射されるようになります。これで完成です。

動作確認

動作確認をしてみましょう。今までと同じくスペースキーで開始します。

danamku-stg-6

デモを開く

うずまき弾幕

danmaku-stg-7

うずまき状に弾を並べた後、一斉にランダム動き出す弾幕です。

こういった複雑な動きをするものを作る時はEnemyから直接生成するよりも、弾幕を生成するクラスを作って、そのクラスのオブジェクトをEnemyから生成するほうが綺麗になります。

まずはEnemyBulletクラスを編集して、弾を停止する仕組みを作りましょう。isFrozenプロパティを追加して、isFrozenプロパティがfalseのときだけ動くように変更します。

class EnemyBullet extends SpriteActor {
    constructor(x, y, velocityX, velocityY, isFrozen = false) {
        const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 16, 16, 16));
        const hitArea = new Rectangle(4, 4, 8, 8);
        super(x, y, sprite, hitArea, ['enemyBullet']);

        this.velocityX = velocityX;
        this.velocityY = velocityY;
        this.isFrozen = isFrozen;
    }

    update(gameInfo, input) {
        if(!this.isFrozen) {
            this.x += this.velocityX;
            this.y += this.velocityY;
        }

        if(this.isOutOfBounds(gameInfo.screenRectangle)) {
            this.destroy();
        }
    }
}

次に弾をうずまき状に生成するSpiralBulletsSpawnerクラスを作りましょう。

class SpiralBulletsSpawner extends Actor {
    constructor(x, y, rotations) {
        const hitArea = new Rectangle(0, 0, 0, 0);
        super(x, y, hitArea);

        this._rotations = rotations;
        this._interval = 2;
        this._timeCount = 0;
        this._angle = 0;
        this._radius = 10;
        this._bullets = [];
    }

    update(gameInfo, input) {
        // 指定回数回転したらやめる
        const rotation = this._angle / 360;
        if(rotation >= this._rotations) {
            this._bullets.forEach((b) => b.isFrozen = false); // 凍結解除
            this.destroy();
            return;
        }

        // インターバル経過までは何もしない
        this._timeCount ++;
        if(this._timeCount < this._interval) { return;}
        this._timeCount = 0;
        
        // 角度と半径を増加させていく
        this._angle += 10;
        this._radius += 1;

        // 弾を発射する
        const rad = this._angle / 180 * Math.PI;
        const bX = this.x + Math.cos(rad) * this._radius;
        const bY = this.y + Math.sin(rad) * this._radius;
        const bSpdX = Math.random() * 2 - 1; // -1〜+1
        const bSpdY = Math.random() * 2 - 1;
        const bullet = new EnemyBullet(bX, bY, bSpdX, bSpdY, true);
        this._bullets.push(bullet);

        this.spawnActor(bullet);
    }
}

SpiralBulletsSpawnerクラスの処理は簡単です。角度と半径を徐々に増加させていき、ランダムな速度を持つ弾を配置します。配置した弾は最初は凍結されています。指定した回数だけ回転したら、全ての弾の凍結を解除して、自身を破棄します。

このクラスのオブジェクトをEnemyクラスから生成するようにします。また、不要なメソッド等は削除しておきましょう(残しておいてもかまいません)。

class Enemy extends SpriteActor {
    constructor(x, y) {
        const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 0, 16, 16));
        const hitArea = new Rectangle(0, 0, 16, 16);
        super(x, y, sprite, hitArea, ['enemy']);

        this.maxHp = 50;
        this.currentHp = this.maxHp;
        
        this._interval = 500;
        this._timeCount = this._interval;

        // プレイヤーの弾に当たったらHPを減らす
        this.addEventListener('hit', (e) => {
           if(e.target.hasTag('playerBullet')) {
               this.currentHp--;
               this.dispatchEvent('changehp', new GameEvent(this));
           }
        });
    }

    update(gameInfo, input) {
        // インターバルを経過していたら弾を撃つ
        this._timeCount++;
        if(this._timeCount > this._interval) {
            const spawner = new SpiralBulletsSpawner(this.x, this.y, 4);
            this.spawnActor(spawner);
            this._timeCount = 0;
        }

        // HPがゼロになったらdestroyする
        if(this.currentHp <= 0) {
            this.destroy();
        }
    }
}

うずまき弾幕が何重にも重なるといけないので、インターバルを長めにとります。あとは今までと同じようにオブジェクトを生成してspawnActorするだけです。回転数は4回転にしておきます。

動作確認

danmaku-stg-7

デモを開く

花火弾幕

danamku-stg-8

花火弾幕はひとつの弾がはじけて複数の弾に分裂する弾幕です。

これは簡単に作ることができます。EnemyBulletを継承して、分裂するFireworksBulletを作りましょう。

class FireworksBullet extends EnemyBullet {
    constructor(x, y, velocityX, velocityY, explosionTime) {
        super(x, y, velocityX, velocityY);

        this._eplasedTime = 0;
        this.explosionTime = explosionTime;
    }

    // degree度の方向にspeedの速さで弾を発射する
    shootBullet(degree, speed) {
        const rad = degree / 180 * Math.PI;
        const velocityX = Math.cos(rad) * speed;
        const velocityY = Math.sin(rad) * speed;

        const bullet = new EnemyBullet(this.x, this.y, velocityX, velocityY);
        this.spawnActor(bullet);
    }

    // num個の弾を円形に発射する
    shootCircularBullets(num, speed) {
        const degree = 360 / num;
        for(let i = 0; i < num; i++) {
            this.shootBullet(degree * i, speed);
        }
    }

    update(gameInfo, input) {
        super.update(gameInfo, input);

        // 経過時間を記録する
        this._eplasedTime++;
        
        // 爆発時間を超えたら弾を生成して自身を破棄する
        if(this._eplasedTime > this.explosionTime) {
            this.shootCircularBullets(10, 2);
            this.destroy();
        }
    }
}

放射状に弾を発射する部分はEnemyクラスの使い回しです。あとは経過時間を記録して、一定時間を経過したら弾を生成し、自身を破棄するだけです。

これをEnemyクラスから生成します。

class Enemy extends SpriteActor {
    constructor(x, y) {
        const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 0, 16, 16));
        const hitArea = new Rectangle(0, 0, 16, 16);
        super(x, y, sprite, hitArea, ['enemy']);

        this.maxHp = 50;
        this.currentHp = this.maxHp;
        
        this._interval = 100;
        this._timeCount = 0;

        // プレイヤーの弾に当たったらHPを減らす
        this.addEventListener('hit', (e) => {
           if(e.target.hasTag('playerBullet')) {
               this.currentHp--;
               this.dispatchEvent('changehp', new GameEvent(this));
           }
        });
    }

    update(gameInfo, input) {
        // インターバルを経過していたら弾を撃つ
        this._timeCount++;
        if(this._timeCount > this._interval) {
            const spdX = Math.random() * 4 - 2; // -2〜+2
            const spdY = Math.random() * 4 - 2;
            const explosionTime = 50;
            const bullet = new FireworksBullet(this.x, this.y, spdX, spdY, explosionTime);
            this.spawnActor(bullet);
            this._timeCount = 0;
        }

        // HPがゼロになったらdestroyする
        if(this.currentHp <= 0) {
            this.destroy();
        }
    }
}

動作確認

danamku-stg-8

デモを開く

おわりに

全3回の連載になりましたが、これで終わりです。いかがだったでしょうか。

シューティングゲームというのは他のゲームより比較的簡単に作ることができるので、ゲーム制作やプログラミングの学習には最適だと思います。私自身もこの連載記事を書く中でいくつか学ばされたことがあります。

ネット上には数多くのシューティングゲーム制作講座がありますが、「簡単なフレームワークを作ってその上でゲームを作る」という形式のものは珍しいと思います。現実にはゲーム制作は何かしらのゲームエンジン(内製・外製問わず)を使って行われるものなので、ある程度実情に即したものになっているはずです。

エフェクトの管理や音の鳴らし方など、まだまだ書けることはたくさんあるのですが、これ以上書くとなるとプログラミングはあまり関係なくゲーム制作論的なものになってしまい、私の目的とするところではなくなってしまうので、ここらへんで終わりにさせていただきます。

全3回の連載、お付き合いいただきありがとうございました。