CPSC 120 | Principles of Computer Science | Fall 2024 |
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.
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.
The short version:
Help with learning the process of constructing programs is fine; shortcutting the process and arriving at a result that you didn't produce yourself or don't fully understand how to produce is not.
Always attempt the problem yourself first, using this lab handout, the materials from class posted on the schedule page, and the assigned reading in the textbook.
Your primary resources for help should be office hours and the Teaching Fellows.
You may not work with other students to write code together.
You may not shortcut to a solution by copying code (except as specifically authorized in instructions) or using someone else's program as a guide or to understand what yours should be like even if you don't directly copy anything, You may not be in possession of someone else's program or solution before you have handed in your own.
You must document any help received (including from TFs) and any resources used other than the textbook and posted course materials. Put comments in your sketch indicating who helped (or the source used) and how / with what.
Make sure that you understand not only the result achieved, but also how one knows what to do to achieve that result. This gets at the process of writing a sketch — identifying what code elements are needed, filling in the details for a particular task, and pulling it all together.
Review the discussion in previous lab handouts and the full collaboration policy for more details.
To hand in your work:
Make sure that your name and a short description of the sketch are included in a comment at the beginning of each sketch.
Make sure that you've auto-formatted each sketch.
Copy the entire lab11a, lab11b, lab11c, and lab11d directories from your sketchbook (~/cs120/sketchbook) to your handin directory (found inside /classes/cs120/handin).
If you did any of the extra credit, also copy the entire lab11e directory from your sketchbook to your handin directory.
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.
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.
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.
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.
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 }
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.
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.
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.
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 lab11a and paste in template 1.
Add the boids library 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.
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);
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);
Your task is to create a sketch with a flock of boids moving around the drawing window. To do this:
Create a new sketch named lab11b and paste in template 1.
Add the boids library as directed in Sketches With More Than One File above.
Modify the sketch to have a flock of 100 boids. Use the array-ify process from lab 10 — in this case, the position and velocity variables will need to become arrays, with the appropriate changes to the initialization of those variables and the addition of loops around step 1 and steps 2-4. (The neighborhood radius and angle and the boid's maximum acceleration and speed will be the same for all boids, so these should not become arrays.)
Hint: You can put all of steps 2-4 into the body of one loop — it's OK not to have a separate loop for each part. The idea is that you repeat "update the boid" for each boid and steps 2-4 are all part of updating the boid. (It is legal to put a single loop around all of steps 1-4, but it is more organized to keep the "draw the scene" and "update the animation variables" parts separate.)
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.
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);
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.
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 lab11c and paste in template 1.
Add the boids library as directed in Sketches With More Than One File above.
Modify 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. The particular action is chosen as follows:
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).
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.
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.
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:
Create a new sketch named lab11d and paste in template 2. (The provided code in template 2 contains two boids, a prey boid and a predator boid, along with a food source for the prey boid.)
Add the boids library as directed in Sketches With More Than One File above.
Modify the sketch to have 100 prey boids — the prey position, velocity, and hunger variables will need to become arrays, with the appropriate changes to the initialization of those variables and the addition of loops to draw all the prey boids and around the entire "update prey" section. Give each of the 100 prey boids a random position, velocity, and hunger in setup.
Fill in prey steps 2a and 2b (in the "update prey" section) in draw() so the prey behaves as follows:
See the "Evade and Pursue" section below for how to use the evade behavior, and refer back to earlier exercises for the other behaviors. Use the weights you worked out for #2 as a starting point for the flocking behaviors, though you may need to tweak them a bit. You'll also need to find a good weight for evade.
Fill in predator steps 2a and 2b (in the "update predator" section) in draw() so the predator behaves as follows:
See the "Evade and Pursue" section below for how to use the pursue behavior, and refer back to earlier exercises for the other behaviors. What is "in sight" for the predator is defined by the predator's neighborhood — you can use the magnitude of the steering vector returned for the predator's pursue behavior to determine if any prey is in sight.
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.
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.)