さて、前回は器をやったので、今回は真打の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、一件落着ぅ~!
として終わりにしようと思いましたが、
本日、ひらめき
があり、
後もう一回だけ、↑の変更点を解説しますね。

