のらぬこの日常を描く

ノージャンルのお役立ち情報やアニメとゲームの話、ソフトウェア開発に関する話などを中心としたブログです。

Re:ゼロから始める弾幕アルゴリズム ~ Unityで作る弾幕STG

こんにちは、のらぬこです。

先日、Unity入門書に掲載されているSTGに色々手を加えて遊んでみた話を書きました。

noranuk0.hatenablog.com

今回は、その続として、東方などの弾幕シューティングゲームで登場するような弾幕をあれこれ作ってみたお話です。

Unity使えば、当たり判定は自動で行ってくれる(オブジェクト同士が衝突したら、コールバックメソッドが自動で呼び出される)ため、衝突判定のロジックは不要です。

なので、弾幕作る場合も、弾道の数式をコード化すればお仕事完了っていうのが便利ですね。

ちなみに、Unityの場合、重力以外の力でオブジェクトをあれこれ動かす場合や、衝突時に慣性の法則以外の挙動を与えたい場合には、オブジェクトにC#コードをアタッチして動きや衝突時の動作をC#で書くことになっています*1

ということで、弾の動きはC#コードで制御していくことになります。

なお、以下に掲載している画像内のキャラクターは以下の書籍のサンプルデータに含まれる画像を使用しております。

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

全方位弾

記事書いておいてあれですが、弾幕げーとか東方とか全然詳しくないので、この言い方でいいのかわからないですが、敵の周り全方位に同時にばらまかれるタイプの弾幕です。 f:id:noranuk0:20161029222814p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update1()
    {
        float baseDir = GetAim();
        int count = 0;
        while (true)
        {
            // 8秒毎に、間隔6度、速度1で敵キャラを中心として全方位弾発射。
            for (int rad = 0; rad < 360; rad += 6)
            {
                Bullet.Add(transform.position.x, transform.position.y, rad, 1);
            }
            yield return new WaitForSeconds(8.0f);
            count++;
        }
    }
}

ワインダー

弾幕の檻に閉じ込められて移動範囲がめちゃ制限されるやつです。
自機狙いの3Way弾とかとセットで使う感じでしょうか。

f:id:noranuk0:20161029225404p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    // 敵キャラと自キャラの位置関係(角度)を取得
    public float GetAim()
    {
        float dx = target.X - transform.position.x;
        float dy = target.Y - transform.position.y;
        return Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
    }

    IEnumerator _Update2()
    {
        float baseDirection = GetAim();
        int count = 0;
        while (true)
        {
            // 檻の中心角度を設定。最初の自キャラ角度を中心として+30度~-30度の範囲をゆらゆら動かす
            float dir = baseDirection + Mathf.Sin(count * Mathf.Deg2Rad) * 30.0f;

            // 偶数弾を撃つ感じで、自キャラ角度を外して上下3本づつ0.05秒毎に弾発射
            for (int index = -3; index < 3; index++)
            {
                Bullet.Add(transform.position.x, transform.position.y, dir + index * 30 + 15, 3);
            }
            yield return new WaitForSeconds(0.05f);
            count++;
        }
    }
}

うずまき弾

ロールケーキみたいな渦巻きを描くような弾幕です。

f:id:noranuk0:20161029230424p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update3()
    {
        int rad = 0;
        while (true)
        {
            Bullet.Add(0, null, 0, transform.position.x, transform.position.y, rad, 1);
            rad += 8;
            yield return new WaitForSeconds(0.05f);
        }
    }
}

切り返し使って避けるやつ

自キャラ狙いの奇数弾が割りと休みなく襲って来るやつです。
ソースは、画面端で切り返し*2できるように、10発撃ったら0.2秒ほどディレイを入れて弾間隔をあけています。

下の図のように、これ単体だと画面端からそのまま大きく敵の後ろに回れば、切り返し不要で避けられるので、例えば画面右半分レーザーで覆うとかして移動範囲狭めたほうが良いかと。

f:id:noranuk0:20161029230649p:plain

public class Bullet : MonoBehaviour
{
    public static Bullet Add(float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        return bullet;
    }
}

public class Enemy : MonoBehaviour
{
    // 敵キャラと自キャラの位置関係(角度)を取得
    public float GetAim()
    {
        float dx = target.X - transform.position.x;
        float dy = target.Y - transform.position.y;
        return Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
    }

    IEnumerator _Update4()
    {
        while (true)
        {
            float rad = GetAim();
            for (int index = 0; index < 10; index++)
            {
                Bullet.Add(0, null, 0, transform.position.x, transform.position.y, rad, 2);
                yield return new WaitForSeconds(0.05f);
            }
            yield return new WaitForSeconds(0.2f);
        }
    }
}

永夜叉の2面?ボスが使ってたようなやつ

これにどういう名前がついていて、どう説明すればいいかわからないので、とりあえず下の図を見てください。 f:id:noranuk0:20161029232303p:plain

青い弾は時計回り、赤い玉は反時計回りに回転しつつ、円が徐々に大きくなっていく弾幕です。

この弾幕はこれまでと少し実装方法が異なります。
これまでの弾幕は、弾は直進していましたが、この弾幕の弾の軌跡は曲線を描きます。 f:id:noranuk0:20161029232546p:plain

これまで紹介した4つの弾幕は、敵が弾を発射するときに位置、初速度、発射角を指定すれば、あとはUnityが面倒見てくれたのですが、今回は、弾クラスで弾位置を自前で更新しないと駄目です、多分。
ある式で与えられた曲線状を等速直線運動せよ、みたいなこともUnity標準で書けるのかもしれませんが、まだ修行中の身ゆえそういうところまでは調べきれていないです。
てゆか、この曲線が数式的にはどういう式になるのかもよくわからん。

