CPSC 120 Principles of Computer Science Fall 2025

Topics: Boids I

Due: Mon 11/24 11:59pm


Introduction

Topics showcase applications of the core concepts, in this case arrays and making choices.

Behavioral animation is an animation and simulation technique where simple behaviors are combined to yield complex individual and group behavior.

These exercises are a chance to explore some of those combinations and to see how one might write a program to simulate complex group behavior; the focus is on combining behaviors to get interesting results rather than implementing the steering behaviors themselves. The exercises utilize the three patterns for boid behavior: a single behavior, multiple behaviors active at once, and choosing between behaviors or sets of behaviors. Boids II brings all three patterns together in a simulation of a small ecosystem (shown).


Handin

Hand in a hardcopy (paper) of your worksheet in class or under my office door (Lansing 302).

To hand in your sketches:


Policies

Like labs, topics are individual assignments — what you hand in must be your own work, your own ideas, your own effort. You may get help in office hours and from Teaching Fellows, but you may not work together with a partner or in a group with others to create solutions or write code.

The policies on late work and extensions, academic integrity, and the use of AI for topics are the same as for lab 2. Review the policies there. One extension token is needed for revise-and-resubmit without an initial handin.

Also review assignments and evaluation on the Policies page for how topics factor into the final grade. The short version: topics are optional for a passing grade (C-), but achieving proficiency for at least some topics is required for a higher grade.


Preliminaries

Getting Started

This assignment is different from most of the other labs in this course in that you will be working with a substantial amount of code written by someone else. As with arrays, the task in each exercise is to modify or add a small piece of an otherwise complete sketch rather than writing a sketch from scratch. Additional provided code — a library of steering behaviors for boids — will be used as a "black box" — you won't need to look at or understand the internals of how it does what it does, just how to use it appropriately.

Provided Code

All three exercises use the same starter code. This is a template for a sketch containing a single boid. It contains variable declarations for the boid's properties (position, velocity, max acceleration force, max speed, and neighborhood), initializes those properties, and does all of draw()'s job except for steps 2a and 2b. (Steps 2a and 2b must be customized for the particular behavior(s) that you want the boid to have.) You can run the starter code but it won't do anything interesting because there aren't any steering forces being applied to the boid.

Also provided is a boids library which contains definitions for functions that compute steering vectors for a number of the behaviors discussed in class, along with a function for drawing boids. The functions you'll need for this lab are described in the relevant exercises below.

Sketches With More Than One File

While you could paste a sketch template and the boids library into the same file, it is common practice for libraries to be separate from the specific code for a particular sketch. This helps keep individual files smaller and more organized (and can cut down on the need for copy-and-paste).

To add the boids library to a sketch, create a new tab (not a new sketch) — click on the little down arrow next to the tab that shows the name of your sketch (just above the main editor area), choose "New Tab", and enter "boids_lib" as the name for the new file. You should see a second tab named "boids_lib" appear next to the first. Switch to that tab, paste in the provided boids library code, and save your sketch.

Boids in Processing

Also reference the slides from 11/12.

Implementing Boids

Boids have five properties: position, velocity, maximum acceleration, maximum speed, and a neighborhood (defined by a radius and an angle). For technical reasons, position and velocity will each be represented by a single variable of type PVector rather than separate x and y or xspeed and yspeed variables. (PVector bundles together an x component and a y component.) Variables of type PVector are declared in the same way as any other variable, with a type and a name:

  PVector pos;   // boid's position

Initialize a PVector variable by creating a new PVector with the desired values e.g.

  pos = new PVector(100,200);

As with the initialization of other animation variables, this typically goes in setup().

The x and y parts of a PVector can be accessed by adding .x or .y after the name of the PVector variable (e.g. pos.x and pos.y).

The pattern for draw() for boids sketches is shown below. This follows the general pattern for animated sketches in Processing — where draw() does two things, draw the scene and update the animation variables — but with the "update" step expanded into several substeps (compute the net steering force, use that steering force to update the boid's velocity, and use the boid's velocity to update the position).

void draw () {
  // 1 - draw scene

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  //  b - combine forces
  //  c - limit the size of the force that can be applied

  // 3 - update boid's velocity
  //  a - add net steering force
  //  b - limit boid's max speed

  // 4 - update boid's position
  //  a - update position by adding velocity
  //  b - wrap at edges of window
}

You can observe this template combined with code in the slides from class and in the wander, wander+seek, and wander or seek examples.

Steering Behaviors

