アマゾンバナーリンク

弾幕シューティングゲーム制作その12 オプションとパワーアップとホーミングショットの追加

前回スコアボードの表示の追加までやりました。ここでボスの追加して一通りのゲームを作ろう!!!って思ったのですが、オプション機能を追加するのを忘れてたので、今回それらをやります。

オプション画像の読み込み

まずはいつも通り画像の読み込み処理を追加します。

ImageOption = DX.LoadGraph("dat/img/char/ball.png");
if(ImageOption == 1)
{
    MessageBox.Show("dat/img/char/ball.pngが開けませんでした");
}

オプションから敵への角度を取得する関数

オプションを作る前にオプションから敵への角度が必要になるので追加します。

// 一番近い敵の角度を計算する
public double GetEnemyAngle(double x, double y)
{
    if (0 < Enemy.Count)
    {
        // 一番近くのプレイヤーの要素を取得する
        var enemy = Enemy.OrderByDescending(p => (p.X - x) * (p.X - x) + (p.Y - y) * (p.Y - y)).FirstOrDefault();
        return Math.Atan2(enemy.Y - y, enemy.X - x) / PI2;
    }
    else // 敵が1匹もいなかったら真上の角度を返す
    {
        return 0.75;
    }
}

オプションとショットの追加

この処理を追加する際にクラスの構造も変えました。
具体的には、今まで使っていた、CPlayerを抽象クラスにして、派生クラスでCNormalPlayerを定義することによって、操作状態のプレイヤーや復帰する途中の無敵状態のプレイヤーを分岐して生成することができるようになりました。

