GoogleAdsence

弾幕講座 レーザーの作り方

2020年2月10日

弾幕だけではなくかっこいいレーザーを作ってみたいと思いますよね?
そこで今回はレーザーの作り方を解説します。

レーザーに必要な情報

  • レーザーの軌道の計算
  • 当たり判定の計算
  • レーザーを描画

それではこれらを順番に考えていきましょう。

レーザーの当たり判定

レーザーの形は長い細い太い短いはありますが四角で自機は円で判定します。
ここで問題になるのが「長方形と円との接触」です。
結論からいうと以下の手順で行えば判定できます。

1 円の中に長方形の頂点が無いか
→あるなら接触している
→ないなら次の判定に移る

2 長方形の中に円が入り込んでいないか
→入りこんでいるなら接触している
→ないなら次の判定に移る

3 円の中心と辺との距離が半径以内か
→半径以内なら接触している
→半径以内でないなら完全に接触していない

円の中に長方形の頂点が無いか判定する

ピタゴラスの定理を使って点と点の距離を測ります。
公式は(目的のX座標-自分のX座標)* (目的のX座標-自分のX座標)* (目的のY座標-自分のY座標)* (目的のY座標-自分のY座標)=半径2乗です。

プログラムで書くと r = Math.Sqrt((x*x)+(y*y))です。

// 2次元ベクトル 
public struct Vector2_t
{
    public double x, y;
};
//点が円の中にあるかどうか。0:なし 1:あり
double QuestionPointAndCircle(Vector2_t p, Vector2_t rp, double r)
{
    double dx = p.x - rp.x, dy = p.y - rp.y;
    if (dx * dx + dy * dy < r * r) return 1;
    else return 0;
}
//円の中に長方形の4点のうちどれかがあるかどうか判定
for (i = 0; i < 4; i++)
{
    if (QuestionPointAndCircle(pt[i], rpt, r) == 1)
        return true;
}

2 長方形の中に円が入り込んでい無いか

この判定をするにはベクトルの内積、外積を使います。
内積の定義は

と習いましたよね。
これを元に以下の様に求められます。

ベクトルAとBとの内積及びの定義
A・B=|A|×|B|×cosθ
A×B=|A|×|B|×sinθ
A・B=Ax*Bx+Ay*By
A×B=Ax*By+Ay*Bx
これをプログラムでで表すと
θ= tan2(A×B, A*B)
ではさっそく実装してみましょう。

//3点から角度を返す
double GetSita(Vector2_t pt0, Vector2_t pt1, Vector2_t rpt)
{
    /* ベクトル C→P と C→Q のなす角θおよび回転方向を求める.*/
    Vector2_t c, p, q; /* 入力データ */
    Vector2_t cp;      /* ベクトル C→P */
    Vector2_t cq;      /* ベクトル C→Q */
    double s;          /* 外積:(C→P) × (C→Q) */
    double t;          /* 内積:(C→P) ・ (C→Q) */
    double theta;      /* θ (ラジアン) */

    /* c,p,q を所望の値に設定する.*/
    c.x = pt0.x; c.y = pt0.y;
    p.x = pt1.x; p.y = pt1.y;
    q.x = rpt.x; q.y = rpt.y;

    /* 回転方向および角度θを計算する.*/
    Vector2Diff(out cp, p, c);          /* cp ← p - c   */
    Vector2Diff(out cq, q, c);          /* cq ← q - c   */
    s = Vector2OuterProduct(cp, cq); /* s ← cp × cq */
    t = Vector2InnerProduct(cp, cq); /* t ← cp ・ cq */
    theta = Math.Atan2(s, t);
    return theta;
}

3 円の中心と辺との距離が半径以内か

今度は線分と点の距離を求めます。
長方形の頂点をα、βとした時、円の中心rが長方形の中にあるか外にあるかをまず判定します。

図のrの様に長方形の中にある場合は、ベクトルabとベクトルacがなす角θにおいてcosθとなるはずです。
長方形の中にあるのなら、rから直線αβに垂直に線を降ろした垂線が距離となります。

