アマゾンバナーリンク

弾幕シューティングゲーム制作その9 エフェクトの追加

あけましておめでとうございます!みなさんはいかがお過ごしでしょうか?
私は年末年始もいつもと変わらず引きこもってゲーム制作です。
前回はlistのバッファを作り、自キャラがやられた時に復活する処理を追加しました。
今回はエフェクトを追加します。

エフェクトの作成

まずは、エフェクトクラスを作成します。もうお決まりのパターンで、エフェクトクラスにMoveとDraw関数を用意して、派生クラスでオーバーライドします。それではさっそくCEffect.csを新規作成して以下のコードを追加しましょう。

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

namespace ShootingGame
{
    public class CEffect : CMover
    {
        public CEffect(double x, double y) : base(x, y, 1, 0, 0, 0, 0)
        {

        }
        public virtual void Move()
        {

        }
        public virtual void Draw()
        {

        }
    }
    // 敵エフェクト
    public class CEnemyCrashEffect : CEffect
    {
        int Bright, Color;
        double Size, Angle;
        public CEnemyCrashEffect(double x, double y, int color) : base(x, y)
        {
            Angle = SGP.Rang(PI);
            Color = color;
            Bright = 255;
            Size = 0.0;
        }
        public override void Move()
        {
            Cnt++;
            Size += 0.08f;
            if (Cnt > 10)
            {
                Bright -= 25;
            }
            if (Cnt > 20)
            {
                Life = 0;
            }
        }
        public override void Draw()
        {
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_ADD, Bright);
            DX.DrawRotaGraphF((float)X, (float)Y, Size, Angle, SGP.ImageHitEffect[Color], 1);
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);
        }
    }
    // 敵エフェクトを生成する工場
    public class CEnemyCrashEffectFactory : CEffect
    {
        int Color;
        public CEnemyCrashEffectFactory(double x, double y) : base(x, y)
        {
            SGP.PlaySound(1); // 敵やられ音
            Color = DX.GetRand(4);
        }
        public override void Move()
        {
            if (Cnt % 2 == 0)
            {
                SGP.EffectBuf.Add(new CEnemyCrashEffect(X, Y, Color));
            }
            if (Cnt > 8)
            {
                Life = 0;
            }
            Cnt++;
        }
    }
    // プレイヤーがやられた時のエフェクト
    public class CPlayerClashEffect: CEffect
    {
        public CPlayerClashEffect(double x, double y) : base(x, y)
        {
            // プレイヤーやられ音
            SGP.PlaySound(3);
        }
        public override void Move()
        {
            Cnt++;
            if (Cnt > 50)
            {
                SGP.PlayerBuf.Add(new CRevivalPlayer(X, Y));
                Life = 0;
            }
        }
    }
}

敵のエフェクトを生成する時に、直接生成するのではなく、エフェクトを生成する工場を生成して、そこから実際のエフェクトを生成する仕様にしてます。具体的には

  1. 敵のHPが0になる
  2. CEnemyCrashEffectFactoryを生成
  3. 2フレームに1度CEnemyCrashEffectを8フレームまで生成する

という流れになってます。

さらにプレイヤーも前回の処理では、直接CRevivalPlayerクラスを生成してましたが、CPlayerClashEffectを一度生成してから、CRevivalPlayerクラスを生成するように変更してます。
今の段階では、待ち時間を設定してるだけですが、後からエフェクトを追加したくなったら、すぐできるようにこの仕様にしました。

エフェクト用画像の読み込み

いつも通りLoadDivGraph関数を使います。以下をCShootingGame.csに追加します。

public int[] ImageHitEffect = new int[5]; // エフェクト画像のイメージハンドル
// 敵消滅エフェクトをInitialize関数に追加
if (DX.LoadDivGraph("dat/img/enemy/hit_effect.png", 5, 5, 1, 140, 140, ImageHitEffect) == -1) MessageBox.Show("hit_effect.png読み込み失敗");

またエフェクト用のリストも作成しMainLoop関数に以下を追加します。

public List<CEffect> Effect = new List<CEffect>();
public List<CEffect> EffectBuf = new List<CEffect>();
// MainLoopに追加
if (0 < EffectBuf.Count) { Effect.AddRange(EffectBuf); EffectBuf.Clear(); }
foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Move();
foreach (var effect in Effect.Where(x => 0 < x.Life)) effect.Draw();