オプションに関する処理は、新しいので、ラインを引いておきます。
CNormalPlayerの内部クラスにしてるところ以外は、今まで追加してきた処理と特に変わりはないです。
Vキーで低速移動中には、オプションの位置を中央に寄せるようにしてます。(152-160の処理)

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 abstract class CPlayer : CMover
    {
        protected int ImageCnt = 0;
        protected double Alpha = 255.0;
        public CPlayer(double x, double y, int life) : base(x, y, life, -5, -5, 5, 5)
        {

        }
        virtual public void Move()
        {
          
        }
        virtual public void Draw()
        {
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_ALPHA, (int)Alpha);
            // 自キャラの描画
            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);
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);            
            // X,Y座標の描画
            DX.DrawString(35, 15, $"X{X},Y{Y},Cnt{Cnt}", DX.GetColor(255, 255, 255));
        }

        protected void CalcApha()
        {
            Alpha = 130.0 + 130.0 * Math.Sin(Cnt * 0.15);
            if (Alpha > 255)
            {
                Alpha = 255;
            }
            if (Alpha < 0)
            {
                Alpha = 0;
            }
        }
    }
    public class CNormalPlayer : CPlayer
    {
        public CNormalPlayer(double x, double y, double angle = 0.0, bool muteki_falg = false) : base(x: x, y: y, life: 1)
        {
            Sqrt2 = 1 / Math.Sqrt(2.0);
            MutekiFlag = muteki_falg;
            NumOptions = 4;
            if (NumOptions > 4) NumOptions = 4;

            for (int i = 0; i < MAX_OPTIONS; i++)
            {
                Option.Add(new COption(X, Y, i * (1.0 / MAX_OPTIONS) + angle));
            }
        }
        const int MAX_OPTIONS = 4;
        protected bool MutekiFlag;
        double Sqrt2;

        // 味方機
        public class COption
        {
            double X, Y, Angle;
            public COption(double x, double y, double angle)
            {
                X = x; Y = y; Angle = angle;
            }

            public void MoveTo(double x, double y, int dist)
            {
                //角度をラジアン単位に変換する
                double rad = Angle * PI2;

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

                Angle += 0.005;
            }
            public void Draw()
            {
                DX.DrawRotaGraphF((float)X, (float)Y, 1.0, 0, SGP.ImageOption, 1);
            }
            public void Shot()
            {
                SGP.ShotBuf.Add(new CShot(X, Y, SGP.GetEnemyAngle(X, Y), 10, 2));
            }
        }

        List<COption> Option = new List<COption>();
        // 全味方機を移動させる(ショット時)
        void MoveOptionsToShotPosition()
        {
            // 画面上に出現している味方機を移動
            foreach(var option in Option) option.MoveTo(X, Y, 60);
        }
        // 全味方機を移動させる(ビーム時)
        void MoveOptionsToBeamPosition()
        {
            // 画面上に出現している味方機を移動
            foreach(var option in Option) option.MoveTo(X, Y, 30);
        }
        // 味方機の数
        int NumOptions;
        public CNormalPlayer(double x, double y, int life, double angle = 0.0, bool muteki_falg = false) : base(x, y, life)
        {
            Sqrt2 = 1 / Math.Sqrt(2.0);
            MutekiFlag = muteki_falg;
            NumOptions = 4;
            if (NumOptions > 4) NumOptions = 4;

            for (int i = 0; i < MAX_OPTIONS; i++)
            {
                Option.Add(new COption(X, Y, i * (1.0 / MAX_OPTIONS) + angle));
            }
        }
        public override 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;
            }

            // 味方機の移動
            if (GetPadKey(EINPUT_TYPE.Slow) > 0)//低速移動中なら
            {
                MoveOptionsToBeamPosition();
            }
            else
            {
                MoveOptionsToShotPosition();
            }

            // 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;
            }

            // 無敵状態なら
            if (MutekiFlag)
            {
                CalcApha();
                if (Cnt > 140)
                {
                    Alpha = 255.0;
                    MutekiFlag = false;
                }
            }
            else
            {
                // 敵の弾に当たったら
                foreach (var bullet in SGP.Bullet)
                {
                    if (Hit(bullet))
                    {
                        //bullet.Life = 0;
                        Life = 0;
                        Option.Clear();
                        // プレイヤーやられ音
                        SGP.PlaySound(3);
                        // 残機がなくなったら
                        if (--SGP.PlayerNum < 0)
                        {

                        }
                        else // 残機があるなら復活
                        {
                            SGP.EffectBuf.Add(new CPlayerClashEffect(FIELD_MAX_X / 2 + 20, FIELD_MAX_Y));
                        }
                        break;
                    }
                }
            }
            if (Y < 150)
            {
                foreach (var item in SGP.Item)
                {
                    item.FastFlag = true;
                }
            }
            else
            {
                foreach (var item in SGP.Item)
                {
                    item.FastFlag = false;
                }
            }

            Cnt++;
        }
        public override void Draw()
        {
            base.Draw();
            foreach (var option in Option) option.Draw();
        }

        int[] CShot0Num = { 2, 4 };
        int[] CShot0Pos_X = { -10, 10, -30, 30 };
        int[] CShot0Pos_Y = { -30, -30, -10, -10 };
        // 通常ショット登録
        void HiSpeedShot()
        {
            for (int i = 0; i < CShot0Num[SGP.Power < 200 ? 0 : 1]; ++i)
            {
                SGP.ShotBuf.Add(new CShot(X + CShot0Pos_X[i], Y + CShot0Pos_Y[i], 0.75f, 10));
            }
            foreach (var option in Option) option.Shot();
        }
        //低速通常ショット登録
        void LowerSpeedShot()
        {
            for (int i = 0; i < CShot0Num[SGP.Power < 200 ? 0 : 1]; ++i)
            {
                SGP.ShotBuf.Add(new CShot(X + CShot0Pos_X[i] / 3, Y + CShot0Pos_Y[i] / 2, 0.75f, 10));
            }
            foreach (var option in Option) option.Shot();
        }
    }
    public class CRevivalPlayer : CPlayer
    {
        public CRevivalPlayer(double x, double y) : base(x:x, y:y, life:1)
        {

        }
        public override void Move()
        {   // 自機を点滅させる
            CalcApha();

            Cnt++;
            if (Cnt > 120)
            {               
                Life = 0;
                SGP.PlayerBuf.Add(new CNormalPlayer(x:X, y:Y, life:1, muteki_falg:true));
            }
            else
            {
                Y--;
            }
        }
    }
}

敵の弾が外にでたら消す処理を追加

前回まで、ずっと敵の弾を画面外にでたら、消す処理を忘れてたので追加します><
CEnemyクラスのMove関数です。追加する部分にハイライトしときます。

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

実行結果

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

これでオプションが追加できました。次こそいよいよボスを作ってゲームの骨格を完成させる予定です。