これは垂線のベクトルと線分αβとの内積が0になることを用いて以下のように実装出来ます。

//点と線分との距離を求める
double GetDistance(double x, double y, double x1, double y1,
                    double x2, double y2)
{
    double dx, dy, a, b, t, tx, ty;
    double distance;
    dx = (x2 - x1); dy = (y2 - y1);
    a = dx * dx + dy * dy;
    b = dx * (x1 - x) + dy * (y1 - y);
    t = -b / a;
    if (t < 0) t = 0;
    if (t > 1) t = 1;
    tx = x1 + dx * t;
    ty = y1 + dy * t;
    distance = Math.Sqrt((x - tx) * (x - tx) + (y - ty) * (y - ty));
    return distance;
}
//線分と点との距離を求める
for (i = 0; i < 4; i++)
{
    if (GetDistance(rpt.x, rpt.y, pt[i].x, pt[i].y, pt[(i + 1) % 4].x, pt[(i + 1) % 4].y) < r)
        return true;
}

さてこれで判定ができるようになったので、CLazer.csを全部見てみましょう。

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

namespace ShootingGame
{
    public class CLazer
    {
        public int Cnt, Knd, Color, State;//フラグ、カウンタ、種類、色
        public double Width, Angle, Length, Hantei;//幅、角度、長さ、判定範囲(表示幅に対して0~1で指定)、回転速度
        public Vector2_t StartPt;//レーザーを発射する始点、表示座標、当たり判定範囲
        public Vector2_t[] DispPt, OutPt;
        public TagLazerPhy LazerPhy;
        public int Life = 1;
        public void Move()
        {
            LazerCalc();
            if (OutLazer() == 1)
            {
                SGP.Player[0].Death();
            }
        }
        public void Draw()
        {
            DX.SetDrawMode(DX.DX_DRAWMODE_BILINEAR);//線形補完描画
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_ADD, 255);
            DX.DrawRotaGraphF(//発射位置のエフェクトを描画
                (float)StartPt.x, (float)StartPt.y, 1.0, 0,
                SGP.img_lazer_moto[Knd][Color], 1
            );
            DX.DrawModiGraphF(//レーザーを描画
                (float)DispPt[0].x, (float)DispPt[0].y,
                (float)DispPt[1].x, (float)DispPt[1].y,
                (float)DispPt[2].x, (float)DispPt[2].y,
                (float)DispPt[3].x, (float)DispPt[3].y,
                SGP.img_lazer[Knd][Color], 1
            );
            DX.SetDrawBlendMode(DX.DX_BLENDMODE_NOBLEND, 0);
            //myDrawSquare(//当たり判定範囲を表示
            //    OutPt[0].x, OutPt[0].y,
            //    OutPt[1].x, OutPt[1].y,
            //    OutPt[2].x, OutPt[2].y,
            //    OutPt[3].x, OutPt[3].y
            //);
        }
        void myDrawSquare(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3)
        {
            DX.DrawTriangle((int)x0, (int)y0, (int)x1, (int)y1, (int)x2, (int)y2, DX.GetColor(255, 0, 0), 1);
            DX.DrawTriangle((int)x0, (int)y0, (int)x3, (int)y3, (int)x2, (int)y2, DX.GetColor(255, 0, 0), 1);
        }
        public CLazer(int col, int knd, double angle,
            double startpt_x, double startpt_y, int width, int state, int length, int hantei,
            int conv_flag=0, double lphy_conv_base_x=0.0, double lphy_conv_base_y=0.0,
            double lphy_angle=0.0, int lphy_time=0)
        {
            Cnt = 0;
            Color = col;//弾の色
            Knd = knd;//弾の種類
            Angle = angle;//角度
            StartPt.x = startpt_x;//座標
            StartPt.y = startpt_y;
            Cnt = 0;
            Width = width;//幅
            State = state;//ステータス
            Length = length;//レーザーの長さ
            Hantei = hantei;
            LazerPhy.ConvFlag = conv_flag;//回転フラグ
            LazerPhy.ConvBaseX = lphy_conv_base_x;//回転基準位置
            LazerPhy.ConvBaseY = lphy_conv_base_y;
            LazerPhy.ConvX = startpt_x;//回転元の位置
            LazerPhy.ConvY = startpt_y;
            LazerPhy.Angle = lphy_angle;
            LazerPhy.BaseAng = angle;
            LazerPhy.Time = lphy_time;

            DispPt = new Vector2_t[4];
            for (int j = 0; j < 4; ++j) DispPt[j] = new Vector2_t();
            OutPt = new Vector2_t[4];
            for (int j = 0; j < 4; ++j) OutPt[j] = new Vector2_t();
        }
        //座標回転
        //(x0,y0)から(mx,my)を基準にang角回転した角度を(x,y)にいれる
        void ConvPos0(out double x, out double y, double x0, double y0, double mx, double my, double ang)
        {
            double ox = x0 - mx, oy = y0 - my;
            x = ox * Math.Cos(ang) + oy * Math.Sin(ang);
            y = -ox * Math.Sin(ang) + oy * Math.Cos(ang);
            x += mx;
            y += my;
        }

