飛び交うレーザー(前編)

こんにちは、きうちです。

むしむしする季節がやってきつつありますね。いかがお過ごしでしょうか。

換気など湿気対策をしつつ、水分はこまめにとっていきたいところですね!

★ ★ ★

今回は「飛び交うレーザー」というお題でお送りしようかと思います。

これは何かというと、線が斜めに飛んで行って画面の端で反射していく、というものです。

昔、とあるシューティングゲームで見かけた「反射レーザー」というものをマネしてみようと思って作りました。

言語は例によってC#にします。Windowsフォームアプリとして作成します。

フォームのサイズはひとまず 800×600 にします。
今回はフォームに直接描画しようと思いますので、PictureBoxは無し。

背景は黒くします。

また、レーザーの情報を持たせるためのクラスを別ファイルで作ります。名前はLaserにします。

    public class Laser
    {
        public Point[] P { get; set; }

        public Point[] V { get; set; }

        public Pen C { get; set; }

        public Laser(Point[] p, Point[] v, Pen c)
        {
            P = p;
            V = v;
            C = c;
        }
    }

3つあるプロパティの、Pが座標情報、Vが移動量(速度ベクトル)です。Cが描画色です。

レーザーを下図のように「点と点を結ぶいくつかの線の集まり」で表現したいので、PとVが配列になっています。

この図、見やすくするために点を7つだけ描いていますが、あとで登場するコードではもうすこし多くしています。多いほど長いレーザーになります。

コンストラクタで値を受け取りプロパティに格納するようにします。

フォーム側のコンストラクタでは、以下のようにレーザーの情報を作ります。

できたレーザーはフィールドの l (小文字のエル) に格納することにします。

    public partial class Form1 : Form
    {
        /// <summary>レーザーのデータ</summary>
        private Laser l;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Form1()
        {
            InitializeComponent();

            l = MakeLaser(Pens.DeepSkyBlue);
        }

        /// <summary>
        /// レーザーのデータを作る
        /// </summary>
        /// <param name="pen">レーザーの色</param>
        /// <returns>レーザーのデータ</returns>
        private Laser MakeLaser(Pen pen)
        {
            var x = 320;
            var y = 320;

            var pp = new List<Point>();
            var vv = new List<Point>();

            for (int i = 0; i < 16; i++)
            {
                pp.Add(new Point(x, y));
                x -= 16;
                y -= 16;
                vv.Add(new Point(16, 16));
            }

            return new Laser(pp.ToArray(), vv.ToArray(), pen);
        }
    }

実際に作る処理は MakeLaser というメソッドを定義してそこに書くことにしました。
このメソッドは戻り値としてLaserのインスタンスを返します。
320, 320 の位置を起点にして(起点は適当に決めました)、斜め左上方向に座標を決めていきます。
点の数は16とします。
点と点の間隔はX方向16ピクセル、Y方向16ピクセルとします(このぐらいが経験的にちょうどよかったので)。
色はとりあえず明るい青(DeepSkyBlue)。

続いて、作ったレーザーの情報を基に画面に描画させる処理を作ります。
フォームの Paint イベントメソッドで実装します。

        /// <summary>
        /// フォーム描画イベント
        /// </summary>
        /// <param name="sender">イベント発生オブジェクト</param>
        /// <param name="e">イベント引数</param>
        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            for (int i = 1; i < l.P.Length; i++)
            {
                e.Graphics.DrawLine(l.C, l.P[i - 1], l.P[i]);
            }
        }

やっていることは Laser のインスタンスが持っている点情報と色情報を使って線を描くということ。

ここまでで実行してみます。

青い線が描かれましたね!

★ ★ ★

続いて、この線(レーザー)を動かしてみます。
タイマーを1個追加して、そのイベントの中で座標を更新する処理を行います。
インターバルは10ミリ秒程度にします(これもいろいろ試してみてこれくらいがちょうどよかったので)。

        /// <summary>
        /// タイマーTICK時処理
        /// </summary>
        /// <param name="sender">イベント発生オブジェクト</param>
        /// <param name="e">イベント引数</param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            for (int i = 0; i < l.P.Length; i++)
            {
                l.P[i] = MoveObject(l.P[i], ref l.V[i]);
            }
            Refresh();
        }

        /// <summary>
        /// 移動させる
        /// </summary>
        /// <param name="p">現在座標</param>
        /// <param name="v">座標増分</param>
        /// <returns>移動後の座標</returns>
        private Point MoveObject(Point p, ref Point v)
        {
            int x = p.X + v.X;
            int y = p.Y + v.Y;
            if (x < 0 || x > this.ClientSize.Width)
            {
                v.X = -v.X;
                x = p.X + v.X;
            }
            if (y < 0 || y > this.ClientSize.Height)
            {
                v.Y = -v.Y;
                y = p.Y + v.Y;
            }
            return new Point(x, y);
        }

