GoogleAdsence

弾幕シューティングゲーム制作その13 ゲームの骨格の完成

2020年1月6日

今回でスクリプトでBGMの再生とフェードアウトやボスの生成、ボスの物理移動の計算、HPゲージと当たり判定、敵の種類、ボスの弾幕の追加など、今までに比べるとかなりの量の追加になります。
ただこれでゲームの最低限の機能は実装できたし、今までやってきたことの繰り返しになりますので、安心してください。

新しいコマンドの追加

今までに敵生成コマンドと待機コマンドは作ったので、ボス生成、BGM再生、BGMフェードアウトコマンドを追加します。

// ボス生成用関数へのポインタ
public delegate void NEW_BOSS_FUNC(double x, double y);
public class CBossCreateCommand : ICommand 
{
    NEW_BOSS_FUNC Func;
    double X, Y;
    public CBossCreateCommand(NEW_BOSS_FUNC func, double x, double y)
    {
        Func = func;
        X = x; 
        Y = y; 
    }
    public void Run() { Func(X, Y); }
};

// BGM再生コマンド
public class CBGMCommand : ICommand
{
    int BGMIndex;
    public CBGMCommand(int bgm_index)
    {
        BGMIndex = bgm_index;
    }
    public void Run() { SGP.PlayMusic(BGMIndex); }
};
// BGMフェードアウトコマンド
public class CFadeOutCommand : ICommand
{
    int SoundNum;
    double StartVal, EndVal, EndTime;
    public CFadeOutCommand(int sound_num, double start_val, double end_val, double end_time)
    {
        SoundNum = sound_num;
        StartVal = start_val;
        EndVal = end_val;
        EndTime = end_time;
    }
    public void Run()
    {
        SGP.Scene.Add(new CVolumeChanger(SoundNum, StartVal, EndVal, EndTime));
    }
}

ボス生成コマンドは敵生成コマンドと同じ仕様ですし、BGMコマンドもRun関数でPlaySoundを呼んでるだけです。なのでCFadeOutCommandについて説明します。

CFadeOutCommandについて

まずRun関数でSceneリストに追加されてるCVolumeChangerの中身を見てみましょう>

public class CVolumeChanger : CScene
{
    CFadeTimer FadeTimer;
    int SoundNum;
    public CVolumeChanger(int sound_num, double start_val, double end_val, double end_time)
    {
        SoundNum = sound_num;
        FadeTimer = new CFadeTimer(start_val, end_val, end_time);
    }
    public override void Move()
    {
        double t = FadeTimer.CalcTime();
        DX.ChangeVolumeSoundMem((int)t, SGP.MusicSE[SoundNum]);

        if (!FadeTimer.Flag)
        {
            if (DX.CheckSoundMem(SGP.MusicSE[SoundNum]) == 1)
            {
                DX.StopSoundMem(SGP.MusicSE[SoundNum]);
            }
            Life = 0;
        }
    }
}

ここで重要なのはCFadeTimerクラスです。初期化する時にstart_val, end_val, end_timeを設定し、FadeTimer.CalcTime();で毎ループ値を計算してます。さらにFadeTimerクラスを見てみましょう。

public class CFadeTimer
{
    public bool Flag { set; get; }
    double StartVal, EndVal, StartTime, EndTime, Delta;
    public CFadeTimer(double start_val, double end_val, double end_time)
    {
        Flag = true;
        StartVal = start_val;
        EndVal = end_val;
        EndTime = end_time;
        StartTime = DX.GetNowCount();
        Delta = (EndVal - StartVal) / EndTime;
    }
    public double CalcTime()
    {
        if (Flag)
        {
            double t = DX.GetNowCount() - StartTime;
            if (t >= EndTime) Flag = false;
            return Delta * t + StartVal;
        }
        return 0;
    }
}

