アマゾンバナーリンク

初心者弾幕講座その2 角度を変更する弾幕

こんにちは!ジェイです。前回に引き続き弾幕制作講座パート2をやっていきます。前回の内容で弾のを好きな方向に発射できるようになりました。今回は具体的な弾幕のコードを中心に説明していきます。

弾クラスのプログラム

以下は弾一発分を計算と描画するプログラムです。
Move関数の中身で前回説明したラジアンの数値がAngleに入ってます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static BulletCourse.CBulletCourse;
using DxLibDLL;
using System.Windows.Forms;

namespace BulletCourse
{
    public class CBullet : CMover
    {
        public double Speed { set; get; } = 0.0;
        public double Angle { set; get; } = 0.0;
        public int Color { set; get; }
        public int GraphType { set; get; }
        public int Rotate { set; get; }
        public double SpeedRate { set; get; }
        public double AngleRate { set; get; }
        public bool Kaiten { set; get; } = false;
        double Yaw, VYaw;// 回転角度
        double PI2or1, AddRotate, ExRate;
        bool BlendFlag;
   
        public CBullet(double x, double y, double speed = 3.0, double angle = 0,
         double speed_rate = 0.0, double angle_rate = 0.0,
         int graph_type = 0, int color = 0, double pi2or1 = PI2, double ex_rate=1.0,bool blend_flag = false) : base(x,y,1)
        {
            BlendFlag = blend_flag;
            Speed = speed;
            Angle = angle;
            SpeedRate = speed_rate;
            AngleRate = angle_rate;
            GraphType = graph_type;
            Color = color;
            ExRate = ex_rate;
            PI2or1 = pi2or1; // これにはPI2か1.0のどちらかを入れるようにする
            // SGP.GetPlayerAnglePI2を使った時には1.0を入れる事
            // GetPlayerAngle1を使った時にはPI2を入れる事
            if (PI2or1 == 1.0)
            {
                AddRotate = PI / 2;
            }
            else
            {
                AddRotate = 0.25;
            }
            Yaw = 0.0;
            VYaw = 0.0;

        }
        public bool Move()
        {    
           //弾を回転させる
            if (Rotate == 1) Yaw += VYaw;
            //角度をラジアン単位に変換する
            double rad = Angle * PI2or1;

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

			// 画面の外に出たなら消す
            if (X < 0|| // 左座標の上限
                X > FMX - 10 + 42 || // 右座標の上限
                Y < 0 || // 上座標の上限
                Y > FMY + FY) // 下座標の上限
            {
                return false;
            }
            ++Cnt;
            return Life > 0;
        }
        public void Draw()
        {
            double disp_angle = !Kaiten ? 0 : PI2 * (Cnt % 120) / 120;
			~中略~
            DX.DrawRotaGraphF((float)X, (float)Y, ExRate, (Angle + AddRotate + Yaw + disp_angle) * PI2or1, BC.ImageBullet[GraphType][Color], 1);
            ~中略~
        }
    }
}

狙い撃ち弾

そして、今回は弾を発射する最初の1フレーム目にプレイヤーまでの角度をGetPlayerAngle1で求めて、0.0~1.0の値を angleの引数に入れてBullet.Add(new Bullet(引数));で発射しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static BulletCourse.CBulletCourse;
using DxLibDLL;

