アマゾンバナーリンク

弾幕シューティングゲーム制作その5 敵のショットと敵との当たり判定とサウンドの追加

前回、自キャラのショットと敵との当たり判定追加までやりました。
今回と同じ様にで敵のショットと自キャラとの当たり判定とやられた時の処理まで追加します。

敵にショットを打たせる

敵ショット用のCBulletクラスを追加するのですが、その前に当たり判定を簡単に設定できる様にCMoverクラスに変更を加えます。

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

namespace ShootingGame
{
    public class CMover
    {
        protected double X { set; get; }
        protected double Y { set; get; }
        protected double L { set; get; }
        protected double T { set; get; }
        protected double R { set; get; }
        protected double B { set; get; }
        public int Life { set; get; }
        protected int Cnt { set; get; }
		public CMover(double x, double y, int life)
		{
			X = x;
			Y = y;
			Life = life;
			Cnt = 0;
		}
		// 弾幕用コンストラクタ
		public CMover(double x, double y, int life, int range_type)
        {
            X = x;
            Y = y;
            Life = life;
            Cnt = 0;
			switch (range_type)
			{
				case 0:
					L = -10.0;
					T = -10.0;
					R = 10.0;
					B = 10.0;
					break;
				case 1:
					L = -4.0;
					T = -4.0;
					R = 4.0;
					B = 4.0;
					break;
				case 2:
					L = -2.0;
					T = -10.0;
					R = 2.0;
					B = 10.0;
					break;
				case 3:
					L = -5.0;
					T = -5.0;
					R = 5.0;
					B = 5.0;
					break;
				case 4:
				case 5:
				case 6:
				case 7:
				case 8:
				case 9:
					L = -4.0;
					T = -4.0;
					R = 4.0;
					B = 4.0;
					break;
				case 10:
					L = -1.0;
					T = -1.0;
					R = 1.0;
					B = 1.0;
					break;
				case 11:
					L = -4.0;
					T = -4.0;
					R = 4.0;
					B = 4.0;
					break;
				case 12:
				case 13:
					L = -3.0;
					T = -3.0;
					R = 3.0;
					B = 3.0;
					break;
				case 14:
					L = -0.5;
					T = -0.5;
					R = 0.5;
					B = 0.5;
					break;
				case 15:
					L = -4.0;
					T = -4.0;
					R = 4.0;
					B = 4.0;
					break;
			}
		}
        public CMover(double x, double y, int life, double l, double t, double r, double b)
        {
            X = x;
            Y = y;
            Life = life;
            Cnt = 0;
            L = l;
            T = t;
            R = r;
            B = b;
        }
        // 当たり判定処理
        public bool Hit(CMover m)
        {
            return
                X + L < m.X + m.R && m.X + m.L < X + R &&
                Y + T < m.Y + m.B && m.Y + m.T < Y + B;
        }
        public bool Hit(CMover m, float l, float t, float r, float b)
        {
            return
                X + l < m.X + m.R && m.X + m.L < X + r &&
                Y + t < m.Y + m.B && m.Y + m.T < Y + b;
        }
    }
}

次に新規追加でCBullet.csを作成。

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

namespace ShootingGame
{
    public class CBullet : CMover
    {
        int Type, Color, Rotate;
        double Yaw, VYaw;// 回転角度
        double Angle, Speed;
        double AngleRate, SpeedRate;

        public CBullet(double x, double y, double angle, double angle_rate, double speed, double speed_rate,
                 int type, int color, int rotate) : base(x, y, 1, type)
        {
            Angle = angle;
            AngleRate = angle_rate;
            Speed = speed;           
            SpeedRate = speed_rate;
            Type = type;
            Color = color; 
            Rotate = rotate;
            Yaw = 0.0;
            VYaw = 0.0;
            SGP.PlaySound(0);
        }
        public void Move()
        {
            //弾を回転させる
            if (Rotate == 1) Yaw += VYaw;

            //角度をラジアン単位に変換する
            double rad = Angle * PI2;

            // 角度と速度を使って、座標を更新する
            X += Speed * Math.Cos(rad);
            Y += Speed * Math.Sin(rad);

            // 角度に角速度を加算する
            Angle += AngleRate;

            // 速度に加速度を加算する
            Speed += SpeedRate;
        }
        public void Draw()
        {
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0, (Angle + 0.25f + Yaw) * PI2, SGP.ImageBullet[Type][Color], 1);
            //DX.DrawBox((int)(X + L), (int)(Y + T), (int)(X + R), (int)(Y + B), DX.GetColor(0, 0, 255), 1);
        }
    }
}

