GoogleAdsence

凍符「パーフェクトフリーズ」

2020年1月16日

今回の弾幕は、東方紅魔郷 お馴染みのチルノスペルカードの凍符「パーフェクトフリーズ」です。

それとボスの移動と弾幕の制御部分を切り分けました。

ボスの弾幕生成クラス

CBossBulletFactory.csというファイルを新規作成して以下のコードを追加。

public delegate int MOVE_BOSS_POS(double x1, double y1, double x2, double y2, double dist, int t);
public class CBossBulletFactory
{
    protected MOVE_BOSS_POS MoveBossPos;
    protected int Cnt = 0;
    protected double BaseAngle = 0.0;
    public CBossBulletFactory(MOVE_BOSS_POS move_boss_pos)
    {
        MoveBossPos = move_boss_pos;
    }
    public virtual void Run(double x, double y) { }
}

基底クラスであるCBossBulletFactoryを派生したクラスが弾幕の制御をするクラスです。
Run関数を呼ぶことによって弾幕を生成します。
ただ弾幕の計算のとボスの移動も制御しなければならないので、デリゲートでMOVE_BOSS_POSを宣言し、弾幕クラスを生成する時にMoveBossPos関数を受け取ります。
これによってボスを制御するクラスは非常に見やすくなりました。

public class CBoss : CMover
{
    public bool DeathFlag { set; get; } = false;
    public bool MutekiFlag{ set; get;} = false;
    protected CPhysics Phy = new CPhysics();
    public CBoss(double x, double y) : base(x, y,1, -5.0, -5.0, 5.0, 5.0)
    {

    }
    public virtual void Move() { }
    public virtual void Draw() { }
    //今いる位置からdist離れた位置にtカウントで移動する
    public int MoveBossPos(
            double x1, double y1, double x2, double y2, double dist, int t)
    {
        int i = 0;
        double x, y, angle;
        for (i = 0; i < 1000; i++)
        {
            x = X;//今のボスの位置をセット
            y = Y;
            angle = SGP.Rang(PI);//適当に向かう方向を決める
            x += Math.Cos(angle) * dist;//そちらに移動させる
            y += Math.Sin(angle) * dist;
            if (x1 <= x && x <= x2 && y1 <= y && y <= y2)
            {
                //その点が移動可能範囲なら
                //input_phy_pos(x, y, t);
                // x1 初期X座標 y1 初期Y座標 x2 目標X座標 y2 目標Y座標かける時間
                if (Phy != null)
                    Phy.InputPhyPos(X, Y, x, y, t);
                return 0;
            }
        }
        return -1;//1000回試してダメならエラー
    }
}

public class CBoss1 : CBoss
{
    CBossBulletFactory BossBulletFactory;
    int MaxLife = 650;
    int State = 0;      

    public CBoss1(double x, double y) : base(x, y)
    { }
    public override void Move()
    {
        switch (State)
        {
            case 0: // 移動登録
                Life = 1;
                MutekiFlag = true;
                if (Phy != null)
                {
                    Phy.InputPhyPos(X, Y, 240, 170, 90);
                    State = 1;
                }
                break;
            case 1: // 移動
                if (!Phy.Flag)
                {
                    Cnt = 0;
                    State = 2;
                }
                break;
            case 2:
                Life += 4;
                if (Life >= MaxLife)
                {
                    Life = MaxLife;
                    MutekiFlag = false;
                    State = 3;
                    BossBulletFactory = new CPerfectFreeze(MoveBossPos);
                }
                break;
            case 3:
                BossBulletFactory.Run(X, Y);
				break;
		}
	}
}

弾幕を74行目で生成し、78行目で計算してます。別の種類の弾幕にしたい時は74行目を入れ替えるだけで済む様になりました。非常に簡単ですよね。この様にクラスを細かく設計してやると、余計な変数に触らなくてよくなるので、バグの可能性が少なくるし、発生したとしても特定しやすくなります。

凍符「パーフェクトフリーズ」

では、実際のパーフェクトフリーズのプログラムを見てみましょう。