タイマーTICKイベントの中ではレーザーを構成する各座標がそれぞれの速度値を用いて座標を更新します。
具体的な座標を更新する処理は MoveObject というメソッドを用意してその中でやることにしました。

X座標、Y座標各々について速度を足します。
また、もし画面からはみでるようであれば速度を反転(-1倍)させて座標を計算しなおします。
例えば、画面下端をはみ出そうになったらその点のY方向の移動速度をマイナス倍します。プラスだったものがマイナスになり、斜め右下方向に進んでいたものが斜め右上方向に進むようになります。下図のようなイメージ。

そうしてできた新しい座標をLaserのオブジェクトの中に格納します。

コンストラクタの中ではタイマーをスタートさせるよう処理を追加します。

        public Form1()
        {
            InitializeComponent();

            l = MakeLaser(Pens.DeepSkyBlue);

            timer1.Start(); // タイマーをスタートさせる
        }

ここでまた実行してみます。

レーザーが飛びましたね!

★ ★ ★

1本だけじゃさみしいので、増やします。色もいろいろな色にします。
開始位置もランダムにしましょう。

        /// <summary>レーザーの色</summary>
        private Pen[] COLORS = { Pens.Blue, Pens.Red, Pens.Magenta, Pens.Green, Pens.Cyan, Pens.Yellow, Pens.White };

        /// <summary>レーザーのデータ</summary>
        private Laser[] ll; // 変更: 配列にする

        /// <summary>乱数生成オブジェクト</summary>
        private Random random = new Random();

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Form1()
        {
            InitializeComponent();

            var list = new List<Laser>(); // 変更: for文で複数作る
            for (int i = 0; i < COLORS.Length; i++)
            {
                Laser l = MakeLaser(COLORS[i]);
                list.Add(l);
            }
            ll = list.ToArray();

            timer1.Start(); // タイマーをスタートさせる
        }

        /// <summary>
        /// レーザーのデータを作る
        /// </summary>
        /// <param name="pen">レーザーの色</param>
        /// <returns>レーザーのデータ</returns>
        private Laser MakeLaser(Pen pen)
        {
            var x = random.Next(this.ClientSize.Width / 2, this.ClientSize.Width); // 変更: ランダムに
            var y = random.Next(this.ClientSize.Height / 2, this.ClientSize.Height);

            var pp = new List<Point>();
            var vv = new List<Point>();

            for (int i = 0; i < 16; i++)
            {
                pp.Add(new Point(x, y));
                x -= 16;
                y -= 16;
                vv.Add(new Point(16, 16));
            }

            return new Laser(pp.ToArray(), vv.ToArray(), pen);
        }

準備の方はこんな感じ。

描画処理やタイマー処理は以下。

        /// <summary>
        /// フォーム描画イベント
        /// </summary>
        /// <param name="sender">イベント発生オブジェクト</param>
        /// <param name="e">イベント引数</param>
        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            foreach (Laser l in ll)
            {
                for (int i = 1; i < l.P.Length; i++)
                {
                    e.Graphics.DrawLine(l.C, l.P[i - 1], l.P[i]);
                }
            }
        }

        /// <summary>
        /// タイマーTICK時処理
        /// </summary>
        /// <param name="sender">イベント発生オブジェクト</param>
        /// <param name="e">イベント引数</param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            foreach (Laser l in ll)
            {
                for (int i = 0; i < l.P.Length; i++)
                {
                    l.P[i] = MoveObject(l.P[i], ref l.V[i]);
                }
            }
            Refresh();
        }

実行してみます。

だいぶにぎやかになりましたね!

★ ★ ★

というわけで、今日はこのへんで。

次回は、スクリーンセーバーっぽくする、というのをやってみたいと思います!

それでは!