GoogleAdsence

弾幕シューティングゲーム制作その16 最終回 ステージ管理

前回で会話シーン用のスクリプトエンジンをゲームに組み込みました。今回で最終回になるわけで、ステージの管理についてのお話をします。

ステージ管理の規模で設計を変える

ステージ管理の実装については、規模にもよりますが、今回はステージを変更する時に設定するCShooting.csのSetStage関数だけで、次のステージに行く準備をします。
もし、規模が大きい場合には、CShootingGame自体に基底クラスを定義して初期化、メインループ、終了処理の3つの関数をオーバーライドして、分岐する方法があります。

今回は使いませんが、先に大規模な場合の仮のソースコードを紹介します。

public class CBaseGame
{
	public CBaseGame()
	{}
	public virtual void Initalize(){}
	public virtual void MainLoop(){}
	public virtual void Destory(){}
}

public clss CShootingGame : CBaseGame
{
	public CShootingGame()
	{}
	public override void Initalive(){}
	public override void MainLoop(){}
	public override void Destory(){}
}
CBaseGame BaseGame;
private void Form1_Load(object sender, EventArgs e)
{
    ClientSize = new Size(640, 480);
    DX.SetUserWindow(Handle); //DxLibの親ウインドウをこのフォームウインドウにセット
    DX.DxLib_Init();
    // 描画先を裏画面に変更
    DX.SetDrawScreen(DX.DX_SCREEN_BACK);

    BaseGame = new CShootingGame();
    BaseGame.Initalize();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    BaseGame.Destory();
}

//ループする関数
public void MainLoop()
{
    BaseGame.MainLoop();
}

こんな感じで書いておいて、
BaseGame = new CShootingGame();
の部分を違ったCBaseGameの派生クラスを作って、インスタンスを生成するとInitalize、MainLoop、Destoryの3つともすべて入れ替えることができます。

今回のステージ管理に必要な機能

  • ボスを倒した後、専用のスコアを表示
  • 次のステージのスクリプトを読み込む
  • BGMの停止

以上の処理をアルファブレンドを使いつつ、CSceneクラスの派生クラスのCStageClearクラスで処理します。次のステージの準備をする処理はSGP.SetStageで行っています

