アマゾンバナーリンク

弾幕シューティングゲーム制作その7 ファイルから敵と弾幕の情報を読み込む

2020年1月16日

こんにちは!ジェイです。前回は敵に弾幕を生成するクラスを別に持たせることによって、敵の移動処理と別々に分けられるように設計しました。今回はテキストファイルから、敵の出現データと発射する弾幕を指定できるようにしましょう。

コマンドパターンでイベントを制御する

ゲーム内には、色々なイベントが発生します。
例えば、

  • アイテムの発生
  • 敵の制御
  • 待ち時間
  • BGMの再生
  • 会話などのシーンを発生

これらを制御するのにコマンドパターンを使用します。

まず下の様にRun関数を実装した、インターフェースを用意します。
インターフェイスとは、仮想関数のみ定義できるクラスのことです。
抽象クラスでは、メンバ変数を持たせることができましたが、インターフェイスでは、できないので継承とオーバーライドを使うことを前提とします。

public interface ICommand
{
    void Run();
};

次に派生クラスを用意します。

public class CWaitCommand : ICommand
{
    private CCommandManager CommandManager;
    private int WaitTime;
    public CWaitCommand(CCommandManager game_control, int wait_time)
    {
        CommandManager = game_control;
        WaitTime = wait_time;
    }

    public void Run()
    {
        CommandManager.WaitTime = WaitTime;
    }
};

これは待ち時間を設定するコマンドのクラスです。敵を呼び出すコマンドのクラスなど、ICommandを派生してやれば、好きなイベントを作ることができます。

public class CCommandManager
{
	 private List<ICommand> Command = new List<ICommand>();
    public void Run()
    {
        if (WaitTime > 0) WaitTime--;

        while (CommandIndex < Command.Count && WaitTime == 0)
        {
            Command[CommandIndex].Run();
            CommandIndex++;
        }
    }
}

そして、その定義した派生クラスのコマンドをCCommandManagerクラスのList<ICommand>型のCommandに追加します。CShootingGameクラスのMainLoop関数でRunを実行すれば、WaitTimeが0の時に、異なった派生クラス型のCommand.Runが実行されて、イベントが起きる仕組みです。

CEnemyクラスの改造

コマンドパターンを使うには引数を単純にする必要があるので、構造体に必要なパラメーターをまとめてCEnemy.csに追加します。

public struct TagEnemyStatus
{
    public double X, Y, HitSize;
    public int Life, GraphType, WaitTime, BulletPattern;
    public int BulletInterval, BulletType, BulletColor;
    public TagEnemyStatus(double x=0.0, double y=0.0, double hit_size=32.0,
        int life=1,int graph_type=0, int wait_time=180, int bullet_pattern=0,
        int bullet_interval=60, int bullet_type=0, int bullet_color=0)
    {
        X = x;
        Y = y;
        HitSize = hit_size;
        Life = life;
        GraphType = graph_type;
        WaitTime = wait_time;
        BulletPattern = bullet_pattern;
        BulletInterval = bullet_interval;
        BulletType = bullet_type;
        BulletColor = bullet_color;
    }
}

敵を実際に生成する関数を定義

基本的にクラスはインスタンス化する前には、そのクラスのメンバ関数を呼び出すことはできないのですが、staticを付けて静的なメンバ関数をCEnemyの派生クラスに定義してやることで、自分のクラス自身を生成する関数を定義することができます。以下のNew関数の様に定義します。

public class CEnemy0 : CEnemy
{

    public CEnemy0(TagEnemyStatus enemy_status) 
        : base(enemy_status)
    {

    }
    public override void Move()
    {
    }

    public static void New(TagEnemyStatus enemy_status)
	  {
		    SGP.Enemy.Add(new CEnemy0(enemy_status));
    }
}

そして、Enemyクラスで定義したNew関数をデリゲートを使って、EnemyFunc関数の配列に入れてゲーム初期化時にEnemyFunc[i]の様にCEnemyCreateCommandに登録して、Run()関数のCreateEnemyFuncを呼び出すことで、指定したCEnemyの派生クラスのNew関数を呼んで、好きなタイプの敵キャラを生成することができます。

