| CPSC 120 | Principles of Computer Science | Fall 2025 |
|
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). |
Hand in a hardcopy (paper) of your worksheet in class or under my office door (Lansing 302).
To hand in your sketches:
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 boids1, boids2, and boids3 directories from your sketchbook (~/cs120/sketchbook) to your handin directory (found inside /classes/cs120/handin).
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.
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.
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.
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.
Also reference the slides from 11/12.
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.
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.
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.
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).
Do the exercises in order. Note: you must use the provided code and follow the patterns discussed in class and below for credit — achieving the end result by some other means will not count.
Read through all of each exercise before you start on it. In particular, note that the "to do this" steps are what you should actually do to complete the exercise — don't just read the first sentence of the problem, look at the example, and try to write the sketch from there. Follow the steps!
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.
Be sure to save your sketch frequently (ctrl-S). (Every time you run your sketch is good.) The editor does not auto-save!
|
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:
|
|
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 100 boids moving around the drawing window. To do this:
|
|
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:
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. |
|
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.
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.