ShootingGame.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 List<CEffect> Effect = new List<CEffect>();

        public List<CPlayer> PlayerBuf = new List<CPlayer>();
        public List<CEnemy> EnemyBuf = new List<CEnemy>();
        public List<CBackGround> BackGroundBuf = new List<CBackGround>();
        public List<CShot> ShotBuf = new List<CShot>();
        public List<CBullet> BulletBuf = new List<CBullet>();
        public List<CEffect> EffectBuf = new List<CEffect>();

        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 int[] ImageHitEffect = new int[5]; // エフェクト画像のイメージハンドル
        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読み込み失敗");//花弾
            // 敵消滅エフェクト
            if (DX.LoadDivGraph("dat/img/enemy/hit_effect.png", 5, 5, 1, 140, 140, ImageHitEffect) == -1) MessageBox.Show("hit_effect.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]}が読み込めません");
                }
            }
            PlayerBuf.Add(new CPlayer(FIELD_MAX_X/2, 240, 1));
            BackGround.Add(new CBackGround());
        }
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            CommandManager.Run();

            if (0 < PlayerBuf.Count) { Player.AddRange(PlayerBuf); PlayerBuf.Clear(); }
            if (0 < EnemyBuf.Count) { Enemy.AddRange(EnemyBuf); EnemyBuf.Clear(); }
            if (0 < ShotBuf.Count) { Shot.AddRange(ShotBuf); ShotBuf.Clear(); }
            if (0 < BulletBuf.Count) { Bullet.AddRange(BulletBuf); BulletBuf.Clear(); }
            if (0 < EffectBuf.Count) { Effect.AddRange(EffectBuf); EffectBuf.Clear(); }
            if (0 < BackGroundBuf.Count) { BackGround.AddRange(BackGroundBuf); BackGroundBuf.Clear(); }
            // ここに処理を追加する
            // 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 effect in Effect.Where(x => 0 < x.Life)) effect.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 effect in Effect.Where(x => 0 < x.Life)) effect.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);
            Effect.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;
            }
        }
        // 渡された-ang~angまでのランダムな角度を返す
        public double Rang(double angle)
        {
            return (double)(-angle + angle * 2 * DX.GetRand(10000) / 10000.0);
        }
    }
}

エフェクトを出す

ここまでで準備はできたので、実際に敵の消滅エフェクトを出す処理(ハイライトの部分)を追加しましょう。

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))
            {
                //SGP.PlaySound(14);
                if(--enemy.Life <= 0)
                {
                    SGP.EffectBuf.Add(new CEnemyCrashEffectFactory(enemy.X, enemy.Y));
                }
                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);
    }
}

   public class CPlayer : CMover
    {
        protected bool MutekiFlag;
        protected double Alpha = 255.0;
        double Sqrt2;
        int ImageCnt = 0;
        public CPlayer(double x, double y, int life, bool muteki_falg= false) : base(x, y, life, -5, -5, 5, 5)
        {
            Sqrt2 = 1 / Math.Sqrt(2.0);
            MutekiFlag = muteki_falg;
        }
        virtual 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;
            }
            
            // 無敵状態なら
            if (MutekiFlag)
            {
                CalcApha();
                if (Cnt > 140)
                {
                    MutekiFlag = false;
                }
            }
            else
            {
                // 敵の弾に当たったら
                foreach (var bullet in SGP.Bullet)
                {
                    if (Hit(bullet))
                    {
                        //bullet.Life = 0;
                        Life = 0;
                        SGP.EffectBuf.Add(new CPlayerClashEffect(FIELD_MAX_X / 2 + 20, FIELD_MAX_Y));
                        break;
                    }
                }
            }
            Cnt++;
        }
        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));
        }

        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.ShotBuf.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.ShotBuf.Add(new CShot(X + CShot0Pos_X[i] / 3, Y + CShot0Pos_Y[i] / 2, 0.75f, 10));
            }
        }
        protected void CalcApha()
        {
            Alpha = 130.0 + 130.0 * Math.Sin(Cnt * 0.15);
            if (Alpha > 255)
            {
                Alpha = 255;
            }
            if (Alpha < 0)
            {
                Alpha = 0;
            }
        }
    }

実行結果

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

これでエフェクト処理が簡単に追加できるようになりました。次回は背景のスクロール機能を実装します。