デリゲートとは

C++でいうところの関数ポインタです。デリゲートを使うことによって関数を入れておける変数のようなもので、間接的に呼び出すことができるようになります。
delegate 戻り値の型 メソッド型名(引数の型, …); で定義できます。

詳しくは以下の説明がわかりやすいと思います。

public delegate void CREATE_ENEMY_FUNC(TagEnemyStatus enemy_status);
public class CCommandManager
{
    private CREATE_ENEMY_FUNC[] EnemyFunc =
    {
        CEnemy0.New,
        CEnemy1.New,
        CEnemy2.New,
        CEnemy3.New,
        CEnemy4.New,
        CEnemy5.New
    };
}
public class CEnemyCreateCommand : ICommand
{
    private TagEnemyStatus EnemyStatus;
    CREATE_ENEMY_FUNC CreateEnemyFunc;
    public CEnemyCreateCommand(CREATE_ENEMY_FUNC func, TagEnemyStatus enemy_status)
    {
        CreateEnemyFunc = func;
        EnemyStatus = enemy_status;
    }

    public void Run()
    {
        CreateEnemyFunc(EnemyStatus);
    }
};

実際にプログラムを書き換える手順

まず引数をEnemyStatausの構造体にまとめたのでEnemyFactory.csを以下の様に変更してください。

弾幕生成クラスの書き換え

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static ShootingGame.CShootingGame;

namespace ShootingGame
{
    public abstract class CBulletFactory
    {
        protected TagEnemyStatus EnemyStatus;
        protected int Cnt;
        public CBulletFactory(TagEnemyStatus enemy_status)
        {
            Cnt = 0;
            EnemyStatus = enemy_status;
        }
        public virtual void Run(double x, double y) { }
    }

    public class CAimBulletFactory : CBulletFactory
    {
        public CAimBulletFactory(TagEnemyStatus enemy_status) : base(enemy_status)
        {

        }
        public override void Run(double x, double y)
        {
            if (Cnt % 24 == 0)
            {
                SGP.Bullet.Add(new CBullet(x:x, y:y, speed:2.5, angle:SGP.GetPlayerAngle(x, y), graph_type: EnemyStatus.BulletType, color: EnemyStatus.BulletColor));
            }
            Cnt++;
        }
    }
}

敵クラスの書き換え

CEnemyクラスは基底クラスで直接呼び出すことをできないようにabstractにしてます。
ここでは、CEnemy0~CEnemy5の6種類の行動パターンを用意しました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;
using System.Windows.Forms;
using static ShootingGame.CShootingGame;

namespace ShootingGame
{
    public struct TagEnemyStatus
    {
        public double X, Y, HitSize;
        public int Life, GraphType, WaitTime, BulletPattern;
        public int BulletInterval, BulletType, BulletColor;
        public TagEnemyStatus(double x=0.0, double y=0.0, double hit_size=32.0,
            int life=1,int graph_type=0, int wait_time=180, int bullet_pattern=0,
            int bullet_interval=60, int bullet_type=0, int bullet_color=0)
        {
            X = x;
            Y = y;
            HitSize = hit_size;
            Life = life;
            GraphType = graph_type;
            WaitTime = wait_time;
            BulletPattern = bullet_pattern;
            BulletInterval = bullet_interval;
            BulletType = bullet_type;
            BulletColor = bullet_color;
        }
    }
    public abstract class CEnemy : CMover
    {
        protected int Muki;
        protected int ImageCnt = 0;
        protected int Score = 100;
        protected CBulletFactory BulletFactory = null;
        protected bool BulletDischarge; // 弾幕発射フラグ
        protected TagEnemyStatus EnemyStatus;