        void LazerCalc()
        {
            //表示位置を設定
            DispPt[0].x = StartPt.x + Math.Cos(Angle + PI / 2) * Width;
            DispPt[0].y = StartPt.y + Math.Sin(Angle + PI / 2) * Width;
            DispPt[1].x = StartPt.x + Math.Cos(Angle - PI / 2) * Width;
            DispPt[1].y = StartPt.y + Math.Sin(Angle - PI / 2) * Width;
            DispPt[2].x = StartPt.x + Math.Cos(Angle - PI / 2) * Width + Math.Cos(Angle) * Length;
            DispPt[2].y = StartPt.y + Math.Sin(Angle - PI / 2) * Width + Math.Sin(Angle) * Length;
            DispPt[3].x = StartPt.x + Math.Cos(Angle + PI / 2) * Width + Math.Cos(Angle) * Length;
            DispPt[3].y = StartPt.y + Math.Sin(Angle + PI / 2) * Width + Math.Sin(Angle) * Length;
            //あたり範囲を設定
            OutPt[0].x = StartPt.x + Math.Cos(Angle + PI / 2) * (Width * Hantei) + Math.Cos(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[0].y = StartPt.y + Math.Sin(Angle + PI / 2) * (Width * Hantei) + Math.Sin(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[1].x = StartPt.x + Math.Cos(Angle - PI / 2) * (Width * Hantei) + Math.Cos(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[1].y = StartPt.y + Math.Sin(Angle - PI / 2) * (Width * Hantei) + Math.Sin(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[2].x = StartPt.x + Math.Cos(Angle - PI / 2) * (Width * Hantei) + Math.Cos(Angle) * Length * Hantei + Math.Cos(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[2].y = StartPt.y + Math.Sin(Angle - PI / 2) * (Width * Hantei) + Math.Sin(Angle) * Length * Hantei + Math.Sin(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[3].x = StartPt.x + Math.Cos(Angle + PI / 2) * (Width * Hantei) + Math.Cos(Angle) * Length * Hantei + Math.Cos(Angle) * Length * ((1 - Hantei) / 2);
            OutPt[3].y = StartPt.y + Math.Sin(Angle + PI / 2) * (Width * Hantei) + Math.Sin(Angle) * Length * Hantei + Math.Sin(Angle) * Length * ((1 - Hantei) / 2);

            double ymax = LazerPhy.Angle, ty = LazerPhy.Time, t = Cnt;
            double delt = (2 * ymax * t / ty - ymax * t * t / (ty * ty));
            if (LazerPhy.Time != 0)//回転移動時間内なら
                Angle = LazerPhy.BaseAng + delt;//回転する
            if (LazerPhy.ConvFlag == 1)
            {//座標変換をするか
                ConvPos0(out StartPt.x, out StartPt.y,
                    LazerPhy.ConvX, LazerPhy.ConvY,
                    LazerPhy.ConvBaseX, LazerPhy.ConvBaseY,
                    -delt);
            }
            if (Cnt > LazerPhy.Time)
            {//回転時間を過ぎるとやめる
                LazerPhy.Time = 0;
                LazerPhy.ConvFlag = 0;
            }
            Cnt++;
        }
        //レーザーの物理的計算を行う為の構造体
        public struct TagLazerPhy
        {
            public int ConvFlag;
            public double Time, BaseAng, Angle;
            public double ConvX, ConvY, ConvBaseX, ConvBaseY;
        };

        /* 2次元ベクトル */
        public struct Vector2_t
        {
            public double x, y;
        };

        /* diff ← ベクトル p - q */
        void Vector2Diff(out Vector2_t diff, Vector2_t p, Vector2_t q)
        {
            diff.x = p.x - q.x;
            diff.y = p.y - q.y;
        }

        /* ベクトル p と q の内積 */
        double Vector2InnerProduct(Vector2_t p, Vector2_t q)
        {
            return p.x * q.x + p.y * q.y;
        }

        /* ベクトル p と q の外積 */
        double Vector2OuterProduct(Vector2_t p, Vector2_t q)
        {
            return p.x * q.y - p.y * q.x;
        }

        //点と線分との距離を求める
        double GetDistance(double x, double y, double x1, double y1,
                            double x2, double y2)
        {
            double dx, dy, a, b, t, tx, ty;
            double distance;
            dx = (x2 - x1); dy = (y2 - y1);
            a = dx * dx + dy * dy;
            b = dx * (x1 - x) + dy * (y1 - y);
            t = -b / a;
            if (t < 0) t = 0;
            if (t > 1) t = 1;
            tx = x1 + dx * t;
            ty = y1 + dy * t;
            distance = Math.Sqrt((x - tx) * (x - tx) + (y - ty) * (y - ty));
            return distance;
        }

        //点と点との距離を返す
        double GetPtAndPt(Vector2_t p1, Vector2_t p2)
        {
            return Math.Sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
        }

        //点が円の中にあるかどうか。0:なし 1:あり
        double QuestionPointAndCircle(Vector2_t p, Vector2_t rp, double r)
        {
            double dx = p.x - rp.x, dy = p.y - rp.y;
            if (dx * dx + dy * dy < r * r) return 1;
            else return 0;
        }

        //3点から角度を返す
        double GetSita(Vector2_t pt0, Vector2_t pt1, Vector2_t rpt)
        {
            /* ベクトル C→P と C→Q のなす角θおよび回転方向を求める.*/
            Vector2_t c, p, q; /* 入力データ */
            Vector2_t cp;      /* ベクトル C→P */
            Vector2_t cq;      /* ベクトル C→Q */
            double s;          /* 外積:(C→P) × (C→Q) */
            double t;          /* 内積:(C→P) ・ (C→Q) */
            double theta;      /* θ (ラジアン) */

            /* c,p,q を所望の値に設定する.*/
            c.x = pt0.x; c.y = pt0.y;
            p.x = pt1.x; p.y = pt1.y;
            q.x = rpt.x; q.y = rpt.y;

            /* 回転方向および角度θを計算する.*/
            Vector2Diff(out cp, p, c);          /* cp ← p - c   */
            Vector2Diff(out cq, q, c);          /* cq ← q - c   */
            s = Vector2OuterProduct(cp, cq); /* s ← cp × cq */
            t = Vector2InnerProduct(cp, cq); /* t ← cp ・ cq */
            theta = Math.Atan2(s, t);
            return theta;
        }

        //長方形と円との当たりを判定する
        bool HitjudgeSquareAndCircle(Vector2_t[] pt, Vector2_t rpt, double r)
        {
            int i;
            double[] a = new double[4];
            double[] b = new double[4];//a:傾き b:y切片
            double x = rpt.x, y = rpt.y;
            double theta, theta2;

            /*円の中に長方形の4点のうちどれかがあるかどうか判定*/
            for (i = 0; i < 4; i++)
            {
                if (QuestionPointAndCircle(pt[i], rpt, r) == 1)
                    return true;
            }
            /*ここまで*/

            /*長方形の中に物体が入り込んでいるかどうかを判定判定*/

            theta = GetSita(pt[0], pt[1], rpt);//3点の成す角1
            theta2 = GetSita(pt[2], pt[3], rpt);//3点の成す角2

            if (0 <= theta && theta <= PI / 2 && 0 <= theta2 && theta2 <= PI / 2)
                return true;

            /*ここまで*/

            //線分と点との距離を求める
            for (i = 0; i < 4; i++)
            {
                if (GetDistance(rpt.x, rpt.y, pt[i].x, pt[i].y, pt[(i + 1) % 4].x, pt[(i + 1) % 4].y) < r)
                    return true;
            }
            /*ここまで*/
            return false;//どこにもヒットしなかったらぶつかっていない
        }

        //public static int LAZER_MAX = 100;
        const double CRANGE = 2.0;
        int OutLazer()
        {
            int j;
            Vector2_t[] sqrp = new Vector2_t[4];//長方形の4点と円の中心
            Vector2_t rpt;

            if (SGP.Player.Count <= 0) return 0;

            rpt.x = SGP.Player[0].X;
            rpt.y = SGP.Player[0].Y;

            //レーザーが登録されていて、当たり判定をする設定なら
            if (Hantei != 0)
            {
                for (j = 0; j < 4; j++)
                {//レーザーの4点を設定
                    sqrp[j].x = OutPt[j].x;
                    sqrp[j].y = OutPt[j].y;
                }
                //長方形と円との接触判定
                if (HitjudgeSquareAndCircle(sqrp, rpt, CRANGE))
                    return 1;
            }
            
            return 0;
        }
    }
}
public class CLazerBullet1 : CBossBulletFactory
{
    public CLazerBullet1(MOVE_BOSS_POS move_moss_pos) : base(move_moss_pos)
    {
        SGP.SpelName = "レーザー";
    }
    int num = 0;
    public override bool Run(double x, double y)
    {
        const int TM008 = 420;
        const int DIST = 60;
        int i, j, t = Cnt % TM008, t2 = Cnt;
        
        if (t2 == 0) num = 4;
        if (t == 0)
        {
            for (j = 0; j < 2; j++)
            {
                for (i = 0; i < num; i++)
                {
                    double angle = PI2 / num * i + PI2 / (num * 2) * j + PI2 / (num * 4) * ((num + 1) % 2);
                    int plmn = (j != 0? -1 : 1);
                    SGP.Lazer.Add(new CLazer(
                        col:j, knd:0,
                        angle:angle,
                        startpt_x :x + Math.Cos(angle) * DIST,
                        startpt_y :y + Math.Sin(angle) * DIST,
                        width:2, state:j, length:240, hantei:0, conv_flag:1, 
                        lphy_conv_base_x:x, lphy_conv_base_y:y, lphy_angle:PI / num * plmn, lphy_time:80));
                }
            }
            SGP.PlaySound(11);
        }
        //レーザー計算
        for(i = 0; i < SGP.Lazer.Count; ++i)
        {
           int cnt = SGP.Lazer[i].Cnt;
           int state = SGP.Lazer[i].State;
           if (state == 0 || state == 1)
           {
               if (cnt == 80)
               {
                   SGP.Lazer[i].Width = 30;//幅を30に
                   SGP.Lazer[i].Hantei = 0.5;//表示幅の半分を判定範囲に
               }
               if (cnt >= 260 && cnt <= 320)
               {
                   if (cnt == 280)
                       SGP.Lazer[i].Hantei = 0;
                   SGP.Lazer[i].Width = 10 * (60 - (cnt - 260)) / 60.0;
                   if (cnt == 320)
                   {
                       //lazer[i].flag = 0;
                       SGP.Lazer[i].Life = 0;
                   }
                       
               }
           }              
        }
        if (t == TM008 - 1)
            num = (++num);
        ++Cnt;
        return true;
    }

実行結果