CPSC 120 Principles of Computer Science Fall 2024

Lab 11
Boids

Due: Fri 11/22 at the start of class

Labs are due at the start of class. It is OK if you show up and copy your files to the handin directory at the very beginning of class, but this should take at most a couple of minutes and you should not spend the next lab period finishing up the previous week's lab.


Introduction

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

This lab is 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 first three exercises introduce the three patterns for boid behavior: a single behavior, multiple behaviors active at once, and choosing between behaviors or sets of behaviors. The last exercise brings all those elements together.


Academic Integrity and Collaboration

The short version:

Review the discussion in previous lab handouts and the full collaboration policy for more details.

Handin

To hand in your work:


Preliminaries

Getting Started

This lab 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 in lab 10, 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.

Start by reading through the "Boids in Processing" section below. This gives you an overview of how the sketch templates you are provided with are organized, though you'll primarily be working with steps 2a and 2b to complete exercises.

Then look through the rest of this "Preliminaries" section and move on to the exercises.

Provided Code

Similar to the arrays lab, you won't be starting from scratch with your sketches. In this case, you will be filling in a few parts of a template and making use of a small library of functions that implement different steering behaviors.

Template 1 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.)

Template 2 is a variation of template 1 containing two boids (predator and prey) and a food source for the prey boid, for use in exercise #4.

The boids library 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, choose "New Tab", and enter "boids" as the name for the new file. You should see a second tab named "boids" appear next to the first. Switch to that tab, paste in the provided boids library code, and save your sketch.


Boids in Processing

Read through this section while looking at one (or more) of the wander, wander+seek, and wander or seek examples from class — match up each element that is talked about with the code in those sketches.

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
}

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 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 exercises below.


Exercises

Put your name and a description of the sketch in comments at the beginning of each sketch. Also don't forget to Auto Format your code before handing it in. (Refer back to lab 2 for more on Auto Format if needed.)

Be sure to follow the "to create this sketch" steps in each exercise! Also read through each bullet point in full before starting to work on it. This lab is about exploring some aspects of behavioral animation and emergent behavior within a specific framework of provided code, not implementing behavioral animation from scratch.

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:

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 boids moving around the drawing window. To do this:

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:

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 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.


Exercise 4

Your task is to create a sketch which simulates an ecosystem where prey boids forage for food and try to escape predators (as in the demo at the top of this page). To do this:

Evade and Pursue

The boids library contains the following function for this behavior:

// compute the evade steering vector 
//  pos, vel - position and velocity of boid
//  maxspeed - boid's max speed
//  pursuerpos, pursuervel - position and velocity of pursuer
PVector computeEvade ( PVector pos, PVector vel, float maxspeed, PVector pursuerpos, PVector pursuervel ) { ... }

// compute the pursue steering vector - pursues the nearest quarry within the boid's neighborhood
//  pos, vel - position and velocity of boid
//  radius, angle - define this boid's neighborhood
//  maxspeed - boid's max speed
//  quarrypos, quarryvel - positions and velocities of potential quarries
PVector computePursue ( PVector pos, PVector vel, float radius, float angle, float maxspeed, PVector[] quarrypos, PVector[] quarryvel ) { ... }

These functions return a steering vector with a magnitude of 0 if there is nothing in sight.


Extra Credit

You can earn extra credit by going substantially beyond the required elements. Create a new sketch lab11e and experiment with creating something interesting from the provided behaviors. (Include at least one that you haven't already used in this lab; there are other behavior functions in the boids library file that aren't described in this handout. Look through that file to find them — focus on the comments and the function headers. You can look at how they are implemented if you are curious, but it isn't necessary to do that to use them.) For credit, the combination of behaviors must result in some sort of describable individual or group behavior e.g. you might use offset pursuit to have a group of boids fly in formation, or you might have a game of tag where a boid pursues another that tries to hide behind obstacles. Include a comment in your sketch with that description.

You could also implement your own steering behaviors. Stop by to discuss possibilities if you are interested in this. (Some familiarity with working with vectors and/or geometry would be helpful.)