        public CEnemy(TagEnemyStatus enemy_status)
            : base(enemy_status.X, enemy_status.Y, enemy_status.Life, -enemy_status .HitSize/ 2, -enemy_status.HitSize/ 2, enemy_status.HitSize/ 2, enemy_status.HitSize/ 2)
        {      
            Muki = 1;
            EnemyStatus = enemy_status;
            BulletDischarge = false;
            CreateBulletFactory(enemy_status.BulletPattern);
        }
        public virtual void Move()
        {
            if (X < 10 + 20 || // 左座標の上限
                X > FIELD_MAX_X - 10 + 42 || // 右座標の上限
                Y < 0 || // 上座標の上限
                Y > FIELD_MAX_Y - 5 + 20) // 下座標の上限
            {
                Life = 0;
            }

            if (BulletFactory != null && BulletDischarge)
            {
                BulletFactory.Run(X, Y);
            }

            ++Cnt;
        }
        public virtual void Draw()
        {
            // 敵を描画
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, SGP.ImageEnemy[ImageCnt], 1);
            // 当たり判定を描画
            //DX.DrawBox((int)(X + L), (int)(Y + T), (int)(X + R), (int)(Y + B), DX.GetColor(0, 0, 255), 1);
        }
        public void CreateBulletFactory(int bullet_pattern)
        {
            switch (bullet_pattern)
            {
                case 0:
                    BulletFactory = new CAimBulletFactory(EnemyStatus);
                    break;
            }
        }
    }

    public class CEnemy0 : CEnemy
    {
  
        public CEnemy0(TagEnemyStatus enemy_status) 
            : base(enemy_status)
        {

        }
        public override void Move()
        {
            if (Cnt < 60)
            {
                Y += 2.0;
            }
            else if (Cnt < 300)
            {
                BulletDischarge = true;
            }
            if (Cnt > 300 + 240)
            {
                BulletDischarge = false;
                Y -= 2.0;
            }

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }

        public static void New(TagEnemyStatus enemy_status)
	    {
		    SGP.Enemy.Add(new CEnemy0(enemy_status));
        }
    }

    // 下がってきて停滞して上がっていく
    public class CEnemy1 : CEnemy
    {
        int WaitTime;
        public CEnemy1(TagEnemyStatus enemy_status)
            : base(enemy_status)
        {
            WaitTime = enemy_status.WaitTime;
        }
        public override void Move()
        {
            int act1_time = 50;
            int act2_time = 120;

            if (Cnt < act1_time)
            {
                Y += 2.0f;
            }
            else if (Cnt < act1_time + WaitTime)
            {
                // 弾幕発射フラグをオン
                BulletDischarge = true;
            }
            else if (Cnt < act1_time + WaitTime + act2_time)
            {
                // 弾幕発射フラグをオフ
                BulletDischarge = false;
                Y -= 2.0f;
            }

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }
        public static void New(TagEnemyStatus enemy_status)
        {
            SGP.Enemy.Add(new CEnemy1(enemy_status));
        }
    }

    //下がってきて停滞して左下に行く
    public class CEnemy2 : CEnemy
    {
        public CEnemy2(TagEnemyStatus enemy_status)
            : base(enemy_status)
        {

        }
        public override void Move()
        {
            int act1_time = 40;
            int wait_time = 40;
            //int act2_time = 120;
            if (Cnt < act1_time)
            {
                Y += 2.0f;
            }
            else if (Cnt < act1_time + wait_time)
            {
                BulletDischarge = true;
            }
            else //if( Time < act1_time+wait_time+act2_time )
            {
                X -= 1.5f;
                Y += 2.8f;
            }

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }
        public static void New(TagEnemyStatus enemy_status)
        {
            SGP.Enemy.Add(new CEnemy2(enemy_status));
        }
    }

    //下がってきて停滞して右下に行く
    public class CEnemy3 : CEnemy
    {
        int WaitTime;
        public CEnemy3(TagEnemyStatus enemy_status)
            : base(enemy_status)
        {

        }

        public override void Move()
        {
            int act1_time = 40;
            int wait_time = 40;
            //int act2_time = 120;
            if (Cnt < act1_time)
            {
                Y += 2.0f;
            }
            else if (Cnt < act1_time + wait_time)
            {
                BulletDischarge = true;
            }
            else //if( Time < act1_time+wait_time+act2_time )
            {
                X += 1.5f;
                Y += 2.8f;
            }

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }
        public static void New(TagEnemyStatus enemy_status)
        {
            SGP.Enemy.Add(new CEnemy3(enemy_status));
        }
    }
    //すばやく降りてきて左へ
    public class CEnemy4 : CEnemy
    {
        int WaitTime;
        public CEnemy4(TagEnemyStatus enemy_status)
            : base(enemy_status)
        {

        }

        public override void Move()
        {
            if (Cnt == 0)
            {
                VY = 3.0f;
            }
            else if (Cnt == 30) // 途中で左向きに
            {
                BulletDischarge = true;
            }

            if (Cnt < 460)
            {
                VX -= 3.0f / 100.0f;//左向き加速
                VY -= 3.0f / 100.0f;//減速
            }
            X += VX;
            Y += VY;

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }
        public static void New(TagEnemyStatus enemy_status)
        {
            SGP.Enemy.Add(new CEnemy4(enemy_status));
        }
    }
    //すばやく降りてきて右へ
    public class CEnemy5 : CEnemy
    {
        int WaitTime;
        public CEnemy5(TagEnemyStatus enemy_status)
            : base(enemy_status)
        {

        }

        public override void Move()
        {
            if (Cnt == 0)
            {
                VY = 5.0f;
            }
            else if (Cnt == 30) // 途中で左向きに
            {
                BulletDischarge = true;
            }

            if (Cnt < 460)
            {
                VX += 5.0f / 100.0f;//右向き加速
                VY -= 5.0f / 100.0f;//減速
            }
            X += VX;
            Y += VY;

            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;

            // 基底クラスのMove関数を呼び出す
            base.Move();
        }
        public static void New(TagEnemyStatus enemy_status)
        {
            SGP.Enemy.Add(new CEnemy5(enemy_status));
        }
    }
}