このクラスの役割は、指定した時間である値からある値まで数を変化させる事です。
以下は引数の説明です。

  • start_val 変化させたい数の初期値
  • end_val 変化させたい数の終了値
  • end_time 終了時間(end_timeかけて終了させる)

使用例 500~1000までの値を5秒かけて変化させたい場合
初期化
CFadeTimer FadeTimer = new CFadeTimer( 500, 1000, 5000 );
毎ループ行う処理
FadeTimer.CalcTime();

ボスの画像の読み込み

それではいつも通りLoadDivGraphで画像を読み込みましょう。

if (DX.LoadDivGraph("dat/img/char/riria.png", 8, 8, 1, 100, 100, ImageRiria) == -1)
{
    MessageBox.Show("dat/img/char/riria.pngが開けませんでした");
}
string[] boss_back =
{
    "dat/img/enemy/bossback.png",
    "dat/img/enemy/bossback3.png",
    "dat/img/enemy/bossback4.png"
};
for (int i = 0; i < boss_back.Length; ++i)
{
    ImageBossBack[i] = DX.LoadGraph(boss_back[i]);
    if (ImageBossBack[i] == -1)
    {
        MessageBox.Show($"{boss_back[i]}が開けませんでした");
    }
}

ボスクラス

ついにボスの処理を追加します!ボスは移動や描画処理が特殊なので、雑魚敵とはクラスとリストを分けます。

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
{
    int MaxLife = 650;
    int State = 0;
    double BaseAngle = 0.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;
                }
                break;
            case 3:
                BulletType1();
                Cnt++;
                if (Life <= 0)
                {
                    SGP.Bullet.Clear(); // 敵の弾を削除
                    SGP.PlaySound(7);
                    DeathFlag = true;
                }
                break;            
        }
        if (Phy != null)
        {
            (X, Y) = Phy.CalcPhy(X, Y);
        }
    }
    int magic_color = 0;
    public override void Draw()
    {       
        switch (magic_color)
        {
            case 0:
                DX.SetDrawBright(255, (int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))), (int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))));
                break;
            case 1:
                DX.SetDrawBright((int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))), 255, (int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))));
                break;
            case 2:
                DX.SetDrawBright((int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))), (int)(255 - 255 * Math.Sin(PI / 180 * (Cnt % 360))), 255);
                break;
        }

        if (Cnt % 360 == 0 && Cnt != 0) magic_color = (magic_color + 1) % 3;

        DX.SetDrawBlendMode(DX.DX_BLENDMODE_ALPHA, 150);

        float x = (float)X;
        float y = (float)Y;
        // ボスの背景魔方陣の外枠の丸を描画
        DX.DrawRotaGraphF(x, y,
                       (0.5 + 0.1 * Math.Sin(PI2 / 360 * (Cnt % 360))) * 2,
                       PI2 * (Cnt % 340) / 340,
                       SGP.ImageBossBack[0], 1);

        // ボス背景の魔方陣描画
        DX.DrawRotaGraphF(x, y,
                       (0.4 + 0.05 * Math.Sin(PI2 / 360 * (Cnt % 360))) * 3,
                       2 * PI * (Cnt % 580) / 580,
                       SGP.ImageBossBack[1], 1);

        x = (float)(X + 60 * Math.Sin(PI2 / 230 * ((Cnt + 40) % 230)));
        y = (float)(Y + 80 * Math.Sin(PI2 / 189 * ((Cnt + 40) % 189)));
        // ボス背景のクルクル回る四角を描画(大)
        DX.DrawRotaGraphF(x, y,
                        0.6 + 0.05 * Math.Sin(PI2 / 120 * (Cnt % 120)),
                        PI2 * (Cnt % 40) / 40, SGP.ImageBossBack[2], 1);

        x = (float)(X + 60 * Math.Sin(PI2 / 153 * (Cnt % 153)));
        y = (float)(Y + 80 * Math.Sin(PI2 / 120 * (Cnt % 120)));
        // ボス背景のクルクル回る四角を描画(中)
        DX.DrawRotaGraphF(x, y,
                        0.4 + 0.05 * Math.Sin(PI2 / 120 * (Cnt % 120)),
                        PI2 * (Cnt % 30) / 30, SGP.ImageBossBack[2], 1);

        x = (float)(X + 60 * Math.Sin(PI2 / 200 * ((Cnt + 20) % 200)));
        y = (float)(Y + 80 * Math.Sin(PI2 / 177 * ((Cnt + 20) % 177)));
        // ボス背景のクルクル回る四角を描画(小)
        DX.DrawRotaGraphF(x, y,
                       0.3 + 0.05 * Math.Sin(PI2 / 120 * (Cnt % 120)),
                       PI2 * (Cnt % 35) / 35, SGP.ImageBossBack[2], 1);

        DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);

        DX.SetDrawBright(255, 255, 255);

        // ボス描画
        DX.DrawRotaGraphF((float)X, (float)(Y + Math.Sin(PI2 / 130 * (Cnt % 130)) * 10), 1.0, 0, SGP.ImageRiria[0], 1);

        DrawHp();
    }
    int move_cnt = 0;
    void BulletType1()
    {
        int i, j, t = Cnt % 400;
        if (t == 0)
        {
            for (j = 0; j < 12; j++)
            {
                for (i = 0; i < 20; i++)
                {
					SGP.Bullet.Add(new CBullet(x: X, y: Y, speed: 0.5f * j + 1.5, angle: (PI2 / 20 * (i - 10)) / PI2, speed_rate: 0.0, angle_rate: 0.0, graph_type: 7, color: 0, state: 0));
                }
            }
        }
        foreach (var bullet in SGP.Bullet)
        {
            if (bullet.State == 0)
            {
                if (bullet.Cnt == 50)
                {
                    bullet.Speed = 0.0;
                }
                else if (bullet.Cnt == 120)
                {
                    bullet.Speed = 3.0;
                    bullet.State = -1;
                }
            }
        }
        if (t % 2 == 0 && 40 < t && t < 100)
        {
            for (i = 0; i < 2; i++)
            {
				SGP.Bullet.Add(new CBullet(x: X, y: Y, speed: 2.0f, angle: 0.2f * t + 0.2f * i, speed_rate: 0.0, angle_rate: 0.0, graph_type: 3, color: 3, state: 1));
            }
        }
        foreach (var bullet in SGP.Bullet)
        {
            if (bullet.State == 1 && bullet.Cnt == 60)
            {
                bullet.Angle = SGP.GetPlayerAngle(bullet.X, bullet.Y);
                bullet.Speed = 4.0;
            }
        }
    }
    public void DrawHp() 
    {
        const int wd = 400;
        const int dx = 32;
        const int dy = 16;
       
        DX.DrawBox(dx, dy, dx + wd, dy + 10, SGP.Color[2], 0);//メーターの枠を描画
        DX.DrawBox(dx, dy, dx + wd * Life / MaxLife, dy + 10, SGP.Color[2], 1);//メーターの中身を描画
    }
    public static void New(double x, double y)
    {
        SGP.Boss.Add(new CBoss1(x, y));
    }
}

