アマゾンバナーリンク

弾幕シューティングゲーム制作その1 設計と仕様とプログラムの骨格を作る

2020年1月11日

今回から縦スクロールの弾幕シューティングゲーム制作の講座を行います。

まず仕様の説明から

  • 2D縦スクロール弾幕シューティングゲーム
  • スコア表にスコアや残機を表示
  • 1ステージに中ボスとラスボスがいる
  • 敵の情報はtxtファイルから読み込む
  • 自キャラ以外にアイテムやオプションもある
  • オプションでキーコンフィグ設定できる(コントローラーにも対応)

ざっくりですが、こんな感じで開発環境は、VisualStudioでC#とDXライブラリを使います。
おまけで音楽と弾幕を同調させるというのも考えてますが、音ゲー講座を制作した後にしようかと思います。

次に必要な処理を洗い出す

  • 自キャラをキー入力で動かす
  • 敵キャラや弾を動かす
  • 敵や弾との当たり判定
  • スコアボードに必要な情報を表示
  • オプションを自キャラに追従して動かす
  • ファイルから読み込んで敵キャラと弾幕を生成する
  • 背景のスクロール

作る前の心構え

そして、意識したいのが可読性と保守性です。初心者の人にでも読みやすく、後から仕様を追加することになっても楽に追加や変更ができるコーディングを心がけます。
それと数学の知識が多少必要になりますが、高校でならった数学の範囲で、ベクトルと三角関数が理解できれば、問題なく作れます。

最初は無理に理解してなくてもよい

すべてのゲームを作るのに最低限必要な処理で、テンプレートを配布するので、それを使って、色々なゲームを作るとよいです。見ると、うわー、沢山あるなと思うかもしれませんが、現時点では特に理解してなくても大丈夫です。 最初は中身を理解するというよりすでに用意されたクラスや関数の使い方を学ぶ方が重要です。

理解していく理想のステップ

  1. 人が作ったクラスや関数をそのまま使う
  2. 何度か使い方を理解したら、中身を少し書き変えてみる
  3. さらに慣れたら、自作してみる

という感じでわからなくてもちょっとずつ気軽に覚えていくと楽だと思います。

設計について

まずは、ゲーム全体を管理する基底クラスの CShootingGame に、初期化関数、毎ループ処理する関数、終了処理関数を定義します。自分自身のクラスにCShootingGame型の静的変数とプロパティを使うことによって、どのファイルからも参照できるグローバル変数のような扱い方をするためです。

キー入力とFPS制御について

キー入力とパッド入力の両方に対応とマウス入力対応とFPSを制御する機能を追加します。キー入力とパッド入力はCInputクラス、マウス入力はCMouseクラス、FPSを制御は、CFpsControlクラスでいずれもstatic(静的クラス)を使ってます。

静的クラスについて

静的クラスとは、静的変数と静的関数のみで構成されるクラスです。このクラスでは、new演算子でインスタンスを生成することができません。クラス名.関数名という形で呼び出せます。静的関数には以下の特徴があります。

  • 静的メンバ関数は実体がなくても呼べる。
  • 静的メンバ関数は静的メンバしか使えない。

静的クラスや関数を使うべき場面

静的クラスや関数を使うべき場面

入力系統やFPS制御の機能はどんなゲームにも、共通して使えるので静的クラスを使うのが適してます。しかし、敵キャラや弾などのオブジェクトは、ゲーム中に沢山複製するので、静的クラスを使うのには適しません。

インスタンス化して複製するべき場面

自キャラや敵キャラなど動く物すべてのクラス
ゲーム内では、CMoverというクラスを動く物すべてのクラスを派生して作っていきます。

さっそく作り始める

テンプレートで使うプロジェクトはこちらからダウンロードできます。 以下に ソースも張っておきますが、自分で作りたい人は下の講座を参考にしてください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;

namespace ShootingGame
{
    public class CShootingGame
    {
        // 実装は外部から隠蔽(privateにしておく)
        private static CShootingGame ShootingGame; // staticで変数を宣言
        // 変数の取得・変更用のプロパティ
        public static CShootingGame ShootingGamePro
        {
            set { ShootingGame = value; }
            get { return ShootingGame; }
        }
        // 初期化関数
        public void Initalize() 
        {
            // ここに初期化処理を追加する

        }
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            // ここに処理を追加する