31行目のSGP.PlaySound(0);は後でサウンドの追加の説明をした後に使えるようになるので、エラーが表示されてもコメントアウトしておいてください。

CBulletクラスについて

まずはコンストラクタの処理

  • double x, double yは敵の弾の座標
  • double angleは敵の弾の角度
  • double angle_rateは敵の弾の角度に加算する値
  • double speedは敵の弾の速度
  • double speed_rateは敵の弾の速度に加算する値
  • int typeは敵の弾の画像クラフィックハンドルとCMoverの引数に渡して当たり判定の広さを決める
  • int color 敵の弾の色
  • int rotateが1なら敵の弾を回転させる

Move関数について

  1. rotateが1ならYawにVYaw分だけ加算してグラフィックを回転させる
  2. Angleは0.0~1.0の範囲なので、PIの2倍をかけてラジアン単位に変換する
  3. ラジアン単位の角度を元に斜辺が1になるcosでx軸とsinでy軸を求める
  4. 3で求めた数にSpeedを掛けて速度を調整する
  5. 角度に角速度を加算する
  6. 速度に加速度を加算する

Draw関数について

DrawRotaGraphFについては何度も出ているのでこちらを参照してくだい。
DrawBoxをコメントアウトすると当たり判定が表示されます。

敵の弾の画像読み込み

毎回お決まりの画像読み込みの処理です。
音をならす処理も追加したので、CShootingGame.csを全部お見せします。

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 void Initalize() 
        {
            // ここに初期化処理を追加する

            // 自キャラ画像の読み込み
            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]}が読み込めません");
                }
            }
            // 音量調整
            //DX.ChangeVolumeSoundMem(32, SoundSE[0]);
            //DX.ChangeVolumeSoundMem(32, SoundSE[1]);
            //DX.ChangeVolumeSoundMem(32, SoundSE[2]);
            //DX.ChangeVolumeSoundMem(32, SoundSE[3]);
            //DX.ChangeVolumeSoundMem(32, SoundSE[6]);
            //DX.ChangeVolumeSoundMem(64, SoundSE[7]);
            //DX.ChangeVolumeSoundMem(64, SoundSE[8]);
            //DX.ChangeVolumeSoundMem(64, SoundSE[9]);
            //DX.ChangeVolumeSoundMem(64, SoundSE[10]);
            //DX.ChangeVolumeSoundMem(64, SoundSE[11]);
            //DX.ChangeVolumeSoundMem(32, SoundSE[12]);

            Player.Add(new CPlayer(320, 240, 1));
            BackGround.Add(new CBackGround());
        }
        int Cnt = 0;
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            if(Cnt == 0)
            {
                Enemy.Add(new CEnemy(FIELD_MAX_X / 2+20, 0, 20,-8.0, -8.0, 8.0, 8.0));
            }
            Cnt = (Cnt + 1) % 600; // 5秒に1回
            // ここに処理を追加する
            // 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);
        }
    }
}

敵の画像イメージハンドルを読み込む際には二次元配列ではなく、ジャグ配列を使っています。
二次元配列の方が高速ですが、配列の要素の配列の数が均一では、LoadDivGraphで読み込めないので、ジャグ配列を使ってます。詳しくは下の説明がわかりやすいです。

サウンドを鳴らす処理

まずLoadSoundMemでint型の変数に読みこんでから、 PlaySoundMemで実際に音を鳴らします。
画像を描画する時と同じ様な処理なのでわかりやすいです。

実際にSEをならす時にはPlaySound関数で音が保存されてるハンドルを指定して、CheckSoundMemで再生されているなら、StopSoundMemで再生中の音を停止してから、 PlaySoundMem で再生してます。

自キャラのショット音とやられた時の音を追加

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

namespace ShootingGame
{
    public class CPlayer : CMover
    {
        double VX { set; get; }
        double VY { set; get; }
        double Sqrt2;