MoveとDraw関数を基底クラスのCBossで実装し、継承したCBoss1クラスで実際の処理を書くのも今まで通りです。

ボスの弾幕について

前に紹介した%余剰演算子ですが、弾幕を作る時には必ずといっていいほど使います。
BulletType1関数の最初で、int t = Cnt % 400;初期化すると0~399の間をループする値を繰り返します。
この性質を利用して一定の時間が経ったら、処理をする目安にしてます。

ストップ&ゴー弾幕

ボスの弾幕BulletType1では以下の2つの弾幕を繰り返し発射してます。

  1. 縦に12、横に20に発射した弾幕が止まり一定時間経つと動く
  2. 横に5方向連続で発射して、一定時間経つとプレイヤーへ向かってくる

1を発射しているのが、165行目で2を発射しているのは、188行目です。
基本はfor文の2重ループで縦と横の数を指定して、その後に移動を変化させたい場合には、Bulletリストのパラメーターを直接いじります。

1の弾幕の操作について

上のソースコードの169~183行目のハイライトの部分です。

  1. 弾が発射されて50カウントでスピードを0にして止める
  2. 弾が発射されて120カウントでスピードを3.0にして進める

BulletクラスのStateは2種類の弾幕のうち操作したい方の弾幕を区別するための物です。

