さて、前回は器をやったので、今回は真打のBoids_for_CS.cs)をやりましょう。

:Boidsウィンドウコントロールのはいったdllのソースコードです。

 

【Boids_for_CS.cs】

///////////////////////////////////////////////
//    Boids_for_CS.cs
//    Copyright (c) 2026 by Y-sama
//    C#プログラム及び日本語コメント文責:Y-sama
//    Original program Copyright by Ben Eater
//    https://eater.net/boids
///////////////////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;        //Graphicsクラス(アンチエイリアス)使用の為
using System.Collections.Generic;    //List使用の為

namespace Boids_for_CS
{
    //グローバル定数(↓こいつらについてはここを参照してください。)
    public enum BoidType    //Boidの種を行動特性別に以下4種とする
    {
        Flocker,            //0 - "Birds of a feather flock together"
        Introvert,            //1 - Harmless "Lone wolf" but tends to get away from a flock
        Sociopath,            //2 - Dangerous, often times kills others
        Dead                //3 - a dead body
        //https://gigazine.net/news/20240921-difference-between-psychopath-sociopath/
    }

    /////////////////////////////////////////////////////////
    //Boid(個体)クラス定義
    //注意:Boidタイプが「死骸(BoidType.Dead)」の場合、
    //(1)呼び出し側でメソッド呼び出しを制限することが原則
    //(2)気になる人は
    //        "if(bType == BoidType.Dead)    return;
    //        を行動メソッドの冒頭に追加してください
    /////////////////////////////////////////////////////////

    public class Boid                        //個体
    {
        //定数
        const int VisualRange = 75;    //視野
        //プロパティ
        public int ID {private set; get;}    //BoidのID(読み取り専用)ー解説:Ver2.0で追加しました。(末尾参照

        //フィールド定義
        public BoidType bType;                //生物学的種のID(ユーザー指定可)-解説:これが↑のやつです。
        public int X;                        //x座標
        public int Y;                        //y座標
        public double dx;                    //x移動距離(-5.0~5.0-左右へ5.0)
        public double dy;                    //y移動距離(-5.0~5.0-上下へ5.0)
        public Queue<Point> History;        //履歴(x、y座標の履歴配列)

        //コンストラクター
        public Boid(int id)    //解説:Ver2.0で引数を追加しました。
        {
            ID = id;                //IDを指定して生成する
            History = new Queue<Point>();
        }

        //擬鳥間の距離計算(√(x軸距離 ^ 2 + y軸距離 ^ 2))
        public int DistanceFrom(Boid other)
        {
            return (int)Math.Sqrt((other.X - this.X) * (other.X - this.X) +
                                (other.Y - this.Y) * (other.Y - this.Y));
        }

        //擬鳥の移動と飛行跡記録
        //特記:Sociopathの場合、他のBoidに追いつけば殺害する

        public void Move(List<Boid> boids)
        {
            //x軸方向へdx、y軸方向へdy移動する(MidpointRounding.AwayFromZero→例「-2.5→-3.0」)
            this.X += (int)Math.Round(this.dx, MidpointRounding.AwayFromZero);
            this.Y += (int)Math.Round(this.dy, MidpointRounding.AwayFromZero);
            //50個までの要素数でキューを管理
            History.Enqueue(new Point(this.X, this.Y));    //Queueに飛行跡データを追加する
            if(History.Count > 50)                        //Queueの数が50を超えたら
                History.Dequeue();                        //一番古いものを削除する
            //BoidType.Sociopathの殺害行為(解説:Ver2.0で追加しました。-ここでIDを使う予定が使いませんでした。)
            if(bType == BoidType.Sociopath)    //自分がSociopath
            {
                foreach(Boid otherBoid in boids)
                {
                    //自分以外他のBoidに追いついたならば
                    if(otherBoid != this && DistanceFrom(otherBoid) == 0)
                    {
                        //そのBoidを殺害する
                        otherBoid.bType = BoidType.Dead;
                    }
                }
            }
        }

        //擬鳥を描画領域内に止め、境界に接近すると反対方向へ向かわせる(dx、dyは移動方向・距離)
        //特記:全BoidType共通

