アマゾンバナーリンク

弾幕シューティングゲーム制作その4 自キャラのショットと敵との当たり判定追加

2020年1月15日

自キャラのショットの追加

こんにちは!ジェイです。前回スコアボードと敵の出現までやったので、今回は自キャラのショットと敵との当たり判定をつけていきましょう。

CShotクラスの定義

自機のショットを制御するクラスCShotをCPlayer.csに追加します。CPlayerクラスの下に追加するとよいです。ShotTypeはショットの種類、Angleはショットの角度、Speedはショットの移動速度です。角度からコサインでX軸の値、サインでY軸の値を求めます。これで斜辺のが必ず1になっているので、それにスピードを掛け算すると1回に移動する距離が求められます。そして、その計算の結果を現在の位置に足すと移動します。

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)
    {
        Angle = angle;
        Speed = speed;
        ShotType = shot_type;
    }
    public void Move()
    {
        double rad = Angle * PI2;
        X += Math.Cos(rad) * Speed;
        Y += Math.Sin(rad) * Speed;
    }
    public void Draw()
    {
        double rad = (Angle - 0.75f) * PI2;
        // 自機ショットを描画
        DX.DrawRotaGraphF((float)X, (float)Y, 1.0, rad, SGP.ImageShot[ShotType], 1);
    }
}

ショット画像の読み込み

CShootingGame.csのCShootingGameクラスのInitalize関数にショット画像の読み込みを追加します。追加した部分はハイライトしてます。LoadDivGraphは何度も使ってるので特に変わったことはありません。

public int[] ImageShot = new int[5]; // ショット画像のイメージハンドル
// 初期化関数
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が読めこめません");
        }
    }

    Player.Add(new CPlayer(320, 240, 10));
    BackGround.Add(new CBackGround());
}

プレイヤーからショットを発射する

次にプレイヤーからショットを発射する処理を追加します。
だいぶ変更を加えたのでCPlayerクラス全部そのまま掲載します。

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)
    {
        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();
            }
        }

        // 上下移動(初期は十字キー上下)
        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;
        }
        Cnt++;
    }
    public void Draw()
    {
        // 自キャラの描画
        DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, SGP.ImagePlayer[ImageCnt], 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));
        }
    }
}

低速移動時には、Zキーを押してる間は、6フレームに1度LowerSpeedShot関数を通常移動時には、HiSpeedShot関数を呼びます。まだ実装してないのですが、パワーが上がるごとにショットの数も増やせるようにしてます。CShot0Num[Power < 200 ?0 : 1]とすることで、Powerが200より小さい時は0を大きい時は1を参照します。つまり、Power199まではfor文のループが2回で終わり、Powerが200以上になるとfor文のループが4回まで増えます。

ここまで追加して、実行すると、自キャラからショットが出るようになります。

自機ショットと敵との当たり判定の追加

当たり判定を行うには自機の座標X,Yを中心とした正方形を作ります。
XからLを引いて、YからTを引いた値が当たり判定の左上座標、XからRを足して、YからBを足した値が当たり判定の右下の座標です。

では、さっそく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, 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;
        }
    }
}

引数が3つと7つのコンストラクタ2つオーバーロードしてますが、これは当たり判定を必要ない場合と必要のある場合で初期化処理を分けてます。当たり判定処理を行うHit関数も受け取った相手の当たり判定の大きさを任意に変えられるものと、そうでないものの二つをオーバーロードして用意してます。

ショットと敵との当たり判定を行う部分

今回最初に追加したCPlayer.csのCShotクラスに敵との当たり判定を追加します。

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)
    {
        Angle = angle;
        Speed = speed;
        ShotType = shot_type;
        L = -6.5;
        T = -6.5;
        B = 6.5;
        R = 6.5;
    }
    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; // 敵のライフを1引く
                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);
    }
}

Lifeを0にすると消去されますが、実際にはCShootingGameクラスのMove関数のShot.RemoveAll(s => s.Life <= 0);で消してます。最後にゲームのメイン処理を行うCShootGame.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.1415926535898f;
        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 int[] ImagePlayer = new int[12]; // プレイヤー画像のイメージハンドル
        public int[] ImageBack = new int[4]; // 背景画像のイメージハンドル
        public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドル
        public int[] ImageShot = new int[5]; // ショット画像のイメージハンドル
        // 初期化関数
        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が読めこめません");
                }
            }

            Player.Add(new CPlayer(320, 240, 10));
            BackGround.Add(new CBackGround());
        }
        int Cnt = 0;
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            if(Cnt == 0)
            {
                Enemy.Add(new CEnemy(FIELD_MAX_X / 2, 0, 20,-8.0, -8.0, 8.0, 8.0));
            }
            Cnt = (Cnt + 1) % 300; // 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 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 back in BackGround.Where(x => 0 < x.Life)) back.Draw();

            // Lifeが0以下の要素を削除する
            Player.RemoveAll(s => s.Life <= 0);
            Shot.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();
        }
    }
}

実行結果

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

今回は自キャラショットと敵との当たり判定を付けるとこまでやりました。次回は敵にもショットを打たせてみましょう。