アマゾンバナーリンク

弾幕制作 月符 「サイレントセレナ」と禁忌「恋の迷路」

前回で講座自体は終わりましたがゲームは完成してないので、まだまだ作業は続きます。今回の成果は、ボス用の2D背景と3D背景とボスの弾幕をつくりました。 パチュリー・ノーレッジのスペルカード月符 「サイレントセレナ」フランドール・スカーレットのスペルカード 禁忌「恋の迷路」 です。

月符 「サイレントセレナ」

この弾幕を生成する部分のソースコードは以下の様になります。

// サイレントセレナ
void SilentSerena()
{
    const int TM001 = 60;

    int t = Cnt % TM001;
    int cnt = Cnt % 400;
    
    if (t == 0)
    {
        if (move_cnt == 1)
            MoveBossPos(20, 30, FIELD_MAX_X - 20, 120, 60, 60);
        BaseAngle = SGP.GetPlayerAnglePI2(X, Y);//基準と成る角度をセット
    }
    // 周期の半分で角度を変える
    if (t == TM001 / 2 - 1)
    {
        BaseAngle += PI / 20;
    }
    // -PIからPIまでの範囲に補正する
    //while( BaseAngle < -PI ) BaseAngle += PI2;
    //while( BaseAngle >= PI ) BaseAngle -= PI2;
    if (t % 6 == 0)
    {
        for (int i = 0; i < 20; i++)
        {
            SGP.BulletBuf.Add(new CBullet(x:X, y:Y, angle:BaseAngle + PI2 / 20 * i, speed:2.7, graph_type:8, color:4,pi2or1:1.0));
        }
    }

    if (t % 4 == 0)
    {
        SGP.BulletBuf.Add(new CBullet(
            x:DX.GetRand(FIELD_MAX_X), y:DX.GetRand(200), 
            angle:PI / 2, speed:1.0 + SGP.Rang(0.5), graph_type:8, color:0, pi2or1:1.0));
    }

    if (0 <= cnt && cnt <= 120 && cnt % 240 == 1)
    {
        for (int s = 0; s <= 150; s++)
        {
            if (s % 6 < 4) continue;
            SGP.BulletBuf.Add(
                new CRainbowBullet(
                    x:X, y:Y, vx:-2.5f + 5.0f / 150 * s,
                    vy:-8.0, vvy:0.2, graph_type:2, color:(s / 6) % 9, pi2or1:1.0));
        }
    }
    if (t == TM001 - 1)
    {
        move_cnt = (move_cnt + 1) % 4;
    }
}

やってることは簡単で以下の処理を400カウント1周期として繰り返してるだけです。本家では最後の虹の弾幕はなくてオリジナルで付け足しました。

  • 60カウント周期にSGP.GetPlayerAnglePI2でプレイヤーとボスとのなす角を計算
  • 60カウント周期の更に4周期に1回ボスを移動させる
  • 周期の半分でPI/20だけ角度を変える。(180/20なのでおよそ9度)
  • 6カウントに1度、360度に20方向に均等に弾幕を打つ
  • 4カウントに1度、PI/2(真下方向)に向けて雨粒の弾幕を生成
  • この弾幕全体が400カウント周期でその始めの120カウントなおかつ240カウント以内なら虹の弾幕を生成

禁忌「恋の迷路」

この弾幕を生成する部分のソースコードは以下の様になります。