        int ImageCnt = 0;
        public CPlayer(double x, double y, int life) : base(x, y, life, -5, -5, 5, 5)
        {
            Sqrt2 = 1 / Math.Sqrt(2.0);
        }
        public void Move()
        {
            // 速度
            VX = 4.0;
            VY = 4.0;
            // 低速キーを押してたら低速に(初期はVキー)
            if (0 < GetPadKey(EINPUT_TYPE.Slow))
            {
                VX = 1.8;
                VY = 1.8;
            }

            ImageCnt = (Cnt % 24) / 6;//現在の画像決定
            // 左右移動(初期は十字キー左右)
            if (0 < GetPadKey(EINPUT_TYPE.Left))
            {
                VX = -VX;
                ImageCnt += 4 * 2;//画像を左向きに
            }
            else if (0 < GetPadKey(EINPUT_TYPE.Right))
            {
                ImageCnt += 4 * 1;//画像を右向きに
            }
            else
            {
                VX = 0;
            }

            // Zキー押しっぱなしで6フレームに1度ショットを発射する
            if (0 < GetPadKey(EINPUT_TYPE.Shot) && GetPadKey(EINPUT_TYPE.Shot) % 6 == 0)
            {
                if (0 < GetPadKey(EINPUT_TYPE.Slow))//低速移動中なら
                {
                    LowerSpeedShot();
                }
                else
                {
                    HiSpeedShot();
                }
                // ショット音
                SGP.PlaySound(2);
            }

            // 上下移動(初期は十字キー上下)
            if (GetPadKey(EINPUT_TYPE.Up) > 0) VY = -VY; else if (GetPadKey(EINPUT_TYPE.Down) == 0) VY = 0;

            // 斜め方向の速度調節
            if (VX != 0 && VY != 0)
            {
                VX = Math.Floor(VX * Sqrt2 + 0.5);
                VY = Math.Floor(VY * Sqrt2 + 0.5);
            }

            // 移動
            X += VX;
            Y += VY;

            // 移動範囲の制限
            if (X < 10 + 20 || X > FIELD_MAX_X - 10 + 42 || Y < 5 + 8 || Y > FIELD_MAX_Y - 5 + 20)
            {
                X -= VX;
                Y -= VY;
            }

            // 敵の弾に当たったら
            foreach(var bullet in SGP.Bullet)
            {
                if(Hit(bullet))
                {
                    //bullet.Life = 0;
                    --Life;
                    // 自キャラやられ音
                    SGP.PlaySound(3);
                    break;
                }
            }

            Cnt++;
        }
        public void Draw()
        {
            // 自キャラの描画
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, SGP.ImagePlayer[ImageCnt], 1);
            DX.DrawBox((int)(X+L), (int)(Y+T), (int)(X+R), (int)(Y+B), DX.GetColor(255, 0, 0), 1);
            // X,Y座標の描画
            DX.DrawString(35, 15, $"X{X},Y{Y},Cnt{Cnt}", DX.GetColor(255, 255, 255));
        }

        int[] CShot0Num = { 2, 4 };
        int[] CShot0Pos_X = { -10, 10, -30, 30 };
        int[] CShot0Pos_Y = { -30, -30, -10, -10 };
        // 通常ショット登録
        void HiSpeedShot()
        {
            //	Power < 200 ?0 : 1
            for (int i = 0; i < CShot0Num[0]; i++)
            {
                SGP.Shot.Add(new CShot(X + CShot0Pos_X[i], Y + CShot0Pos_Y[i], 0.75f, 10));
            }
        }
        //低速通常ショット登録
        void LowerSpeedShot()
        {
            for (int i = 0; i < CShot0Num[1]; i++)
            {
                SGP.Shot.Add(new CShot(X + CShot0Pos_X[i] / 3, Y + CShot0Pos_Y[i] / 2, 0.75f, 10));
            }
        }
    }

    public class CShot : CMover
    {
        int ShotType;
        double Angle, Speed;
        public CShot(double x, double y, double angle, double speed, int shot_type=0) : base(x, y, 1, -6.5, -6.5, 6.5, 6.5)
        {
            Angle = angle;
            Speed = speed;
            ShotType = shot_type;
        }
        public void Move()
        {
            double rad = Angle * PI2;
            X += Math.Cos(rad) * Speed;
            Y += Math.Sin(rad) * Speed;
            // 敵との当たり判定
            foreach(var enemy in SGP.Enemy)
            {
                if(Hit(enemy))
                {
                    --enemy.Life;
                    //SGP.PlaySound(14);
                    Life = 0;
                }
            }
            // 画面外にでたら消す
            if(Y < 0)
            {
                Life = 0;
            }
        }
        public void Draw()
        {
            double rad = (Angle - 0.75f) * PI2;
            // 自機ショットを描画
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0, rad, SGP.ImageShot[ShotType], 1);
            // 自機ショットの当たり判定描画
            //DX.DrawBox((int)(X + L), (int)(Y + T), (int)(X + R), (int)(Y + B), DX.GetColor(255, 0, 0), 1);
        }
    }
}

敵の弾の発射音は一番最初に乗せたCBullet.csのSGP.PlaySound(0);で鳴らしてます。

実行結果

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

今回は敵のショットと当たり判定とサウンドの追加をしました。次回は敵のデータをファイルから読み込めるようにします。