        public void KeepWithinBounds(int width, int height)
        {
            const int margin = 200;        //余白(調整対象)
            if(X < margin)                //x軸左方向
                dx++;                    //右方向(加算)
            if(X > width - margin)        //x軸右方向
                dx--;                    //左方向(減算)
            if(Y < margin)                //y軸上方向
                dy++;                    //下方向(加算)
            if(Y > height - margin)        //y軸下方向
                dy--;                    //上方向(減算)
        }

        //擬鳥の集団への対応(向かう|離れる)速度を少し調節する
        //特記:FlockerとSociopathは擬鳥集団の中央へ向かうが、Introvertは離れる

        public void FlyTowardsCenter(List<Boid> boids)
        {
            const double centeringFactor = 0.005;        //速度変更係数
            int centerX = 0, centerY = 0, numNeighbors = 0;
            foreach(Boid otherBoid in boids)
            {
                //視野内の擬鳥のx、y座標とその数を加算
                if(otherBoid != this)
                {
                    if(DistanceFrom(otherBoid) < VisualRange)
                    {
                        centerX += otherBoid.X;
                        centerY += otherBoid.Y;
                        numNeighbors++;
                    }
                }
            }
            if(numNeighbors > 0)
            {
                //視野内の擬鳥のx、y座標の平均を求め、dx、dyを速度変更係数を乗じて調整する
                double sign = (bType != BoidType.Introvert) ? 1.0 : -1.0;    //死骸対応はしていない
                dx += (Math.Round((double)centerX / numNeighbors, MidpointRounding.AwayFromZero) - X) * centeringFactor * sign;
                dy += (Math.Round((double)centerY / numNeighbors, MidpointRounding.AwayFromZero) - Y) * centeringFactor * sign;
            }
        }

        //衝突しそうな近くの擬鳥から離れる
        //特記:FlockerとIntrovertは離れるが、Sociopathは逆に近づく

        public void AvoidOthers(List<Boid> boids)
        {
            const int minDistance = 20;                    //離反する境界距離
            const double avoidFactor = 0.05;            //速度変更係数
            double moveX = 0.0, moveY = 0.0;
            foreach(Boid otherBoid in boids)
            {
                if(otherBoid != this)
                {
                    //境界距離以内の擬鳥との離反(マイナス)座標を累計する
                    if(DistanceFrom(otherBoid) < minDistance)
                    {
                        //Sociopathでない自分が
                        if(bType != BoidType.Sociopath)
                        {    //Sociopathと遭遇した場合
                            if(otherBoid.bType == BoidType.Sociopath)
                            {    //必死に(通常の倍の加速で)逃げようとする(解説:パニック状態です。)
                                dx += (X - otherBoid.X) * avoidFactor * 2;
                                dy += (Y - otherBoid.Y) * avoidFactor * 2;
                                return;
                            }
                            //相手がSociopathでなければ避けようとする
                            else
                            {
                                moveX += X - otherBoid.X;    //-(otherBoid.X - X)
                                moveY += Y - otherBoid.Y;    //-(otherBoid.Y - Y)
                            }
                        }
                        //自分がSociopathなら
                        else
                        {    //相手に近づこうとする
                            moveX += otherBoid.X - X;
                            moveY += otherBoid.Y - Y;
                        }
                    }
                }
            }
            //自分の方向、速度(dx、dy)に離反・接近速度(速度変更係数を乗じたもの)を反映
            dx += moveX * avoidFactor;
            dy += moveY * avoidFactor;
        }

        //他の擬鳥の速さ(速度と方向)を求め、それへ自分を調整する
        //特記:FlockerとIntrovertは離れるが、Sociopathは逆に近づく