Unlike drawing functions, all of these functions return a value instead of drawing something on the screen. You can see this because all of the function headers start with PVector instead of void. Call these functions just like drawing functions — pass the desired values for the parameters — but instead of the function call being a statement by itself, it should occur on the right-hand side of an assignment statement. For example:

  PVector forward = computeForward(pos,vel,maxspeed);

You can also observe how these functions are used in the slides from class and in the wander, wander+seek, and wander or seek examples.

The specific behavior functions provided as part of the boids library are introduced as needed in the exercises below.

Combining Behaviors

The net steering force is computed in step 2 of the template. There are several patterns for this step depending on how many behaviors are active at once and whether different behaviors or sets of behaviors are active at different times. The specific patterns are introduced in the slides from class and in the exercises below.

Additional Reference

Also review the slides from 11/12 (behavioral animation and boids) for specifics related to this topic and the relevant slides, in-class exercises handouts, and in-class exercise solutions for material related to the core concepts (arrays, making choices).


Exercises

Exercise 1

Your task is to create a sketch in which a single boid arrives at the mouse position, as shown in the example. To do this:

  • Create a new sketch named boids1 and paste in the starter code.

  • Add the boids library to the sketch as directed in Sketches With More Than One File above.

  • Run the sketch — you'll see a single red boid which starts with a random position and velocity and moves in a straight line across the screen. That's because there aren't yet any behaviors to affect its velocity.

  • Fill in steps 2a and 2b in draw() to compute and combine the steering forces for a single behavior (arrive). See the "Arrive" and "One Behavior" sections below for how to use the arrive behavior and how to fill in steps 2a and 2b in the case of a single behavior. You'll only need two lines of code to complete this sketch! Use a threshold of 100 for the arrive behavior.

Arrive

The arrive behavior is implemented by the computeArrive function (part of the provided boids library).

// compute the arrive steering vector
//  pos, vel - position and velocity of boid
//  maxspeed - boid's max speed
//  target - position of the target
//  threshold - distance from target at which the boid starts slowing
PVector computeArrive ( PVector pos, PVector vel, float maxspeed, PVector target, float threshold ) { ... }

Unlike drawing functions, all of the behavior functions return a value instead of drawing something on the screen. You can see this because the function headers start with PVector instead of void. Call these functions just like drawing functions — pass the desired values for the parameters — but instead of the function call being a statement by itself, it should occur on the right-hand side of an assignment statement. For example:

  PVector wander = computeWander(pos,vel,maxspeed);

For computeArrive, you can simply pass the animation variables for the boid's position, velocity, and maxspeed for those parameters but you'll likely need to create a PVector for the arrival target. You can do this by writing new PVector(x,y) where you want the PVector to be used. For example, to compute the steering vector for arriving at the position (150,250) with a threshold of 200 pixels:

  PVector arrive = computeArrive(pos,vel,maxspeed,new PVector(150,250),200);

One Behavior

The net steering force is computed in step 2 of the template. For a single behavior, the pattern is:

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  PVector behavior = computeBehavior(...);
  //  b - combine forces (one behavior)
  PVector steer = new PVector(0,0);
  steer.add(behavior);

Replace the parts in italics and the ... with appropriate values for the desired behavior. For example, for wander:

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  PVector wander = computeWander(pos,vel,maxspeed);
  //  b - combine forces (one behavior)
  PVector steer = new PVector(0,0);
  steer.add(wander);

Exercise 2

