アマゾンバナーリンク

弾幕シューティングゲーム制作その15 会話シーンの追加

2020年1月10日

前回ノベルゲーム用のスクリプトエンジンを制作しました。今回はこれを使って実際にゲーム内に組み込む方法を説明します。そして、会話シーンを追加する前にゲームの流れを止めるポーズ機能を実装します。

ポーズ機能と会話シーン追加する手順

  1. CShootingGame.csにポーズ用フラグの追加
  2. ポーズを実行し解除するまで制御するためのCPauseクラスの作成
  3. CPlayerにポーズを生成するプログラムを追加
  4. 前回作ったCScriptControl.csの追加
  5. CScriptControlを制御するCScriptSceneクラスを作成
  6. CCommandManager.csにCCreateScriptクラスを作成
  7. 5で作成した CCreateScriptをLoadScriptにて追加

以上の手順になります。なぜポーズを制御するためにわざわざCPauseを作るかというと、メインループに直書きしてしまうと、会話シーン中にも、ポーズを解除できたりしてしまうためです。
なので、ポーズを制御するのは必ずCSeneceクラスの派生クラスで行う仕様にします。

ポーズ処理

ポーズ用フラグの追加

CShootingGame.csにポーズ用フラグの追加

public bool Paused { set; get; } = false;
public void MainLoop() 
{
    if (0 < PlayerBuf.Count) { Player.AddRange(PlayerBuf); PlayerBuf.Clear(); }
    if (0 < EnemyBuf.Count) { Enemy.AddRange(EnemyBuf); EnemyBuf.Clear(); }
    if (0 < ShotBuf.Count) { Shot.AddRange(ShotBuf); ShotBuf.Clear(); }
    if (0 < BulletBuf.Count) { Bullet.AddRange(BulletBuf); BulletBuf.Clear(); }
    if (0 < EffectBuf.Count) { Effect.AddRange(EffectBuf); EffectBuf.Clear(); }
    if (0 < ItemBuf.Count) { Item.AddRange(ItemBuf); ItemBuf.Clear(); }
    if (!Paused)
    {
        CommandManager.Run();
        // ここに処理を追加する
        // Lifeが0より多いなら処理する
        foreach (var back in Back.Where(x => 0 < x.Life)) back.Move();
        foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();
        foreach (var shot in Shot.Where(x => 0 < x.Life)) shot.Move();

        foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Move();
        foreach (var boss in Boss.Where(x => !x.DeathFlag)) boss.Move();
        foreach (var bullet in Bullet.Where(x => 0 < x.Life)) bullet.Move();

        foreach (var item in Item.Where(x => 0 < x.Life)) item.Move();
        foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Move();
        foreach (var borad in Borad.Where(x => 0 < x.Life)) borad.Move();
    }
    foreach (var scene in Scene.Where(x => 0 < x.Life)) scene.Move();

    if (Bright != 255) DX.SetDrawBright(Bright, Bright, Bright);
    foreach (var back in Back.Where(x => 0 < x.Life)) back.Draw();
    foreach (var player in Player.Where(x => 0 < x.Life)) player.Draw();          
    foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Draw();
    foreach (var boss in Boss.Where(x => !x.DeathFlag)) boss.Draw();
    foreach (var shot in Shot.Where(x => 0 < x.Life)) shot.Draw();
    foreach (var bullet in Bullet.Where(x => 0 < x.Life)) bullet.Draw();         
    foreach (var item in Item.Where(x => 0 < x.Life)) item.Draw();
    foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Draw();
    if (Bright != 255) DX.SetDrawBright(Bright, Bright, Bright);
    foreach (var borad in Borad.Where(x => 0 < x.Life)) borad.Draw();
    foreach (var scene in Scene.Where(x => 0 < x.Life)) scene.Draw();

    // X,Y座標の描画
    DX.DrawString(35, 35, $"Effect{Effect.Count}", DX.GetColor(255, 255, 255));

    // Lifeが0以下の要素を削除する
    Back.RemoveAll(s => s.Life <= 0);
    Player.RemoveAll(s => s.Life <= 0);
    Shot.RemoveAll(s => s.Life <= 0);
    Bullet.RemoveAll(s => s.Life <= 0);
    Enemy.RemoveAll(s => s.Life <= 0);
    Boss.RemoveAll(s => s.DeathFlag);
    Effect.RemoveAll(s => s.Life <= 0);
    Item.RemoveAll(s => s.Life <= 0);
    Borad.RemoveAll(s => s.Life <= 0);
  Scene.RemoveAll(s => s.Life <= 0);

    CFpsControl.DrawFps(0, 465);
    CFpsControl.FpsWait();
    // 裏画面の内容を表画面に反映する
    DX.ScreenFlip();
}

