⚠️ Warning: This website is still being developed. Code examples, videos, and links may be broken while I continue to work on it. -- Scott, May 10, 2020
Resources for visualizing data using C# and the .NET platform

Boids in C

This project implements the Boids flocking algorithm in C# to create an interesting procedural animation of bird-drones (boids) scurrying about the screen.

Simulation Rules

The Boids algorithm was created by Craig Reynolds in 1986 and is a term used to describe "bird-oid objects". In this simulation complex emergent behavior comes from simple rules:

  • Rule 1: boids steer toward the center of mass of nearby boids
  • Rule 2: boids adjust direction to match nearby boids
  • Rule 3: boids steer away from very close boids

Additional Rules

You can get fancy and apply additional rules to create even more complex and interesting behavior. In my example program I added 3 additional rules:

  • Rule 4: boids speed up or slow down to match a target speed
  • Rule 5: boids are repelled by the edge of the box
  • Rule 6: boids steer away from boids marked as predators

Boids Model Code

Strategy

The velocity of voids is controlled by two variables, Xvel and Yvel. It's worth noting that trig functions can be used to convert these values to heading (in degrees) and speed (pixels per iteration), but this is typically not required.

All rules "steer" boids (adjusting their heading and speed) by acting on their X and Y velocities. Rules never move boids. After the application of all the rules, the position of each boid (X and Y) is moved by its velocity (Xvel and Yvel).

Each rule is given a distance that describes how far away it can act. Avoidance only acts on close boids, while flocking distances are much greater. Similarly, each rule is given a weight (termed power in this code) that describes how much it influences the final velocity. Typically rules with a larger distance have a smaller weight. Flocking weight is less than predator avoidance weight.

The Boid Class

Some helper functions have been omitted, but this is the gist of the Boid class. This class only stores position and velocity of one boid, and any information about the outside world must be passed-in.

public class Boid
{
    public double X;
    public double Y;
    public double Xvel;
    public double Yvel;

    public Boid(double x, double y, double xVel, double yVel)
    {
        (X, Y, Xvel, Yvel) = (x, y, xVel, yVel);
    }
}

The Field Class

The Field class contains a List of Boid objects and is responsible for applying the rules to each Boid. It is instantiated with a set of dimensions and a number of initial boids, and random boids (with random positions and velocities) are placed upon instantiation.

public readonly double Width;
public readonly double Height;
public readonly List<Boid> Boids = new List<Boid>();
private readonly Random Rand = new Random();

public Field(double width, double height, int boidCount = 100)
{
    (Width, Height) = (width, height);
    for (int i = 0; i < boidCount; i++)
        Boids.Add(new Boid(Rand, width, height));
}

Model Advancement

This method applies all the rules and advances the boids model in time. Distances and weights for each rule are defined in arguments.

public void Advance(bool bounceOffWalls = true, bool wrapAroundEdges = false)
{
    // update void speed and direction (velocity) based on rules
    foreach (var boid in Boids)
    {
        (double flockXvel, double flockYvel) = Flock(boid, 50, .0003);
        (double alignXvel, double alignYvel) = Align(boid, 50, .01);
        (double avoidXvel, double avoidYvel) = Avoid(boid, 20, .001);
        (double predXvel, double predYval) = Predator(boid, 150, .00005);
        boid.Xvel += flockXvel + avoidXvel + alignXvel + predXvel;
        boid.Yvel += flockYvel + avoidYvel + alignYvel + predYval;
    }

    // move all boids forward in time
    foreach (var boid in Boids)
    {
        boid.MoveForward();
        if (bounceOffWalls)
            BounceOffWalls(boid);
        if (wrapAroundEdges)
            WrapAround(boid);
    }
}

Rule 1: Steer Toward Center of Mass of Nearby Boids

Return the velocity adjustment needed to point toward the center of the flock (mean flock boid position). Notice that we define a flock (and neighbors) as boids within the given distance.

private (double xVel, double yVel) Flock(Boid boid, double distance, double power)
{
    var neighbors = Boids.Where(x => x.GetDistance(boid) < distance);
    double meanX = neighbors.Sum(x => x.X) / neighbors.Count();
    double meanY = neighbors.Sum(x => x.Y) / neighbors.Count();
    double deltaCenterX = meanX - boid.X;
    double deltaCenterY = meanY - boid.Y;
    return (deltaCenterX * power, deltaCenterY * power);
}

Rule 2: Mimic Direction and Speed of Nearby Boids

Return the velocity adjustment needed to approach the mean speed and direction of nearby boids.

private (double xVel, double yVel) Align(Boid boid, double distance, double power)
{
    var neighbors = Boids.Where(x => x.GetDistance(boid) < distance);
    double meanXvel = neighbors.Sum(x => x.Xvel) / neighbors.Count();
    double meanYvel = neighbors.Sum(x => x.Yvel) / neighbors.Count();
    double dXvel = meanXvel - boid.Xvel;
    double dYvel = meanYvel - boid.Yvel;
    return (dXvel * power, dYvel * power);
}

Rule 3: Steer Away from Extremely Close Boids

Return the velocity adjustment needed to avoid very close boids. This method doesn't use the center of the close flock, but instead summates the "closeness" of all close birds to generate the velocities.