2の弾幕の操作について

上のソースコードの191~198行目のハイライトの部分です。
これは弾が発射されて60カウントでスピードを4.0に角度をプレイヤーに向けるという処理です。

ボスのリストで追加した処理

public List<CBoss> Boss = new List<CBoss>();
foreach (var boss in Boss.Where(x => !x.DeathFlag)) boss.Move();
foreach (var boss in Boss.Where(x => !x.DeathFlag)) boss.Draw();
Boss.RemoveAll(s => s.DeathFlag);

上の処理を見てボスの処理だけ異なる部分は、LifeではなくDeathFlagを見てボスの移動、描画、消去を決めている所です。理由は、ボスは何回かLifeが0になり復活して、新しい攻撃を仕掛けてくるためです。

弾幕については、これから専用の記事を書きますので、少々お待ちください。

読み込んだスクリプトの内容

bgm 0 は0番目のbgmの再生
wait 120 は120フレーム約2秒待つ

  • enemyはコマンド名
  • enemy2は敵の種類
  • exは敵を生成するX座標
  • eyは敵を生成するY座標
  • hit_sは敵の当たり判定
  • lifeは敵の生命力
  • gr_typeは弾のグラフィックのタイプ
  • w_timeは停止する敵の場合、停止する時間
  • bl_patは弾幕生成の種類(BulletFactoryの種類を決定)
  • bl_colは弾の色
stage1.txt

bgm 0
wait 120
enemy enemy2 ex 200 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 5 bl_col 0
enemy enemy2 ex 250 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 5 bl_col 1
wait 120
enemy enemy3 ex 200 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 6 bl_col 1
enemy enemy3 ex 250 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 6 bl_col 2
wait 120
enemy enemy4 ex 200 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 3 bl_col 0
enemy enemy4 ex 250 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 3 bl_col 1
wait 120
enemy enemy5 ex 200 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 3 bl_col 2
enemy enemy5 ex 250 ey 0 hit_s 12 life 3 w_time 60 bl_pat 0 gr_type 3 bl_col 3
wait 120
enemy enemy1 ex 200 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 2
enemy enemy1 ex 250 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 2
wait 10
enemy enemy1 ex 150 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 1
enemy enemy1 ex 300 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 1
wait 10
enemy enemy1 ex 100 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 2
enemy enemy1 ex 350 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 2
wait 10
enemy enemy1 ex 50 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 1 bl_col 3
enemy enemy1 ex 400 ey 0 hit_s 12 life 5 0 w_time 60 bl_pat 0 gr_type 1 bl_col 3
wait 160
enemy enemy0 ex 200 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 0 bl_col 0
enemy enemy0 ex 250 ey 0 hit_s 12 life 5 w_time 60 bl_pat 0 gr_type 0 bl_col 1
wait 400
fadeout 0 255 0 4000
wait 240
bgm 1
boss boss1 240 0
//gameover

実行結果

今回のプロジェクトをダウンロードする。 プロジェクトに画像は同梱してませんのでこちらからダウンロードしてください。画像を置く階層はデバッグモードで実行するなら、bin/Debugでリリースモードで実行するならbin/Releaseです。

次回以降はボムや会話イベント用のスクリプトなどの処理を追加して、さらにゲームを楽しめるようにします。