            CFpsControl.DrawFps(0, 465);
            CFpsControl.FpsWait();
            // 裏画面の内容を表画面に反映する
            DX.ScreenFlip();
        }
        // 終了処理関数
        public void Destory()
        {
            // ここに終了処理を追加する

            DX.DxLib_End();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DxLibDLL;

namespace ShootingGame
{
    static class Program
    {    
        static int ProcessLoop()
        {
            if (DX.ProcessMessage() != 0) return -1;   //プロセス処理がエラーなら-1を返す
            if (DX.ClearDrawScreen() != 0) return -1;   //画面クリア処理がエラーなら-1を返す
            CInput.GetHitKeyStateAll_2();           //現在のキー入力処理を行う
            CMouse.GetHitMouseStateAll_2();         //現在のマウス入力処理を行う
            CInput.GetHitPadStateAll();             //現在のパッド入力処理を行う
            if (CInput.CheckStateKey(DX.KEY_INPUT_ESCAPE) == 1) return -1; // エスケープで終了
            return 0;
        }
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Form1 form = new Form1();
            form.Show();
            while (ProcessLoop() == 0 && form.Created) //Application.Runしないで自分でループを作る
            {
                form.MainLoop();
                Application.DoEvents();
            }
        }
    }

    static class CInput
    {
        public enum EINPUT_TYPE : int
        {
            Down = 0,
            Left = 1,
            Right = 2,
            Up = 3,
            Bom = 4,
            Shot = 5,
            Change = 6,
            Slow = 11,
            Start = 13,
        }
             
        const int KEY_NUM = 256, PAD_NUM = 16;
        private static int[] Key = new int[KEY_NUM];
        private static int[] PadKey = new int[PAD_NUM];

        // 現在のキー入力処理を行う
        public static int GetHitKeyStateAll_2()
        {
            byte[] GetHitKeyStateAll_Key = new byte[KEY_NUM];
            DX.GetHitKeyStateAll(GetHitKeyStateAll_Key);
            for (int i = 0; i < KEY_NUM; ++i)
            {
                if (GetHitKeyStateAll_Key[i] == 1) Key[i]++;
                else Key[i] = 0;
            }
            return 0;
        }
        //パッドとキーボードの両方の入力をチェックする関数
        public static void GetHitPadStateAll()
        {
            int PadInput = DX.GetJoypadInputState(DX.DX_INPUT_PAD1); // パッドの入力状態を取得

            for (int i = 0; i < PAD_NUM; i++)
            {
                if((PadInput & 1 << i) == 0)
                {
                    PadKey[i] = 0;
                }
                else
                {
                    ++PadKey[i];
                }
            }
            int left = (int)EINPUT_TYPE.Left;
            int up = (int)EINPUT_TYPE.Up;
            int right = (int)EINPUT_TYPE.Right;
            int down = (int)EINPUT_TYPE.Down;
            int shot = (int)EINPUT_TYPE.Shot;
            int bom = (int)EINPUT_TYPE.Bom;
            int slow = (int)EINPUT_TYPE.Slow;
            int start = (int)EINPUT_TYPE.Start;
            int change = (int)EINPUT_TYPE.Change;
            PadKey[left]   = Math.Max(PadKey[left],   CheckStateKey(DX.KEY_INPUT_LEFT));
            PadKey[up]     = Math.Max(PadKey[up],     CheckStateKey(DX.KEY_INPUT_UP));
            PadKey[right]  = Math.Max(PadKey[right],  CheckStateKey(DX.KEY_INPUT_RIGHT));
            PadKey[down]   = Math.Max(PadKey[down],   CheckStateKey(DX.KEY_INPUT_DOWN));
            PadKey[shot]   = Math.Max(PadKey[shot],   CheckStateKey(DX.KEY_INPUT_Z));
            PadKey[bom]    = Math.Max(PadKey[bom],    CheckStateKey(DX.KEY_INPUT_X));
            PadKey[slow]   = Math.Max(PadKey[slow],   CheckStateKey(DX.KEY_INPUT_V));
            PadKey[start]  = Math.Max(PadKey[start] , CheckStateKey(DX.KEY_INPUT_ESCAPE));
            PadKey[change] = Math.Max(PadKey[change], CheckStateKey(DX.KEY_INPUT_C));
        }
        public static int CheckStateKey(int handle)
        {
            return Key[handle];
        }
        public static int GetPadKey(EINPUT_TYPE handle)
        {
            return PadKey[(int)handle];
        }
    }
    static class CMouse
    {
        public static int MX = 0, MY = 0, WheelRotVol = 0;
        const int MOUSE_NUM = 8;//マウスのキーは最大8個
        public static int[] Button = new int[MOUSE_NUM];
        // マウスの入力状態を取得
        public static int GetHitMouseStateAll_2()
        {
            if (DX.GetMousePoint(out MX, out MY) == -1) //マウスの位置取得
            {
                return -1;
            }
            int mouse_input = DX.GetMouseInput();       //マウスの押した状態取得
            for (int i = 0; i < MOUSE_NUM; i++)         //マウスのキーは最大8個まで確認出来る
            {
                if ((mouse_input & 1 << i) != 0) Button[i]++;  //押されていたらカウントアップ
                else Button[i] = 0;   //押されてなかったら0
            }
            WheelRotVol = DX.GetMouseWheelRotVol();  //ホイール回転量取得
            return 0;
        }
    }
    static class CFpsControl
    {
        const int FLAME = 60;
        //fpsのカウンタ、60フレームに1回基準となる時刻を記録する変数
        static int FpsCount = 0, Count0t = 0;
        //平均を計算するため60回の1周時間を記録
        static int[] AverageTemp = new int[FLAME];
        //平均fps
        static float FpsAverage = 0.0f;
        static int t = 0;
        
        public static async void FpsWait()
        {
            int term;
            
            if (FpsCount == 0) // 60フレームの1回目なら
            {
                if (t == 0) // 完全に最初ならまだない
                {
                    term = 0;
                }
                else // 前回記録した時間を元に計算
                {
                    term = Count0t + 1000 - DX.GetNowCount();
                }
            }
            else // 待つべき時間=現在あるべき時刻-現在の時刻
            {
                term = (int)(Count0t + FpsCount * (1000.0 / FLAME)) - DX.GetNowCount();
            }

            if (term > 0) // 待つべき時間だけ待つ
            {
                await Task.Delay(term);
            }

            int gnt = DX.GetNowCount();

            if (FpsCount == 0) // 60フレームに1度基準を作る
            {
                Count0t = gnt;
            }

            AverageTemp[FpsCount] = gnt - t;//1周した時間を記録
            t = gnt;

            //平均計算
            if (FpsCount == FLAME - 1)
            {
                FpsAverage = 0;
                for (int i = 0; i < FLAME; i++)
                    FpsAverage += AverageTemp[i];
                FpsAverage /= FLAME;
            }

            FpsCount = (++FpsCount) % FLAME;
        }
        public static void DrawFps(int x, int y)
        {
            if (FpsAverage != 0)
            {
                DX.DrawString(x, y,(1000 / FpsAverage).ToString(), DX.GetColor(255, 255, 255));
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using DxLibDLL;
using static ShootingGame.CBaseGame;

namespace ShootingGame
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        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);

            BaseGamePro = new CShootingGame();
            BaseGamePro.Initalize();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            BaseGamePro.Destory();
        }

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

なお他のファイルから参照した時にクラス名を省略できるように、usingstatic構文を使ってます。


CBaseGameは、自分自身のクラスにstaticで宣言することにより、他のファイルからも参照できるようになります。ある意味、グローバル変数のような扱いをしたい時にこの様にします。

実行結果

今回のプロジェクトをダウンロードする。

真っ暗で左下にFPSが表示されて、Zキーを押すとメッセージボックスが表示されます。次回は自キャラを動かしてみます。