ということで、この実装方針でいいのか微妙に疑問に思いつつコードです。

/// 敵弾
public class Bullet : MonoBehaviour
{
    // 0: 直進
    // 1: 右回転
    // 2: 左回転
    public int type = 0;
    public MonoBehaviour center;
    protected float rad = 0;
    protected float dist = 0;

    public Sprite spriteBlue = null;
    public Sprite spriteRed = null;

    public static Bullet Add(int type, MonoBehaviour center, int rad, float x, float y, float direction, float speed)
    {
        Bullet bullet = ...
        if (bullet != null) {
            bullet.type = type;
            bullet.center = center;
            bullet.rad = rad;
            bullet.dist = 0;
            if (type == 1)
            {
                bullet.gameObject.GetComponent<SpriteRenderer>().sprite = bullet.spriteBlue;
            } else if (type == 2)
            {
                bullet.gameObject.GetComponent<SpriteRenderer>().sprite = bullet.spriteRed;
            }
            }
        return bullet;
    }

    void FixedUpdate()
    {
        if (type != 0)
        {
            float x = center.transform.position.x + Mathf.Sin(rad * Mathf.Deg2Rad) * dist;
            float y = center.transform.position.y + Mathf.Cos(rad * Mathf.Deg2Rad) * dist;
            Vector3 pos = transform.position;
            pos.Set(x, y, 0);
            transform.position = pos;
            if (type == 1)
            {
                rad += 0.2f;
            } else if (type == 2)
            {
                rad -= 0.2f;
            }
        }
        dist += 0.01f;
    }
}

public class Enemy : MonoBehaviour
{
    IEnumerator _Update5()
    {
        while (true)
        {
            for (int rad = 0; rad < 360; rad += 15)
            {
                Bullet.Add(1, this, rad, transform.position.x, transform.position.y, 0, 1);
                Bullet.Add(2, this, rad, transform.position.x, transform.position.y, 0, 1);
            }
            yield return new WaitForSeconds(2.0f);
        }
    }
}

参考書籍の紹介

最後に、この記事を書く少し前からUnityの学習を始めたのですが、僕が参考にした書籍の紹介をしたいと思います。

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

Unity4.6/5.0でつくる 2Dゲーム制作入門 [改訂第二版]

kindle版の電子書籍ですが、お値段500円、紙の本の約1/5ほどの値段で購入できます。個人の方が書かれた書籍で、値段も安いですが、サンプルを作りながら覚えていくタイプの入門書としてはとても良い本だと思います。

内容は、ミニゲーム(マウスクリックで敵を消すゲーム)、ボス戦オンリー(?)のシューティングゲーム、動く床や当たると死ぬ罠が配置されたステージをクリアしていくアクションゲーム、の3つのゲームを書籍の手順に従って実際にUnityに触れながら作り上げていくような構成になっています。

スマホ対応についての記述は無いので、すべての操作をタップでどうにかするようなゲームを作るとなると、別途Webで調べるなり別の書籍を漁るなど必要になってくると思いますが、とりあえず何かゲームを作りながらあれこれ覚えていくというスタンスでしたらとてもわかり易い本だと思います。

さらに、ツールの操作も図付きで操作を1つ1つ説明しているため、途中で躓くことも無いと思います。

書籍のレビューに、「作者が作った独自の基底クラスを継承させて、その独自クラスのメソッドやプロパティを多用している」との指摘もありますが、そのクラス自体はUnityAPIの薄いラッパークラスとなっており、オブジェクトの生ハンドルの隠蔽や、使用しない引数を省略するためのラッパーメソッドが定義されているくらいです。ソースコードも提供されているため、Unity本来のAPIではどのように記述するかという点についても、C#でのメソッド呼び出し、基本的な演算子、条件分岐くらいのコードが読めれば容易に理解できると思います。

なお、画像リソースや完成済のUnityプロジェクトもWebからダウンロードできるようになっているため、絵が描けないのでゲーム用の画像用意するところでまず躓いてしまうという心配もなく、また、とりあえず出来上がっているコードをあれこれ触ってみたいという方にもおすすめです。

ちなみに、書籍の作者さまが書かれたチュートリアル(書籍の第一章に相当する内容です)が、Qiitaにも公開されています。

qiita.com

また、いろいろな本を読み漁ってみたいという方は、kindle unlimited に加入するのも割とお勧めです。

kindle unlimited は、月額980円(2017年7月現在)のみで、kindle unlimitedの対象書籍を無制限に読むことができるサービスです。

対象書籍のラインナップも、上で紹介した書籍も含め、個人で出版された書籍や市販の技術書等、結構豊富に取り揃えられています。
月額980円ですが初月は無料なので、お試しでひと月だけ加入して、ずっと持っておきたい本だけ購入するみたいな使い方もありかと思います。

kindle white paper 等の kindle端末を持っていなくても、androidiOS端末をお持ちであれば、アプリ版のkindleから利用することもできます。

もし興味ございましたら、kindle unlimited の対象書籍を幾つかリストアップしておきますので、ご参考までにどうぞってことで。

kindle unlimited の申込みは下記リンクから行えます

Kindle Unlimited会員登録

もしご興味持っていただけたら、検討してみてはいかがでしょうか?

最後までお読みいただいてありがとうございました。

*1:他にもやり方あるのかもしれませんが、未だ勉強中のため、他のやり方は知らんです

*2:画面端に到達したら大きく避けて弾幕の隙間から反対側に避難