        public void MatchVelocity(List<Boid> boids)
        {
            //Flockerは群れに歩調を合わせるが、IntrovertとSociopathは無視する
            if(bType != BoidType.Flocker)
                return;
            const double matchingFactor = 0.05;            //速度変更係数
            double avgDX = 0.0, avgDY = 0.0;
            int numNeighbors = 0;
            foreach(Boid otherBoid in boids)
            {
                if(otherBoid != this)
                {
                    //視野内の擬鳥のx、y移動距離を加算し、その数を記録
                    if(DistanceFrom(otherBoid) < VisualRange)
                    {
                        avgDX += otherBoid.dx;
                        avgDY += otherBoid.dy;
                        numNeighbors++;
                    }
                }
            }
            //移動距離の合計を総数で除算して平均を求め、自分の移動距離との差に速度変更係数を乗じたもので調整する
            if(numNeighbors > 0)
            {
                avgDX = avgDX / numNeighbors;
                avgDY = avgDY / numNeighbors;
                dx += (avgDX - dx) * matchingFactor;
                dy += (avgDY - dy) * matchingFactor;
            }
        }

        //速度(dx、dyのベクトル値)は変化するが15を超えない
        public void LimitSpeed()
        {
            const double speedLimit = 15.0;            //限界速度
            //x、y軸の移動距離から求められる速度(speed)が
            double speed = Math.Sqrt(dx * dx + dy * dy);
            //限界速度を超えたならば、x、y軸の移動距離を限界速度で頭打ちにする
            if(speed > speedLimit)
            {
                dx = speedLimit / speed * dx;
                dy = speedLimit / speed * dy;
            }
        }

        //擬鳥の描画
        public void Draw(Graphics gHandle, bool draw_trail = false)
        {
            if(bType == BoidType.Dead)    return;            //死骸は(明示的に)描画しない
            //Boidの描画色
            Color[] BoidCol = new Color [3] {Color.Blue, Color.Green, Color.Red};
            //飛行跡のPen色
            Pen[] BoidTrail = new Pen[3] {Pens.SkyBlue, Pens.LightGreen, Pens.Tomato};
            //半直線(0, 0)-(x, y)と正のx軸の平面上の角度(ラジアン単位)
            //float angle = (float)Math.Atan2(dy, dx);

            float angle = (float)(Math.Atan2(dy, dx) * 180.0 / Math.PI);
            //三角形の3つの頂点(原点(0,0)周辺に定義)
            PointF[] points = {
                new PointF(8, 0),    // 上の頂点
                new PointF(-8, -4),    // 左下の頂点
                new PointF(-8, +4)    // 右下の頂点
            };
            //原点の移動と回転
            gHandle.TranslateTransform(X, Y);            //描画位置(X, Y)へ移動
            gHandle.RotateTransform(angle);
            //塗りつぶし実行
            using(Brush brush = new SolidBrush(BoidCol[(int)bType]))
            {
                gHandle.FillPolygon(brush, points);
            }
            //Graphicsの状態をリセット
            gHandle.ResetTransform();
            //飛行跡が真であれば一定の長さの飛行痕(糸のような線)を残す
            if(draw_trail)
            {
                //飛行跡(Point)配列をDrawCurve(DrawLineよりもよさげ)で描く
                Point[] trail = this.History.ToArray();
                if(trail.Length > 1)
                {
                    //gHandle.DrawLines(Pens.SkyBlue, trail);
                    gHandle.DrawCurve(BoidTrail[(int)bType], trail);
                }
            }
        }
    }

    /////////////////////////////////////////////////
    //Boids(個体群)クラス定義(Boidsコントロール)
    /////////////////////////////////////////////////

    public class Boids : Control            //種の群れ
    {
        //フィールド定義
        private static Random rand;            //乱数
        List <Boid> boids;                    //擬鳥配列
        //描画関係プロパティ
        private Bitmap Canvas {set; get;}    //仮想画面ビットマップ
        private Graphics gHandle {set; get;}//仮想画面のグラフィック
        private Timer DrawTimer;            //描画用タイマー
        private bool Draw_Trail = false;    //飛行跡許可フラグ

        public Boids()
        {
            //乱数の初期化
            rand = new Random((int)DateTime.Now.Ticks);
            //擬鳥配列の生成
            boids = new List<Boid>();
            //タイマー初期設定
            DrawTimer = new Timer();
            DrawTimer.Interval = 15;        //設定値15/1000秒(最小値は事実上15程度)
            DrawTimer.Tick += DrawBoids;
            //コントロール生成時処理
            //ちらつき防止(解説:これは全く知らず、ちらつきが出ていましたが、Chat-GPTに教えてもらいました。)

            SetStyle(
                ControlStyles.UserPaint |
                ControlStyles.AllPaintingInWmPaint |
                ControlStyles.OptimizedDoubleBuffer,
                true
            );
            UpdateStyles();
        }