ポーズの制御をするSceneクラスを除く、メインループ内のMove関数をポーズフラグがfalseの時のみ実行するように変更してます。それと今回一番下でダウンロードできるプロジェクトは55行目のシーンの削除が抜けてましたので追加してください。

CPauseクラスの作成

public class CPause : CScene
{
    bool FirstLoop = true;
    public CPause()
    {
        SGP.Paused = true;
    }
    public override void Move()
    {
        // ポーズの解除
        if (GetPadKey(EINPUT_TYPE.Start) == 1 && !FirstLoop)
        {
            SGP.Paused = false;
            Life = 0;
        }

        // FirstLoopは前のシーケンスの時に同じキー判定にしても、すぐに次の画面に行かないようにするためのもの
        if (FirstLoop)
            FirstLoop = false;
    }
}

CPauseクラスを生成したらCShootingGameクラスのPausedフラグをtrueにして、スペースキーを押すとPausedフラグをtrueにして解除します。この時に1ループ目で一気に解除されないように、FirstLoopを使って、2ループ目以降のみポーズを解除できる仕様になってます。

ポーズを実行するプログラムを追加

上で作ったCPauseクラスを生成するプログラムをCNormalPlayerクラスのMove関数に追加します。

public override void Move()
{
    // ポーズ
    if (GetPadKey(EINPUT_TYPE.Start) == 1)
    {
        SGP.Scene.Add(new CPause());
        return;
    }
}      

それと、EINPUT_TYPE.Startがエスケープキーになっていたので、スペースキーに変更しました。
103行目をPadKey[start] = Math.Max(PadKey[start] , CheckStateKey(DX.KEY_INPUT_SPACE));とするだけです。

会話シーンの追加

会話用スクリプトの追加

前回作成したスクリプトエンジンの中身を以下に全部載せました。使えれば問題ないので、この下のプログラムは理解しなくても大丈夫です。

スクリプトを制御するクラスの作成

次にCScriptControlを利用するためのCScriptSceneを作ります。
この時の以下のポイント

  • 7行目の初期化処理でfile_nameでCScriptControlを生成
  • 13行目でScriptControl.MainEngne()でスクリプトの解釈を行う
  • 14行目のScriptControl.CheckKey()でキー入力の判定を行う
  • 26行目のScriptControl.TextDisp()で描画を行う
public class CScriptScene : CScene
{
    CScriptControl ScriptControl = null;
    public CScriptScene(string file_name)
    {
        SGP.Paused = true;
        ScriptControl = new CScriptControl(file_name);
    }
    public override void Move()
    {
        if(ScriptControl != null)
        {
            bool flag = ScriptControl.MainEngne();
            ScriptControl.CheckKey();
            if (!flag)
            {
                Life = 0;
                SGP.Paused = false;
            }
        }
    }
    public override void Draw()
    {
        if(ScriptControl != null)
        {
            ScriptControl.TextDisp();
        }
    }
}

スクリプトを生成するコマンドを追加

ここまできたら実際にスクリプト処理を生成するコマンドをCCommandManager.csに追加しましょう。

// 会話用スクリプト生成コマンド
public class CCreateScript : ICommand
{   
    string FileName;
    
    public CCreateScript(string file_name)
    { 
        FileName = file_name;
    }
    public void Run()
    {
        SGP.Scene.Add(new CScriptScene(FileName));
    }
};

引数には会話用スクリプトが置いてあるパスが格納されています。そして、LoadScript関数の一番下に以下のプログラムを追加してください。

else if (param[0] == "script")
{
    if (param.Length < 2)
    {
        error_string.Add($"{file_name}{cnt}行目のscriptコマンドのパラメーターが足りません");
        continue;
    }
    Command.Add(new CCreateScript(param[1]));
}

実際に読み込むスクリプトの中身

dat/stage/stage1.txt

bgm 0
wait 240
script dat\\story\\story1.txt
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

dat/story/storty1.txt

setcolor,255,255,255
loadgraph,Girl1,dat\img\char\body2.png
loadgraph,Girl2,dat\img\boss\boss_eff0.png
drawbox
chardisp,Girl2,40,100
リンダ あれ?お客さんかな?
リンダ いらっしゃい~
enter
strclear
chardisp,Girl1,200,100
ミイナ ん~お客さんというよりは道を尋ね
ミイナ たずねたいんだけれど…
ミイナ 案内お願いできるかしら?
enter
chardisp,Girl2,40,100
リンダ はいは~い
リンダ でもその前に?
enter
リンダ あたしの弾幕でおもてなしするよっ
end

以上で完成です。

実行結果

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

残すところステージごとの分岐処理だけになりました!ようやく次回で弾幕シューティングゲーム講座は最後になります。