public class CStageClear : CScene
{
    int State = 0;
    int Cnt = 0;
    public CStageClear()
    {
        SGP.Paused = true;
    }
    public override void Move()
    {
        switch(State)
        {
            case 0: // 専用スコアボードのアルファブレンドを少しずつ濃くする
                Cnt += 4;
                if(255 < Cnt)
                {
                    Cnt = 255;
                    State = 1;
                }
                break;
            case 1: // 次のステージの読み込み
                ++SGP.StateIndex;
                SGP.SetStage();
                State = 2;
                break;
            case 2: // 専用スコアボードが完全に描画された状態
                //すべてのスコアが完全に表示しきれたら
                if (SGP.Score == ScoreBuf && SGP.Graze == GrazeBuf
                        && SGP.Point == PointBuf && SGP.Money == MoneyBuf)
                {
                    // ショットキーを押すとState2へ
                    if (GetPadKey(EINPUT_TYPE.Shot) == 1)
                    {
                        State = 3;
                    }
                }
                break;
            case 3: // 徐々に専用スコアボードを消す
                Cnt -= 4;
                if(Cnt < 0) 
                {
                    SGP.StopAllMusic(); // 音楽を消す                 
                    State = -1;
                    SGP.Paused = false; // ポーズを解除
                    Life = 0; // CStageClearをリストから削除する
                }
                break;
        }

    }
    int ScoreBuf = 0, GrazeBuf = 0, PointBuf = 0, MoneyBuf = 0;
    void DrawScoreBorad()
    {
        int score, graze, point, money;
        
        //ここからのif文はスコアを少しずつ足して表示させるための処理
        //SGP.Score-200とScoreBuf += 200の200は同じ数にすること!
        if (SGP.Score - 200 > ScoreBuf)
        {//最初は200ずつ足す
            ScoreBuf += 200;
        }
        else if (SGP.Score - 10 > ScoreBuf)
        {
            ScoreBuf += 10;
        }
        else if (SGP.Score > ScoreBuf)
        {
            ScoreBuf++;
        }

        if (SGP.Graze - 200 > GrazeBuf)
        {
            GrazeBuf += 200;
        }
        else if (SGP.Graze - 10 > GrazeBuf)
        {
            GrazeBuf += 10;
        }
        else if (SGP.Graze > GrazeBuf)
        {
            GrazeBuf++;
        }

        if (SGP.Point - 200 > PointBuf)
        {
            PointBuf += 200;
        }
        else if (SGP.Point - 10 > PointBuf)
        {
            PointBuf += 10;
        }
        else if (SGP.Point > PointBuf)
        {
            PointBuf++;
        }

        if (SGP.Money - 200 > MoneyBuf)
        {
            MoneyBuf += 200;
        }
        else if (SGP.Money - 10 > MoneyBuf)
        {
            MoneyBuf += 10;
        }
        else if (SGP.Money > MoneyBuf)
        {
            MoneyBuf++;
        }
        score = ScoreBuf;
        graze = GrazeBuf;
        point = PointBuf;
        money = MoneyBuf;

        for (int i = 0; i < 9; i++)
        {//スコア表示
            DX.DrawRotaGraph(350 - 20 * i, 150, 1.5f, 0.0f, SGP.ImageNum[1][score % 10], 1);
            score /= 10;
        }

        for (int i = 0; i < 9; i++)
        {//グレイズ表示
            DX.DrawRotaGraph(350 - 20 * i, 205, 1.5f, 0.0f, SGP.ImageNum[1][graze % 10], 1);
            graze /= 10;
        }

        for (int i = 0; i < 9; i++)
        {//ポイント表示
            DX.DrawRotaGraph(350 - 20 * i, 260, 1.5f, 0.0f, SGP.ImageNum[1][point % 10], 1);
            point /= 10;
        }

        for (int i = 0; i < 9; i++)
        {//マネー表示
            DX.DrawRotaGraph(350 - 20 * i, 315, 1.5f, 0.0f, SGP.ImageNum[1][money % 10], 1);
            money /= 10;
        }
    }
    public override void Draw()
    {
        DX.SetDrawArea(32, 16, 416, 464);//描画可能エリアを設定
        DX.SetDrawBlendMode(DX.DX_BLENDMODE_ALPHA, Cnt);
        DX.DrawGraphF(32, 16, SGP.ImageStageClearBorad, 0);
        DrawScoreBorad();
        DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);
        DX.SetDrawArea(0, 0, 640, 480);//エリアを戻す
    }
}

やってる処理をまとめると

  1. コンストラクタでPauseをtrueにして、Scene以外の移動処理を止める
  2. 次のステージの読み込み
  3. すべてのスコアが完全に表示した状態でショットを押したら4へ
  4. 徐々に専用スコアボードを消す
  5. 音楽を消す
  6. ポーズを解除
  7. CStageClearをSceneリストから削除する

以上が計算処理です。描画処理はSetDrawAreaで描画可能範囲を指定し、SetDrawBlendModeでアルファブレンドをし、DrawGraphで描画してます。DrawScoreBoradは設定用スコアボードの数字を描画してます。

次のステージを準備する

public int StateIndex { set; get; } = 0;
// この関数を呼ぶとゲームのメイン画面へ移行する
public void SetStage()
{
    Shot.Clear();
    Bullet.Clear();
    Enemy.Clear();
    Effect.Clear();
    Item.Clear();
    string[] file_name = { "dat/stage/stage1.txt",
        "dat/stage/stage2.txt" };

    if (!CommandManager.LoadScript(file_name[StateIndex]))
    {
        MessageBox.Show($"{file_name}が開けませんでした");
        DX.WaitKey();
        DX.DxLib_End();
        return;
    }
}

SetStageでは、Shot、Bullet、Enemy、Effect、Itemをリストから削除して、StateIndexで読み込むファイルのパスを選択して、そのパスをCommandManager.LoadScript渡して、次のステージのスクリプトを読み込んで、コマンドを追加してます。

それとCCommandManager.csのLoadScript関数の1行目にCommand.Clear();を足してください。これがないと前のステージのコマンドが残ったままで、すぐに次のステージのコマンドを実行できないためです。

実行結果

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

これで弾幕シューティングゲームを作るのに必要な機能はすべて実装できました。後はスクリプトを書いて、難易度の調整し、新しいステージと敵と弾幕を作るの繰り返しです。ここまで読んでいただいた方本当にありがとうございました。