        //終了時リソース開放処理)(解説:ここもFormとはちょっと異なります。コントロールの場合の定番メソッドとしてChat-GPTに指導してもらいました。)
        protected override void Dispose(bool disposing)
        {
            if(disposing)
            {
                if(DrawTimer != null)
                    DrawTimer.Dispose();    //タイマーの廃棄
                if(gHandle != null)
                    gHandle.Dispose();        //描画ハンドルの廃棄
                if(Canvas != null)
                    Canvas.Dispose();        //仮想画面の廃棄
            }
            base.Dispose(disposing);
        }

        //Resizeイベント処理
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            //描画Canvas(再)設定
            InitCanvas();
        }

        //Paintイベント処理
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            //Canvas描画処理
            if(Canvas != null)    //仮想画面がnullの場合はスキップ
                e.Graphics.DrawImage(Canvas, 0, 0);
        }

        //描画Canvas初期化・再設定
        private void InitCanvas()
        {
            //コントロール生成中のエラー対策
            if(this.ClientSize.Width < 1 || this.ClientSize.Height < 1)
                return;
            //既存の仮想画面、グラフィックハンドルがあれば廃棄する

            //解説:ここも私は誤解しておりましたが、Dispose()はメモリーを開放しますが、"= null"はメモリーアドレス参照を明示的に解消します。
            if(gHandle != null)
            {
                gHandle.Dispose();    //解説:メモリーを開放
                gHandle = null;    //解説:メモリーアドレス参照を解消
            }
            if(Canvas != null)
            {
                Canvas.Dispose();    //解説:Ditto
                Canvas = null;    //解説:Ditto
            }
            //ビットマップ仮想画面の作成
            Canvas = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
            //Graphicsクラス(アンチエイリアス)
            gHandle = Graphics.FromImage(Canvas);
            gHandle.SmoothingMode = SmoothingMode.AntiAlias;
            gHandle.PixelOffsetMode = PixelOffsetMode.HighQuality;
        }

        //Boidsの描画(Timer割り込み処理)
        private void DrawBoids(object sender, EventArgs e)
        {
            //描画面を消去
            gHandle.Clear(this.BackColor);
            foreach(Boid boid in boids)
            {
                if(boid.bType == BoidType.Dead)
                    continue;                    //死骸は何もしない
                //速さをそれぞれの規則に基づいて更新
                boid.FlyTowardsCenter(boids);    //群れへの参集
                boid.AvoidOthers(boids);        //近すぎる擬鳥からの離反
                boid.MatchVelocity(boids);        //速度調整
                boid.LimitSpeed();                //限界速度制限
                //Boidsコントロールの枠内にとどめる
                boid.KeepWithinBounds(this.ClientSize.Width, this.ClientSize.Height);
                //現在の速さに基づき位置(x、y座標)を更新し、飛行跡を追加
                boid.Move(boids);
                //Boidsの描画
                boid.Draw(gHandle, Draw_Trail);    //Draw_Trail-飛行跡の描画指定
            }
            //Canvasを表示
            this.Invalidate();
        }

        //解説:以下から公開(public)めそっどになります。
        //擬鳥の生成(擬鳥配列の初期化)
        public string CreateBoids(int count, bool lonewolf = false, bool sociopath = false)
        {
            //コントロール生成中のエラー対策(エラーは空文字を返す)
            if(this.ClientSize.Width < 1 || this.ClientSize.Height < 1)
                return String.Empty;
            //一旦boids(List)を空にする
            boids.Clear();
            //戻り値文字列
            string ResMsg = "Boidsの総数:" + count.ToString();
            //擬鳥配列の初期化
            for(int i = 0; i < count; i++)
            {
                //擬鳥初期化
                Boid bd = new Boid(i);            //iがIDとなる
                bd.bType = BoidType.Flocker;    //一種だけなので暫定的にFlockerとする
                bd.X = rand.Next(this.ClientSize.Width);    //0~Width - 1
                bd.Y = rand.Next(this.ClientSize.Height);    //0~Height - 1 ;
                bd.dx = rand.Next(-5, 6);        //-5.0~5.0-左右へ5.0
                bd.dy = rand.Next(-5, 6);        //-5.0~5.0-上下へ5.0
                bd.History.Enqueue(new Point(bd.X, bd.Y));    //最初の位置を記録(解説:オリジナルにはなかったけど...)
                boids.Add(bd);
            }
            //Introvert(総数の5%程度)を付加する
            int numInt = 0;
            if(lonewolf)
            {
                int num = rand.Next(1, (int)(count * 0.05) + 1);    //1~Count x 5%までの乱数値
                numInt = num;
                while(num > 0)    //総数に対して選択数が小さいのでシャッフル方式は採らない
                {
                    int n = rand.Next(count);
                    if(boids[n].bType == BoidType.Flocker)
                    {
                        boids[n].bType = BoidType.Introvert;
                        num--;
                    }
                }
            }
            //Sociopath(総数の3%程度)を付加する
            int numSocio = 0;
            if(sociopath)        //総数に対して選択数が小さいのでシャッフル方式は採らない
            {
                int num = rand.Next(1, (int)(count * 0.03) + 1);    //1~Count x 3%までの乱数値
                numSocio = num;
                while(num > 0)    //総数に対して選択数が小さいのでシャッフル方式は採らない
                {
                    int n = rand.Next(count);
                    if(boids[n].bType == BoidType.Flocker)
                    {
                        boids[n].bType = BoidType.Sociopath;
                        num--;
                    }
                }
            }
            ResMsg += "(Flocker:" + (count - numInt - numSocio).ToString() + "、Introvert:" + numInt.ToString() + "、Sociopath:" + numSocio.ToString() + ")";
            return ResMsg;
        }

        //Boids開始
        public bool Start()
        {
            //擬鳥未生成の場合のエラートラップ
            if(boids.Count == 0)
            {
                MessageBox.Show("まだ擬鳥が生成されていません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            //描画不可状態でのエラートラップ
            if(Canvas == null || gHandle == null)
            {
                MessageBox.Show("Graphics not ready.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            DrawTimer.Start();                //タイマー割り込み開始
            return true;
        }

        //Boids停止
        public void Stop()
        {
            DrawTimer.Stop();                //タイマー割り込み停止
        }

        //飛行跡描画可否
        public void DrawTrail(bool flag)
        {
            Draw_Trail = flag;                //飛行跡描画指定
        }

        //Boids結果表示(解説:Ver2.0で追加しました。停止した時のBoidsの状況を文字列で返そうと思いました。)
        public void Result()
        {
            string ResMsg = "Boidsの総数:" + boids.Count.ToString();
            int numFlock = 0,  numInt = 0,  numSocio = 0;
            foreach(Boid boid in boids)
            {
                switch((int)boid.bType)
                {
                    case 0:        //Flocker
                        numFlock++;
                        break;
                    case 1:        //Introvert
                        numInt++;
                        break;
                    case 2:        //Sociopath
                        numSocio++;
                        break;
                    default:    //Dead
                        break;
                }
            }
            ResMsg += "(Flocker:" + numFlock.ToString() + "、Introvert:" + numInt.ToString() + "、Sociopath:" + numSocio.ToString() + "、Dead:" + (boids.Count - numFlock - numInt - numSocio).ToString() + ")";
            MessageBox.Show(ResMsg, "Boidsの結果", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }
}

 

【末尾参照】Boid(個体)クラスにIDを入れてますが、これはSociopathが他のBoidに追いついた際にこれを殺害する処理を「Listのディープコピーを自分との距離を基にソートし、自分以外のものがいれば」で考えていたために、オリジナルList配列の何番目になるのかを知るために追加したものでした。でも結局↑のように、「自分がSociopathで、自分自身ではなく、且つ距離が0」という条件でクリアしたので、使いませんでした(これを書いていて気が付きました-笑;)後日の拡張の為に残しておきましょう。

 

如何だったでしょうか?

 

これにてBoids、一件落着ぅ~!

 

として終わりにしようと思いましたが、

 

本日、ひらめき

があり、

 

後もう一回だけ、↑の変更点を解説しますね。