Your task is to create a sketch with a flock of 100 boids moving around the drawing window. To do this:

  • Create a new sketch named boids2 and paste in the starter code.

  • Add the boids library as directed in Sketches With More Than One File above.

  • Modify the sketch to have a flock of 100 boids: first complete the Exercise 2 section of the Boids I worksheet, then, based on your answers from the worksheet, array-ify the sketch so there are 100 boids. You can run the sketch at this point to see 100 boids, each with a random starting position and velocity, moving in straight lines across the screen.

  • Fill in steps 2a and 2b to implement flocking. Flocking is a combination of four behaviors: separation, alignment, and cohesion allow flocks to form, and forward provides a purpose so the flock goes somewhere. See the "Separation, Alignment, Cohesion, and Forward" and "Several Behaviors At Once" sections below for how to use these behaviors and how to fill in steps 2a and 2b in the case of multiple behaviors at once. You will also need to find a suitable set of weights for combining the behaviors — aim for something that looks similar to the demo though you do not need to replicate the demo exactly. (There should be distinct flocks, generally reasonable spacing between boids within a flock, and the boids shouldn't be too twitchy.)

Separation, Alignment, Cohesion, and Forward

The boids library contains the following functions for these behaviors.

// compute the separation steering vector
// separation pushes the boid away from each of its neighbors
//  pos, vel - position and velocity of boid
//  radius, angle - define this boid's neighborhood
//  p, v - positions and velocities of all boids
PVector computeSeparation ( PVector pos, PVector vel, float radius, float angle, PVector[] p, PVector[] v ) { ... }

// compute the alignment steering vector
// alignment steers the boid towards the average of the neighbors' velocities
//  pos, vel - position and velocity of boid
//  radius, angle - define this boid's neighborhood
//  p, v - positions and velocities of all boids
PVector computeAlignment ( PVector pos, PVector vel, float radius, float angle, PVector[] p, PVector[] v) { ... }

// compute the cohesion steering vector
// cohesion steers the boid towards the center of its neighbors
//  pos, vel - position and velocity of boid
//  radius, angle - define this boid's neighborhood
//  p, v - positions and velocities of all boids
PVector computeCohesion ( PVector pos, PVector vel, float radius, float angle, PVector[] p, PVector[] v) { ... }

// compute the forward steering vector
// forward steering vector sends the boid in its current direction at its max speed
//  pos, vel - position and velocity of boid
//  maxspeed - boid's max speed
PVector computeForward ( PVector pos, PVector vel, float maxspeed ) { ... }

Separation, alignment, and cohesion are group behaviors — in addition to the position and velocity of the boid who is to be steered (from one slot of the arrays), they need the positions and velocities of the whole flock (the arrays). That means with the array-ified pos and vel animation variables, you'll want something like the following inside a go-through-the-array loop:

  PVector separation = computeSeparation(pos[i],vel[i],radius,angle,pos,vel);

Several Behaviors at Once

The net steering force is computed in step 2 of the template. For several behaviors at once, the pattern is to compute steering vectors for each of the behaviors separately, then combine them:

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  PVector behavior1 = computeBehavior1(...);
  PVector behavior2 = computeBehavior2(...);
  PVector behavior3 = computeBehavior3(...);
  ...
  //  b - combine forces (one behavior)
  PVector steer = new PVector(0,0);
  behavior1.setMag(1);
  behavior2.setMag(1);
  behavior3.setMag(1);
  ...
  steer.add(behavior1);
  steer.add(behavior2);
  steer.add(behavior3);
  ...

Replace the parts in italics and the ... with appropriate values for the desired behavior. For example, for wander+seek:

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  PVector wander = computeWander(pos,vel,maxspeed);
  PVector seek = computeSeek(pos,vel,maxspeed,new PVector(mouseX,mouseY));
  //  b - combine forces (weighted sum)
  PVector steer = new PVector(0,0);
  wander.setMag(1);
  seek.setMag(1);
  steer.add(wander);
  steer.add(seek);

In both cases, separate steering vectors for each behavior are computed in step 2a and each of the steering vectors is added into the net steering vector steer in step 2b. When there is more than one behavior it is important to set the relative importance (or weight) of each using setMag — the particular values used are less important than their relative values. In the wander+seek case above, setting both magnitudes to the same value means that wander and seek are equally important in how the boid moves. A higher magnitude for wander means that the boid will tend to wander more than seek; a higher magnitude for seek means the opposite.

You'll likely need to experiment a bit to find a good set of magnitudes for a given application. In this case, try starting with equal weights (such as 1) for separation, alignment, and cohesion and a small weight (close to 0) for forward, then adjust the weights one at a time until you achieve something nice. For example, if the boids twitch in place without really moving forward, increase the weight for the forward behavior. If they clump too closely together, increase the weight for separation. If the clumps are small and/or take a long time to form, increase the weight for alignment. As you experiment, keep in mind that it isn't the particular values of the weights that matters so much as the relative values given to different behaviors. Also, it will be easier to find a good combination if you only change one weight at a time and run the sketch after each adjustment.


Exercise 3

Your task is to create a sketch in which boids seek the mouse while avoiding an obstacle and each other. To do this:

  • Create a new sketch named boids3 and paste in the starter code.

  • Add the boids library as directed in Sketches With More Than One File above.

  • Array-ify the sketch to have a flock of 100 boids as you did in exercise #2.

  • Create a randomly-placed obstacle: declare a PVector variable for its position, initialize the position to a random spot within the window (make sure the entire obstacle fits inside the window), and add code to draw the obstacle as a gray ellipse in the "step 1" section of the template. The position variable will be an animation variable (declare it at the beginning of the sketch) even though the obstacle's position is fixed because it gets a different random value each time the sketch runs. Note that you only need one obstacle even though the example has more than one!

  • Fill in steps 2a and 2b so that each boid chooses between four behaviors (avoid obstacles, avoid collisions with other boids, seek, and wander) as described below — first complete the Exercise 3 section of the Boids I worksheet, then, based on your answers from the worksheet, write the code.

    The particular action is chosen as follows:

    • If there's an obstacle in range, the boid should avoid it.
    • Otherwise if there's potential for colliding with another boid, avoid the collision.
    • Otherwise if the boid is near the mouse (within 200 pixels), seek on the mouse position.
    • Otherwise wander.

    See the "Avoid Obstacles, Avoid Collisions, Seek, and Wander" and "Different Behaviors at Different Times" sections below for how to use these behaviors and how to fill in steps 2a and 2b in the case of different behaviors being in effect at different times. To determine if the boid is near the mouse, use the dist function — with PVectors you'll need to extract the x and y parts separately e.g. to find the distance between a PVector p and the current mouse position use dist(p.x,p.y,mouseX,mouseY).

For extra credit, add multiple obstacles and modify the boid's choice of action so that it avoids any obstacles in range. (Handle collisions, the mouse, and wandering as before — only if if there are no obstacles in range.) Note that while the basic idea of array-ifying still holds here, you will need to do a little more than just wrapping arrays around the one-obstacle version of the code to correctly handle the boid's choice of action as described.

Avoid Obstacles, Avoid Collisions, Seek, and Wander

The boids library contains the following functions for these behaviors.

// compute the obstacle avoidance steering vector
//  pos, vel - position and velocity of boid
//  maxforce - maximum steering force that can be applied to the void
//  obspos, obsradius - position and radius of the obstacle to avoid
//  lookahead - how many timesteps into the future to consider
PVector computeObsAvoidance ( PVector pos, PVector vel, float maxforce, PVector obspos, float obsradius, float lookahead ) { ... }

// compute the collision avoidance steering vector (for avoiding other boids)
//  pos, vel - position and velocity of boid
//  maxforce - maximum steering force that can be applied to the void
//  p, v - positions and velocities of all boids
//  lookahead - how many timesteps into the future to consider 
PVector computeCollisionAvoidance ( PVector pos, PVector vel, float maxforce, PVector[] p, PVector[] v, float lookahead ) { ... }

// compute the seek steering vector
//  pos, vel - position and velocity of boid
//  maxspeed - boid's max speed
//  target - position of the target
PVector computeSeek ( PVector pos, PVector vel, float maxspeed, PVector target ) { ... }

// compute the wander steering vector
//  pos, vel - position and velocity of boid
//  maxspeed - boid's max speed
PVector computeWander ( PVector pos, PVector vel, float maxspeed ) { ... }

Both computeObsAvoidance and computeCollisionAvoidance have a lookahead parameter which is used to determine how far away the boid notices obstacles or potential collisions — experiment to find values that are large enough so that the boids generally miss the obstacle and each other, but not so large that they don't go anywhere near anything else. The units are in time steps (frames), so a value of 10, for example, means that the boid will look ahead 10 times the distance it travels from one frame to the next. (This allows the boid to automatically look farther ahead if it is traveling faster.)

For collision avoidance and obstacle avoidance, the computed steering vector has a magnitude of 0 if there is nothing to pursue or avoid. You can get the magnitude of a PVector v with v.mag() — so, for example, if you compute the obstacle avoidance steering vector and it has a magnitude of 0, there's no obstacle to avoid.

Different Behaviors at Different Times

The net steering force is computed in step 2 of the template. When different behaviors are used at different times, the pattern is to compute steering vectors for each of the potential behaviors separately, then use an if statement to combine those that are currently active. The wander or seek example illustrates this pattern — the boid wanders when it is in the left half of the window and seeks the mouse when it is in the right half.

  // 2 - compute net steering force
  //  a - compute steering force for each behavior
  PVector wander = computeWander(pos, vel, maxspeed);
  PVector seek = computeSeek(pos, vel, new PVector(mouseX, mouseY));
  //  b - combine forces (action selection)
  PVector steer = new PVector(0, 0);
  if ( pos.x < width/2 ) {  // wander in the left side of the window
    steer.add(wander);
  } else {  // seek in the right side of the window
    steer.add(seek);
  }

For this sketch, the structure for the if statement to choose between behaviors is given in the language above — if there's an obstacle in range then the boid should avoid it, otherwise if there's the potential for colliding with another boid... should sound a lot like the structure of a conditional with four alternatives.