public class CPerfectFreeze : CBossBulletFactory
{
    public CPerfectFreeze(MOVE_BOSS_POS move_boss_pos) : base(move_boss_pos)
    {

    }
    public override void Run(double x, double y)
    {
        const int TM002 = 650;
        int t = Cnt % TM002;
        double angle;
        if (t == 0 || t == 210)
        {
            //40<x<FMX-40  50<y<150 の範囲で100離れた位置に80カウントで移動する
            MoveBossPos(40, 50, FIELD_MAX_X - 40, 150, 100, 80);
        }
        //最初のランダム発射
        if (t < 180)
        {
            //1カウントに2回発射
            for (int i = 0; i < 2; i++)
            {
                SGP.BulletBuf.Add(new CBullet(x: x, y: y,
                    speed: 3.2 + CUtility.Rang(2.1),
                    angle: CUtility.Rang(PI2 / 20) + PI2 / 10 * t,
                    graph_type: 7, color: DX.GetRand(6), state: 0, pi2or1: 1.0, se_flag: t % 10 == 0, kaiten:1));
            }
        }
        //自機依存による8方向発射
        if (210 < t && t < 270 && t % 3 == 0)
        {
            angle = SGP.GetPlayerAnglePI2(x, y);
            for (int i = 0; i < 8; i++)
            {
                SGP.BulletBuf.Add(new CBullet(x: x, y: y,
                    speed: 3.0 + CUtility.Rang(0.3),
                    //自機とボスとの成す角を基準に8方向に発射する
                    angle: angle - PI / 2 * 0.8 + PI * 0.8 / 7 * i + CUtility.Rang(PI / 180),
                    graph_type: 7, color: 0, state: 2, pi2or1: 1.0, se_flag: t % 10 == 0, kaiten: 1));
            }

        }
        for(int i = 0; i < SGP.Bullet.Count; ++i)
        {
            //tが190の時に全てストップさせ、白くし、カウントリセット
            if (SGP.Bullet[i].State == 0)
            {
                if(t == 190)
                {
                    SGP.Bullet[i].Kaiten = 0;//弾の回転を止める
                    SGP.Bullet[i].Speed = 0.0;
                    SGP.Bullet[i].Color = 0;
                    SGP.Bullet[i].Cnt = 0;
                    SGP.Bullet[i].State = 1;
                }
            }
            //ランダムな方向に移動し始める
            if (SGP.Bullet[i].State == 1)
            {
                if (SGP.Bullet[i].Cnt == 200)
                {
                    SGP.Bullet[i].Angle = CUtility.Rang(PI);
                    SGP.Bullet[i].Kaiten = 1;
                }
                if (SGP.Bullet[i].Cnt > 200)
                    SGP.Bullet[i].Speed += 0.01;
            }
        }
        ++Cnt;
    }
}

この弾幕は1周期650カウントです。アルゴリズムとしては

  • 初めの180カウント未満まで、 全方向ばら撒きショットをうつ
  • 190カウントの時に全てストップさせ、白くし、カウントリセット
  • 200カウントで全方向で再び動き始める

均等にランダムにするための工夫

360°のランダムな方向の生成ならCUtility.Rang(PI);でよいのですが、均等なランダムにならない可能性があります。そこで CUtility.R ang(PI2/20)+PI2/10*tと書いてます。これは

  1. PI2/10*tが1の時は36度を基準に+-PI2/20度つまり+-16度
  2. PI2/10*tが2の時は72度を基準に+-16度
  3. PI2/10*tが3の時は108度を基準に+-16度
  4. PI2/10*tが4の時は144度を基準に+-16度
  5. PI2/10*tが5の時は180度を基準に+-16度
  6. PI2/10*tが6の時は216度を基準に+-16度
  7. PI2/10*tが7の時は252度を基準に+-16度
  8. PI2/10*tが8の時は288度を基準に+-16度
  9. PI2/10*tが9の時は324度を基準に+-16度
  10. PI2/10*tが10の時は360度を基準に+-16度

と10カウント(約1/6秒)かけて1周します。これによってどこか1つの方向に偏らないような乱数を発生させることができます。