そして、コマンドパターンで1番重要なCCommand.csのすべてを載せておきます。

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

namespace ShootingGame
{
    // デリゲートを使用するときに↓にpublicがないとエラーがでて引数に渡すことができないので注意する
    public delegate void CREATE_ENEMY_FUNC(TagEnemyStatus enemy_status);

    public interface ICommand
    {
        void Run();
    };
    // 待機コマンド
    public class CWaitCommand : ICommand
    {
        private CCommandManager CommandManager;
        private int WaitTime;
        public CWaitCommand(CCommandManager game_control, int wait_time)
        {
            CommandManager = game_control;
            WaitTime = wait_time;
        }

        public void Run()
        {
            CommandManager.WaitTime = WaitTime;
        }
    };
    // 敵生成コマンド
    public class CEnemyCreateCommand : ICommand
    {
        private TagEnemyStatus EnemyStatus;
        CREATE_ENEMY_FUNC CreateEnemyFunc;
        public CEnemyCreateCommand(CREATE_ENEMY_FUNC func, TagEnemyStatus enemy_status)
        {
            CreateEnemyFunc = func;
            EnemyStatus = enemy_status;
        }

        public void Run()
        {
            CreateEnemyFunc(EnemyStatus);
        }
    };
    public class CCommandManager
    {
        public CCommandManager()
        {
            Initalize();
        }
        private CREATE_ENEMY_FUNC[] EnemyFunc =
        {
            CEnemy0.New,
            CEnemy1.New,
            CEnemy2.New,
            CEnemy3.New,
            CEnemy4.New,
            CEnemy5.New
        };
        string[] EnemyName =
        {
            "enemy0",
            "enemy1",
            "enemy2",
            "enemy3",
            "enemy4",
            "enemy5",
        };
        private List<ICommand> Command = new List<ICommand>();
        private int CommandIndex { set; get; }
        public int WaitTime { set; get; }
        public void Initalize() 
        {
            Command.Clear();
            CommandIndex = 0;
            WaitTime = 0;
        }
        public void Run()
        {
            if (WaitTime > 0) WaitTime--;

            while (CommandIndex < Command.Count && WaitTime == 0)
            {
                Command[CommandIndex].Run();
                CommandIndex++;
            }
        }
        public bool LoadScript(string file_name)
        {
            CommandIndex = 0;
            WaitTime = 0;

            int fh = DX.FileRead_open(file_name);
            if(fh == 0)
            {
                return false;
            }
            bool comment = false;
            int cnt = 0; // 行数カウント
            while (DX.FileRead_eof(fh) == 0)
            {
                StringBuilder sb = new StringBuilder(256);
                DX.FileRead_gets(sb, 256, fh);
                if (string.IsNullOrWhiteSpace(sb.ToString())) continue;
                if (sb.ToString().Substring(0, 2) == "//") continue;
                System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;
                string[] param = sb.ToString().Split(new char[] { ',', ' ' }, option);
                if (param.Length <= 0) continue;

                if (param[0] == "*/") comment = false;
                else if (param[0] == "/*") comment = true;
                if (comment) continue;

                List<string> error_string = new List<string>();
                if (param[0] == "enemy")
                {
                    if (param.Length < 2)
                    {
                        error_string.Add($"{file_name}{cnt}行目のenemyコマンドのパラメーターが足りません");
                        continue;
                    }
                    for (int j = 0, jn = EnemyName.Length; j < jn; j++)
                    {
                        if (param[1] == EnemyName[j])
                        {
                            TagEnemyStatus enemy_status = new TagEnemyStatus();
							for (int k = 2; k < param.Length - 1; k += 2)
							{
								if (param[k] == "ex")
								{
									enemy_status.X = double.Parse(param[k + 1]);
								}
								else if (param[k] == "ey")
								{
									enemy_status.Y = double.Parse(param[k + 1]);
								}
								else if (param[k] == "hit_s")
								{
									enemy_status.HitSize = double.Parse(param[k + 1]);
								}
								else if (param[k] == "life")
								{
									enemy_status.Life = int.Parse(param[k + 1]);
								}
								else if (param[k] == "w_time")
								{
									enemy_status.WaitTime = int.Parse(param[k + 1]);
								}
								else if (param[k] == "bl_pat")
								{
									enemy_status.BulletPattern = int.Parse(param[k + 1]);
								}
								else if (param[k] == "bl_type")
								{
									enemy_status.BulletType = int.Parse(param[k + 1]);
								}
								else if (param[k] == "bl_col")
								{
									enemy_status.BulletColor = int.Parse(param[k + 1]);
								}
								else if (param[k] == "bl_int")
								{
									enemy_status.BulletInterval = int.Parse(param[k + 1]);
								}
								else if (param[k] == "gr_type")
								{
									enemy_status.GraphType = int.Parse(param[k + 1]);
								}

							}
                            Command.Add(new CEnemyCreateCommand(EnemyFunc[j], enemy_status));
                        }
                    }
                }
                else if( param[0] == "wait")
                {
                    if (param.Length < 2)
                    {
                        error_string.Add($"{file_name}{cnt}行目のwaitコマンドのパラメーターが足りません");
                        continue;
                    }
                    Command.Add(new CWaitCommand(this, int.Parse(param[1])));
                }
            }
            return true;
        }
    }
}