namespace BulletCourse
{
    public abstract class CEnemy : CMover
    {
        protected int Muki = 1;
        protected int ImageCnt = 0;
        public CEnemy(double x, double y) :base(x,y,1)
        {
            X = x;
            Y = y;
        }
        public virtual bool Move()
        {
            if (X < 10 + 20 || // 左座標の上限
                X > FMX - 10 + 42 || // 右座標の上限
                Y < 0 || // 上座標の上限
                Y > FMY - 5 + 20) // 下座標の上限
            {
                return false;
            }
            // 画像用カウンタ
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;
            ++Cnt;
            return Life > 0;
        }
        public void Draw()
        {
            // 敵を描画
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, BC.ImageEnemy[ImageCnt], 1);
        }
    }
    public class CEnemy0 : CEnemy
    {
        public CEnemy0(double x, double y)
            : base(x,y)
        {

        }
        public override bool Move()
        {
            if (Cnt < 60)
            {
                Y += 2.0;
            }
            else if (Cnt < 300)
            {
                // 30フレームに一度弾を発射する
                if(Cnt % 30 == 0)
                    BC.Bullet.Add(new CBullet(x: X, y: Y, speed: 3.0, 
                        angle: BC.GetPlayerAngle1(X,Y), graph_type: 1, color: 0));
            }
            if (Cnt > 300 + 240)
            {
                Y -= 2.0;
            }

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

弾を発射する間隔を調整する場合は56行目を以下の様に変更します。
Cnt % 30 == 0で30フレームに一度
Cnt % n == 0でnフレームに一度
弾の発射する間隔を長くしたかったらnを増やし、短くしたかったら減らすとできます。

次にCBulletに渡す引数の説明です。

  • x(X座標)
  • y(Y座標)
  • speed(速度)
  • angle(角度)
  • graph_type(弾の画像の種類)
  • color(弾の色の種類)

狙い撃ち弾の実行結果

ホーミング弾(追尾弾)

ここまで理解できればホーミング弾を作るのも簡単で、狙い撃ち弾で最初の1フレームに求めた角度を毎ループ求めるだけです。プログラムもCBulletクラスを継承してMove関数の中身をかいてやるだけです。

public class CSimpleHomingBullet : CBullet
{
    public CSimpleHomingBullet(double x, double y, double speed, int graph_type, int color, double pi2or1 = 1.0)
        : base(x: x, y: y, speed: speed, angle: BC.GetPlayerAnglePI2(x, y), graph_type: graph_type, color: color, pi2or1: pi2or1)
    { }
    public override bool Move()
    {
        // プレイヤーまでのなす角度を求める
        Angle = BC.GetPlayerAnglePI2(X, Y);
        // 基底クラス(CBulletクラス)のMove関数を呼ぶ
        return base.Move();
    }
}

今回はGetPlayerAnglePI2で0~PI2の間の角度を求めて使っています。
毎ループ計算するので、わかりやすさより、計算速度をとりました。

誘導弾 実行結果

狙う角度を甘くした誘導弾

この実行結果を見てやばい!って思いませんでしたか?笑
このままでは、確実に当たるまで追いかけてきてしまうので、時間や角度などを制限して、工夫する必要があります。時間を制限するのは、非常に簡単なので、角度を制限する方法を解説します。

public class CConstrainedHomingBullet : CBullet
{
    public CConstrainedHomingBullet(double x, double y, double speed, int graph_type, int color)
        : base(x: x, y: y, speed: speed, angle: BC.GetPlayerAnglePI2(x, y), graph_type: graph_type, color: color, pi2or1: 1.0)
    { }

    public override bool Move()
    {
        double vrad = 0.02f;
        // 旋回角速度を求める
        double diff = BC.GetPlayerAnglePI2(X, Y) - Angle;

        // -PIからPIまでの範囲に補正する
        while (diff < -PI) diff += PI2;
        while (diff >= PI) diff -= PI2;

        // 旋回角速度が最大旋回角度以下ならば
        // 旋回角速度で移動する
        if (Math.Abs(diff) < vrad)
        {
            Angle += diff;
        }
        // 旋回角速度が最大旋回角速度より大きいならば
        // 旋回角速度で旋回する
        else
        {
            // 右周りか左周りか決定
            Angle += (diff < 0 ? -vrad : vrad);
        }

        return base.Move();
    }
}

この角度制限誘導弾であるCConstrainedHomingBulletクラスは、目標の方向と弾の現在の進行方向位置の両方を考慮して、目標の方向に少しずつ回転させる処理を行います。

例えば、目標の方向が弾の進行方向に対して右側にある時は、弾を右回りに回転させます。弾の座標を(X,Y)進行方向をAngle、目標方向を(tx,ty)とすると、進行方向と敵の方向がなす角度は

diff=Math.Atan2(ty-y,tx-x)-Angle

となります。この角度diffが0から180度、ラジアンでは0からπの範囲にある時には、弾を右回りに回転させます。弾の回転スピードをvradとすると、弾の新しい進行方向は、

Angle+vrad

となります。角度diffが-180度から0度、ラジアンでは-πから0の範囲にある時には弾を左回りに回転させます。この時の新しい進行方向は

Angle-vrad

となります。角度diffの絶対値が弾の回転スピードvradの絶対値よりも小さい時は弾を目標の方向に直接向けます。この時の新しい進行方向は、

Angle+diff

となります。このように角度diffが小さい時には弾を目標に直接向けるようにすると、弾が左右に小刻みに振動してしまう問題を回避することができます。

一方、角度diffが大きい時には弾を目標に直接向けないので、プレイヤーとの位置関係によって最終的に弾が当たったり当たらなかったりします。

角度制限誘導弾 実行結果

これで避けられる程度の誘導角度になりました。次回以降は変速弾や分裂弾など、もっと特殊な弾を紹介する予定です。