private (double xVel, double yVel) Avoid(Boid boid, double distance, double power)
{
    var neighbors = Boids.Where(x => x.GetDistance(boid) < distance);
    (double sumClosenessX, double sumClosenessY) = (0, 0);
    foreach (var neighbor in neighbors)
    {
        double closeness = distance - boid.GetDistance(neighbor);
        sumClosenessX += (boid.X - neighbor.X) * closeness;
        sumClosenessY += (boid.Y - neighbor.Y) * closeness;
    }
    return (sumClosenessX * power, sumClosenessY * power);
}

Rule 4: Speed Limit

After the first three rules are applied, the new velocity is calculated for each boid. An operation can then be performed to scale these velocities (keeping their ratio the same) to adjust speed. I accomplish this inside the advancement method in the Boid class.

Notice the IsNan method has to be used to accommodate cases where speed is zero so as not to break the trig functions which calculate heading later.

public void MoveForward(double minSpeed = 1, double maxSpeed = 5)
{
    X += Xvel;
    Y += Yvel;

    var speed = GetSpeed();
    if (speed > maxSpeed)
    {
        Xvel = (Xvel / speed) * maxSpeed;
        Yvel = (Yvel / speed) * maxSpeed;
    }
    else if (speed < minSpeed)
    {
        Xvel = (Xvel / speed) * minSpeed;
        Yvel = (Yvel / speed) * minSpeed;
    }

    if (double.IsNaN(Xvel))
        Xvel = 0;
    if (double.IsNaN(Yvel))
        Yvel = 0;
}

Rule 5: Avoid Edges

This code accelerates boids away from walls with each iteration. Originally it just slows them down as they approach, but with more time they reverse course and travel away from the edge. This method is safe to use with fast boids that may travel off the screen for a brief period of time.

private void BounceOffWalls(Boid boid)
{
    double pad = 50;
    double turn = .5;
    if (boid.X < pad)
        boid.Xvel += turn;
    if (boid.X > Width - pad)
        boid.Xvel -= turn;
    if (boid.Y < pad)
        boid.Yvel += turn;
    if (boid.Y > Height - pad)
        boid.Yvel -= turn;
}

Alternate Rule 5: Wrap the Universe

This code isn't used in my example, but it could be used instead of the "avoid edges" method above. In this method boids that fall off the screen on one edge reappear on the opposite edge.

private void WrapAround(Boid boid)
{
    if (boid.X < 0)
        boid.X += Width;
    if (boid.X > Width)
        boid.X -= Width;
    if (boid.Y < 0)
        boid.Y += Height;
    if (boid.Y > Height)
        boid.Y -= Height;
}

Rule 6: Avoid Predators

Return the velocity adjustment needed to steer away from predators. In this example predators are simply defined as the first N boids using a class-level variable. Similar to the earlier boid avoidance method, this one summates avoidances based on each predator's position instead of responding to the mean position of all predators.

public int PredatorCount = 3;
private (double xVel, double yVel) Predator(Boid boid, double distance, double power)
{
    (double sumClosenessX, double sumClosenessY) = (0, 0);
    for (int i = 0; i < PredatorCount; i++)
    {
        Boid predator = Boids[i];
        double distanceAway = boid.GetDistance(predator);
        if (distanceAway < distance)
        {
            double closeness = distance - distanceAway;
            sumClosenessX += (boid.X - predator.X) * closeness;
            sumClosenessY += (boid.Y - predator.Y) * closeness;
        }
    }
    return (sumClosenessX * power, sumClosenessY * power);
}

Rendering the Boids Model

Graphics Transformation and Rotation

The outline of a boid is defined as a Point[] array. Instead of rotating the points to match the direction of each boid, the entire canvas is rotated around the boid, then the boid is drawn right-side-up. This method greatly simplifies the act of drawing rotated shapes with System.Drawing.

private void RenderBoid(Graphics gfx, Boid boid)
{
    // drawing of a boid centered at (0, 0)
    var boidOutline = new Point[]
    {
        new Point(0, 0),
        new Point(-5, -1),
        new Point(0, 10),
        new Point(5, -1),
        new Point(0, 0),
    };

    using (var brush = new SolidBrush(Color.LightGreen))
    {
        // translate and rotate the canvas around the boid
        gfx.TranslateTransform((float)boid.X, (float)boid.Y);
        gfx.RotateTransform((float)boid.GetAngle());

        // draw the boid at (0, 0)
        gfx.FillClosedCurve(brush, boidOutline);

        // reset before drawing the next object
        gfx.ResetTransform();
    }
}

Rendering is triggered using a timer set to 1 ms. The first 3 boids are predators so they are colored differently.

Field field = new Field(pictureBox1.Width, pictureBox1.Height, 100);
private void timer1_Tick(object sender, EventArgs e)
{
    field.Advance();
    pictureBox1.Image?.Dispose();
    pictureBox1.Image = RenderField(field);
}

public static Bitmap RenderField(Field field)
{
    Bitmap bmp = new Bitmap((int)field.Width, (int)field.Height);
    using (Graphics gfx = Graphics.FromImage(bmp))
    {
        gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        gfx.Clear(ColorTranslator.FromHtml("#003366"));
        for (int i = 0; i < field.Boids.Count(); i++)
        {
            if (i < 3)
                RenderBoid(gfx, field.Boids[i], Color.White);
            else
                RenderBoid(gfx, field.Boids[i], Color.LightGreen);
        }
    }
    return bmp;
}

Resources

Boids Simulators (JavaScript)

Literature

Source Code

System.Drawing Version

OpenGL Version