以下実際に読むテキストの内容です。

wait 120
enemy enemy2 ex 200 ey 0 hit_s 12 life 5 gr_type 0 w_time 60 bl_pat 0 bl_type 5 bl_col 0

先頭にコマンド名 値という内容になってます。
コマンド名はwait、値は120

  • enemyはコマンド名
  • enemy2は敵の種類
  • exは敵を生成するX座標
  • eyは敵を生成するY座標
  • hit_sは敵の当たり判定
  • lifeは敵の生命力
  • gr_typeは弾のグラフィックのタイプ
  • w_timeは停止する敵の場合、停止する時間
  • bl_patは弾幕生成の種類(BulletFactoryの種類を決定)
  • bl_typeは弾の画像のタイプ
  • bl_colは弾の色
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using DxLibDLL;
using System.Windows.Forms;

namespace ShootingGame
{
    public class CShootingGame
    {
        public const double PI = 3.1415926535898;
        public const double PI2 = PI * 2;
        public const int FIELD_X = 32;
        public const int FIELD_Y = 16;
        public const int FIELD_MAX_X = 384;
        public const int FIELD_MAX_Y = 448;
        // 実装は外部から隠蔽(privateにしておく)
        private static CShootingGame ShootingGame; // staticで変数を宣言
        // 変数の取得・変更用のプロパティ
        public static CShootingGame SGP
        {
            set { ShootingGame = value; }
            get { return ShootingGame; }
        }
        public List<CPlayer> Player = new List<CPlayer>();
        public List<CEnemy> Enemy = new List<CEnemy>();
        public List<CBackGround> BackGround = new List<CBackGround>();
        public List<CShot> Shot = new List<CShot>();
        public List<CBullet> Bullet = new List<CBullet>();
        public int[] ImagePlayer = new int[12]; // プレイヤー画像のイメージハンドル
        public int[] ImageBack = new int[4]; // 背景画像のイメージハンドル
        public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドル
        public int[] ImageShot = new int[5]; // ショット画像のイメージハンドル
        public int[][] ImageBullet = new int[20][];
        public CCommandManager CommandManager = new CCommandManager();
        // 初期化関数
        public void Initalize() 
        {
            string file_name = "dat/stage/stage1.txt";
            // ここに初期化処理を追加する
            if (!CommandManager.LoadScript(file_name))
            {
                MessageBox.Show($"{file_name}が開けませんでした");               
                DX.WaitKey();
                DX.DxLib_End();
                return;
            }
            // 自キャラ画像の読み込み
            if (DX.LoadDivGraph("dat/img/char/0.png", 12, 4, 3, 73, 73, ImagePlayer) == -1)
            {
                MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
            }

            // 敵画像読み込み
            if (DX.LoadDivGraph("dat/img/enemy/0.png", 9, 3, 3, 32, 32, ImageEnemy) == -1)
            {
                MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
            }

            // スコアボード読み込み
            int[] index = { 10, 11, 12, 20 };
            for (int i = 0; i < ImageBack.Length; ++i)
            {
                ImageBack[i] = DX.LoadGraph($"dat/img/board/{index[i]}.png");
                if (ImageBack[i] == -1)
                {
                    MessageBox.Show($"BackGroundの{i}番目のImageが読めこめません");
                }
            }
            // ショット画像の読み込み
            string[] img_str = new string[] { "bl_00", "bl1", "bl_01", "bl_011", "bl_11" };
            for(int i = 0; i < img_str.Length; ++i)
            {
                ImageShot[i] = DX.LoadGraph($"dat/img/char/{img_str[i]}.png");
                if (ImageShot[i] == -1)
                {
                    MessageBox.Show($"ImageShotの{i}番目のImageが読めこめません");
                }
            }
            ImageBullet[0] = new int[5];
            ImageBullet[1] = new int[6];
            ImageBullet[2] = new int[10];
            ImageBullet[3] = new int[5];
            ImageBullet[4] = new int[10];
            ImageBullet[5] = new int[3];
            ImageBullet[6] = new int[3];
            ImageBullet[7] = new int[10];
            ImageBullet[8] = new int[10];
            ImageBullet[9] = new int[3];
            ImageBullet[10] = new int[8];
            ImageBullet[11] = new int[8];
            ImageBullet[12] = new int[10];
            ImageBullet[13] = new int[10];
            ImageBullet[14] = new int[4];
            ImageBullet[15] = new int[3];
            if (DX.LoadDivGraph("dat/img/bullet/b0.png", 5, 5, 1, 76, 76, ImageBullet[0]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b1.png", 6, 6, 1, 22, 22, ImageBullet[1]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b2.png", 10, 10, 1, 5, 120, ImageBullet[2]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b3.png", 5, 5, 1, 19, 34, ImageBullet[3]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b4.png", 10, 10, 1, 38, 38, ImageBullet[4]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b5.png", 3, 3, 1, 14, 16, ImageBullet[5]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b6.png", 3, 3, 1, 14, 18, ImageBullet[6]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b7.png", 10, 10, 1, 16, 16, ImageBullet[7]) == -1) MessageBox.Show("画像ファイル読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b8.png", 10, 10, 1, 12, 18, ImageBullet[8]) == -1) MessageBox.Show("b8.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b9.png", 3, 3, 1, 13, 19, ImageBullet[9]) == -1) MessageBox.Show("b9.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b10.png", 8, 8, 1, 8, 8, ImageBullet[10]) == -1) MessageBox.Show("b10.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b11.png", 8, 8, 1, 35, 32, ImageBullet[11]) == -1) MessageBox.Show("b11.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b12.png", 10, 10, 1, 12, 12, ImageBullet[12]) == -1) MessageBox.Show("b12.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b13.png", 10, 10, 1, 22, 22, ImageBullet[13]) == -1) MessageBox.Show("b13.png読み込み失敗");
            if (DX.LoadDivGraph("dat/img/bullet/b14.png", 4, 4, 1, 6, 6, ImageBullet[14]) == -1) MessageBox.Show("b14.png読み込み失敗");//(56)
            if (DX.LoadDivGraph("dat/img/bullet/b15.png", 3, 3, 1, 19, 18, ImageBullet[15]) == -1) MessageBox.Show("b15.png読み込み失敗");//花弾

            string[] sound_str = {
                "enemy_shot.wav", "enemy_death.wav", "cshot.wav",
                "char_death.wav", "b_change.wav", "power.wav",
                "hit.wav", "boss_death.wav","boss_change.wav"
                    ,"bom0.wav", "bom1.wav","lazer.wav",
                "item_get.wav","shot3.ogg","final_hit.wav" }; 

            for (int i = 0; i < sound_str.Length; ++i)
            {
                SoundSE[i] = DX.LoadSoundMem($"dat/se/{sound_str[i]}");
                if(SoundSE[i] == -1)
                {
                    MessageBox.Show($"dat/se/{sound_str[i]}が読み込めません");
                }
            }

            Player.Add(new CPlayer(FIELD_MAX_X/2, 240, 1));
            BackGround.Add(new CBackGround());
        }
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            CommandManager.Run();
            // ここに処理を追加する
            // Lifeが0より多いなら処理する
            foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();         
            foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Move();
            foreach (var shot in Shot.Where(x => 0 < x.Life)) shot.Move();
            foreach (var bullet in Bullet.Where(x => 0 < x.Life)) bullet.Move();
            foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Move();

            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 shot in Shot.Where(x => 0 < x.Life)) shot.Draw();
            foreach (var bullet in Bullet.Where(x => 0 < x.Life)) bullet.Draw();
            foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Draw();

            // 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);
            BackGround.RemoveAll(s => s.Life <= 0);

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

            DX.DxLib_End();
        }
        int[] SoundSE = new int[15];
        public void PlaySound(int type)
        {
            if (DX.CheckSoundMem(SoundSE[type]) != 0)
            {
                DX.StopSoundMem(SoundSE[type]);
            }
            DX.PlaySoundMem(SoundSE[type], DX.DX_PLAYTYPE_BACK);
        }

        // プレイヤーまでの角度を計算する
        public double GetPlayerAngle(double x, double y)
        {           
            if (0 < Player.Count)
            {
                // 一番近くのプレイヤーの要素を取得する
                var player = Player.OrderByDescending(p => (p.X-x)*(p.X-x) + (p.Y-y)*(p.Y-y)).FirstOrDefault();
                return Math.Atan2(player.Y - y, player.X - x) / PI2;
            }
            else
            {
                return 0.0;
            }
        }
    }
}

実行結果

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

これで敵をファイルから読み込んで出現させることができるようになりました。次回は背景やBGMなどを追加してゲームらしく仕上げていきましょう。