double BaseAngle2 = 0.0;
int tcnt=0, cnt=0, cnum=0;
//恋の迷路
void KokinoMeiro()
{
    const int TM003 = 600;
    const int DF003 = 20; 
    int i, j, k, t = Cnt % TM003, t2 = Cnt;
    
    double angle;
    if (t2 == 0)
    {//最初なら
     //40<x<FMX-40  50<y<150 の範囲で100離れた位置に80カウントで移動する
        MoveBossPos(X, Y, FIELD_MAX_X /2, FIELD_MAX_Y/2, 60, 50);
        cnum = 0;
    }
    if (t == 0)
    {//1周期の最初なら
        BaseAngle = SGP.GetPlayerAnglePI2(X, Y);//基準と成る角度をセット
        cnt = 0;
        tcnt = 2;
    }
    if (t < 540 && t % 3 != 0)
    {
        BaseAngle = SGP.GetPlayerAnglePI2(X, Y);//基準と成る角度をセット
        //撃たない方向なら撃たない
        if (tcnt - 2 == cnt || tcnt - 1 == cnt)
        {
            if (tcnt - 1 == cnt)
            {
                //ベースとなる角度をセット
                BaseAngle2 = BaseAngle + PI2 / DF003 * cnt * (cnum!=0 ? -1 : 1) - PI2 / (DF003 * 6) * 3;
                tcnt += DF003 - 2;
            }
        }
        //それじゃなければうつ
        else
        {
            for (i = 0; i < 6; i++)
            {//1回に6発ずつうつ
                angle = BaseAngle + PI2 / DF003 * cnt * (cnum != 0 ? -1 : 1) + PI2 / (DF003 * 6) * i * (cnum != 0 ? -1 : 1);
                SGP.BulletBuf.Add(new CBullet(x: X, y: Y, angle: angle, speed: 2, graph_type: 8, color: cnum != 0 ? 1 : 4, pi2or1: 1.0));
            }
        }
        cnt++;
    }
    //少し大きな弾で円形発射
    if (40 < t && t < 540 && t % 30 == 0)
    {
        for (j = 0; j < 3; j++)
        {
            angle = BaseAngle2 - PI2 / 36 * 4;
            for (i = 0; i < 27; i++)
            {
                SGP.BulletBuf.Add(new CBullet(x: X, y: Y, angle: angle, speed: 4 - 1.6 / 3 * j, graph_type: 7, color: cnum != 0 ? 6 : 0, pi2or1:
                angle -= PI2 / 36;
            }
        }
    }
    if (t == TM003 - 1)
        cnum++;
}

この弾幕は処理を600カウントを1周期として以下の処理をしています。

  • 周期の一番初めに画面の中央にボスを移動
  • SGP.GetPlayerAnglePI2でプレイヤーとボスのなす角度を計算
  • 回転方向をカウントしていき、1周分-2か1周分-1なら発射しない
  • それ以外は1回に6ずつ弾幕を打つ
  • 大きな弾は、40カウント以上、530カウント未満で30カウントに1回、PI2/36(10度)ずつずしながら27方向に打つのを3回繰り返します。

それと今回からなす角度を計算する仕様を変えました。

private double GetMoverAngle(List<CMover> mover, double x, double y, bool pi2_flag)
{
    // 一番近くのmoverの要素を取得する
    double min = -1;
    int num = 0;
    for (int i = 0; i < mover.Count; ++i)
    {
        double dist = (mover[i].X - x) * (mover[i].X - x) + (mover[i].Y - y) * (mover[i].Y - y);
        if (min == -1 || dist < min)
        {
            min = dist;
            num = i;
        }
    }
    return min == -1 ? 0.75 : Math.Atan2(mover[num].Y - y, mover[num].X - x) / (pi2_flag ? PI2 : 1.0);
}
// SGP.GetPlayerAnglePI2を使った時にはCBulletの引数pi2or1に1.0を入れる事
// GetPlayerAngle1を使った時にはCBulletの引数pi2or1にPI2を入れる事
// プレイヤーまでの角度を計算する(0~PI2の間で値が返ってくる)
public double GetPlayerAnglePI2(double x, double y)
{
    List<CMover> player = new List<CMover>(Player);
    return GetMoverAngle(player, x, y, false);
}

// プレイヤーまでの角度を計算する(0~1.0の間で値が返ってくる)
public double GetPlayerAngle1(double x, double y)
{
    List<CMover> player = new List<CMover>(Player);
    return GetMoverAngle(player, x, y, true);
}

プレイヤーまでの角度を計算する処理ですが、0~PI2の間で返ってくるGetPlayerAnglePI2と0~1の間で返ってくるGetPlayerAngle1を用意しました。それに伴いCBulletクラスも少し修正をしてます。

public class CBullet : CMover
{
    public int State { set; get; } = 0;
    public double Speed { set; get; } = 0.0;
    public double Angle { set; get; } = 0.0;
    public double BaseAngle { set; get; } = 0.0;
    public int Color { set; get; }
    int GraphType, Rotate;
    double Yaw, VYaw;// 回転角度
    
    double AngleRate, SpeedRate;
    double PI2or1, AddRotate;
    public CBullet(double x, double y, double speed = 3.0, double angle = 0, double speed_rate = 0.0, double angle_rate = 0.0,
             int graph_type = 0, int color = 0, int state = -1, int rotate = 0, bool se_flag = true, int effect_type = 0, double pi2or1 = PI2) : bas y, 1, graph_type)
    {
        State = state;
        Angle = angle;
        AngleRate = angle_rate;
        Speed = speed;           
        SpeedRate = speed_rate;
        GraphType = graph_type;
        Color = color; 
        Rotate = rotate;
        PI2or1 = pi2or1; // これにはPI2か1.0のどちらかを入れるようにする
        // SGP.GetPlayerAnglePI2を使った時には1.0を入れる事
        // GetPlayerAngle1を使った時にはPI2を入れる事
        if (PI2or1 == 1.0)
        {
            AddRotate = PI/2;
        }
        else
        {
            AddRotate = 0.25;
        }
        Yaw = 0.0;
        VYaw = 0.0;
        // 弾画像によって当たり判定を変える
        switch (graph_type)
        {
            case 0: // でかい丸の弾
                L = T = -9.0f;
                R = B = 9.0f;
                break;
            case 1: // 丸の弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            case 2: // 線の弾
                L = T = -5.0f;
                R = B = 5.0f;
                break;
            case 3: // 剣の弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            case 4: // 光る弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            case 5: // お札弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            case 6: // 孔雀弾
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 7: // 丸+四角弾
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 8: // 米弾
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 9:
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 10: // 粒弾
                L = T = -2.0f;
                R = B = 2.0f;
                break;
            case 11: // 蝶弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            case 12:
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 13:
                L = T = -3.0f;
                R = B = 3.0f;
                break;
            case 14:
                L = T = -1.0f;
                R = B = 1.0f;
                break;
            case 15: // 花弾
                L = T = -4.0f;
                R = B = 4.0f;
                break;
            default: // 指定した画像タイプが範囲外なら通常弾画像にする
                GraphType = 1;
                L = T = -4.0f;
                R = B = 4.0f;
                break;
        }
        if (se_flag)
        {
            SGP.PlaySound(0);
        }          
    }
    public virtual void Move()
    {
        //弾を回転させる
        if (Rotate == 1) Yaw += VYaw;
        //角度をラジアン単位に変換する
        double rad = Angle * PI2or1;

        // 角度と速度を使って、座標を更新する
        X += Speed * Math.Cos(rad);
        Y += Speed * Math.Sin(rad);

        // 角度に角速度を加算する
        Angle += AngleRate;

        // 速度に加速度を加算する
        Speed += SpeedRate;
        if (X < 10 + 20 || // 左座標の上限
            X > FIELD_MAX_X - 10 + 42 || // 右座標の上限
            Y < 0 || // 上座標の上限
            Y > FIELD_MAX_Y - 5 + 20) // 下座標の上限
        {
            Life = 0;
        }
        ++Cnt;
    }
    public void Draw()
    {
        DX.DrawRotaGraphF((float)X, (float)Y, 1.0, (Angle + AddRotate + Yaw) * PI2or1, SGP.ImageBullet[GraphType][Color], 1);
        //DX.DrawBox((int)(X + L), (int)(Y + T), (int)(X + R), (int)(Y + B), DX.GetColor(0, 0, 255), 1);
    }
}

ここで重要なのは、GetPlayerAnglePI2を使う時には引数のPI2or1に1.0をGetPlayerAngle1を使う時には PI2or1にPI2を入れるということです。雑魚の弾幕では0~1.0を使い、ボスの弾幕では0~PI2を使う事が多いので、両方使える仕様に変更しました。

次はボスの変数と関数がごちゃごちゃになってきたので、移動と弾幕のクラスを分離させる作業をする予定です。