The Nature Of Code   Chp06 - Autonomous Agents

Example 6.1
Seeking A Target

Move mouse to interact   p5.js

//seeking "vehicle" follows the mouse position
//implements Craig Reynold's autonomous steering behaviors
//one vehicle "seeks"
let v;

function setup() {
  createCanvas(560,390);
  v = new Vehicle(width / 2, height / 2);
}

function draw() {
  background(220);

  let mouse = createVector(mouseX, mouseY);

  //draw an ellipse at the mouse position
  fill(100);
  stroke(50);
  strokeWeight(2);
  ellipse(mouse.x, mouse.y, 48, 48);

  //call the appropriate steering behaviors for agents
  v.seek(mouse);
  v.update();
  v.display();
}
class Vehicle {
  constructor(x, y) {
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(0, -2);
    this.position = createVector(x, y);
    this.r = 6;
    this.maxspeed = 8;
    this.maxforce = 0.2;
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelerationelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  applyForce(force) {
    //we could add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //method that calculates a steering force towards a target
  //steer = desired - velocity
  seek(target) {
    //vector pointing from the location to the target
    var desired = p5.Vector.sub(target, this.position);
    //scale to max speed
    desired.setMag(this.maxspeed);

    //steering = desired - velocity
    var steer = p5.Vector.sub(desired, this.velocity);
    //limit to max steering force
    steer.limit(this.maxforce);

    this.applyForce(steer);
  }

  display() {
    //draw a triangle rotated in the direction of velocity
    var theta = this.velocity.heading() + PI / 2;
    fill(63,63,147);
    stroke(50);
    strokeWeight(1);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }
}
							

Example 6.2
Arrive Steering Behavior

Move mouse to interact   p5.js

//"vehicle" follows the mouse position
//implements Craig Reynold's autonomous steering behaviors
//one vehicle "arrive"
let v;

function setup() {
  createCanvas(560,390);
  let text = createP("Move mouse to interact");
  text.position(15, 5);
  v = new Vehicle(width/2, height/2);
}

function draw() {
  background(220);

  let mouse = createVector(mouseX, mouseY);

  //draw an ellipse at the mouse position
  fill(100);
  stroke(50);
  strokeWeight(2);
  ellipse(mouse.x, mouse.y, 48, 48);

  //call the appropriate steering behaviors for agents
  v.arrive(mouse);
  v.update();
  v.display();
}
class Vehicle {
  constructor(x, y) {
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(0, -2);
    this.position = createVector(x, y);
    this.r = 6;
    this.maxspeed = 4;
    this.maxforce = 0.1;
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelerationelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //method that calculates a steering force towards a target
  //steer = desired - velocity
  arrive(target) {
    //vector pointing from the location to the target
    var desired = p5.Vector.sub(target, this.position);
    var d = desired.mag();
    //scale with arbitrary damping within 100 pixels
    if (d < 100) {
      var m = map(d, 0, 100, 0, this.maxspeed);
      desired.setMag(m);
    } else {
      desired.setMag(this.maxspeed);
    }

    //steering = desired - velocity
    var steer = p5.Vector.sub(desired, this.velocity);
    //limit to max steering force
    steer.limit(this.maxforce);
    this.applyForce(steer);
  }

  display() {
    //draw a triangle rotated in the direction of velocity
    var theta = this.velocity.heading() + PI / 2;
    fill(63,63,147);
    stroke(50);
    strokeWeight(1);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }
}
                            

Example 6.3
“Stay Within Walls” Steering Behavior

Click to toggle the wall   p5.js

//"made-up" steering behavior to stay within walls
let v;
let debug = true;
let d = 25;

function setup() {
  createCanvas(560,390);
  v = new Vehicle(width/2, height/2);
}

function draw() {
  background(220);

  if (debug) {
    stroke(175);
    noFill();
    rectMode(CENTER);
    rect(width/2, height/2, width-d*2, height-d*2);
  }

  //call the appropriate steering behaviors for agents
  v.boundaries();
  v.update();
  v.display();

}

function mousePressed() {
  debug = !debug;
}
class Vehicle {
  constructor(x, y) {
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(3, 4);
    this.position = createVector(x, y);
    this.r = 6;
    this.maxspeed = 3;
    this.maxforce = 0.15;
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelerationelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  boundaries() {

    let desired = null;

    if (this.position.x < d) {
      desired = createVector(this.maxspeed, this.velocity.y);
    } else if (this.position.x > width - d) {
      desired = createVector(-this.maxspeed, this.velocity.y);
    }

    if (this.position.y < d) {
      desired = createVector(this.velocity.x, this.maxspeed);
    } else if (this.position.y > height - d) {
      desired = createVector(this.velocity.x, -this.maxspeed);
    }

    if (desired !== null) {
      desired.normalize();
      desired.mult(this.maxspeed);
      let steer = p5.Vector.sub(desired, this.velocity);
      steer.limit(this.maxforce);
      this.applyForce(steer);
    }
  }

  display() {
    //draw a triangle rotated in the direction of velocity
    let theta = this.velocity.heading() + PI / 2;
    fill(63,63,147);
    stroke(50);
    strokeWeight(1);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }
}
                            

Example 6.4
Flow Field Following

Click to generate a new flow field   p5.js

//use this variable to decide whether to draw all the stuff
let debug = true;

//Flowfield object
let flowfield;
//an ArrayList of vehicles
let vehicles = [];

function setup() {
  let text = createP("Click to generate a new flow field");
  text.position(15, 5);

  createCanvas(560,390);
  //make a new flow field with "resolution" of 16
  flowfield = new FlowField(20);
  //make a whole bunch of vehicles with random maxspeed and maxforce values
  for (let i = 0; i < 120; i++) {
    vehicles.push(new Vehicle(random(width), random(height), random(2, 5), random(0.1, 0.5)));
  }
}

function draw() {
  background(220);
  //display the flowfield in "debug" mode
  if (debug) flowfield.display();
  //tell all the vehicles to follow the flow field
  for (let i = 0; i < vehicles.length; i++) {
    vehicles[i].follow(flowfield);
    vehicles[i].run();
  }
}


function keyPressed() {
  if (key == ' ') {
    debug = !debug;
  }
}

//make a new flowfield
function mousePressed() {
  flowfield.init();
}
class Vehicle {
  constructor(x, y, ms, mf) {
    this.position = createVector(x, y);
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(0, 0);
    this.r = 4;
    this.maxspeed = ms || 4;
    this.maxforce = mf || 0.1;
  }

  run() {
    this.update();
    this.borders();
    this.display();
  }

  //implementing Reynolds' flow field following algorithm
  // http://www.red3d.com/cwr/steer/FlowFollow.html
  follow(flow) {
    //find what is the vector at that spot in the flow field
    let desired = flow.lookup(this.position);
    //scale it up by maxspeed
    desired.mult(this.maxspeed);
    //steering = desired - velocity
    let steer = p5.Vector.sub(desired, this.velocity);
    //limit to max steering force
    steer.limit(this.maxforce);
    this.applyForce(steer);
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelerationelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  //wraparound
  borders() {
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > width + this.r) this.position.x = -this.r;
    if (this.position.y > height + this.r) this.position.y = -this.r;
  }

  display() {
    //draw a triangle rotated in the direction of velocity
    let theta = this.velocity.heading() + PI / 2;
    fill(63,63,147);
    stroke(50);
    strokeWeight(1);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }
}
class FlowField {
  constructor(r) {
    //find how large is each "cell" of the flow field
    this.resolution = r;
    //determine the number of columns and rows based on sketch's width and height
    this.cols = width / this.resolution;
    this.rows = height / this.resolution;
    //a flow field is a two dimensional array of p5.Vectors
    //we can't make 2D arrays, but this is sort of faking it
    this.field = this.make2Darray(this.cols);
    this.init();
  }

  make2Darray(n) {
    let array = [];
    for (let i = 0; i < n; i++) {
      array[i] = [];
    }
    return array;
  }

  init() {
    //reseed noise so we get a new flow field every time
    //need to get noise working
    noiseSeed(Math.floor(random(10000)));
    let xoff = 0;
    for (let i = 0; i < this.cols; i++) {
      let yoff = 0;
      for (let j = 0; j < this.rows; j++) {
        let theta = map(noise(xoff, yoff), 0, 1, 0, TWO_PI);
        //let theta = map(sin(xoff)+cos(yoff),-2,2,0,TWO_PI);
        //polar to cartesian coordinate transformation to get x and y components of the vector
        this.field[i][j] = createVector(cos(theta), sin(theta));
        yoff += 0.1;
      }
      xoff += 0.1;
    }
  }

  //draw every vector
  display() {
    for (let i = 0; i < this.cols; i++) {
      for (let j = 0; j < this.rows; j++) {
        this.drawVector(this.field[i][j], i * this.resolution, j * this.resolution, this.resolution - 2);
      }
    }
  }

  lookup(lookup) {
    let column = Math.floor(constrain(lookup.x / this.resolution, 0, this.cols - 1));
    let row = Math.floor(constrain(lookup.y / this.resolution, 0, this.rows - 1));
    //println(lookup.x);
    return this.field[column][row].copy();
  }

  //renders a vector object 'v' as an arrow and a location 'x,y'
  drawVector(v, x, y, scayl) {
    push();
    let arrowsize = 4;
    //translate to location to render vector
    translate(x, y);
    stroke(200, 100);
    //call vector heading function to get direction (pointing to the right is a heading of 0) and rotate
    rotate(v.heading());
    //calculate length of vector & scale it to be bigger or smaller if necessary
    let len = v.mag() * scayl;
    //draw three lines to make an arrow (draw pointing up since we've rotate to the proper direction)
    line(0, 0, len, 0);
    //line(len,0,len-arrowsize,+arrowsize/2);
    //line(len,0,len-arrowsize,-arrowsize/2);
    pop();
  }
}
                            

Example 6.5
Simple Path Following

Hit space bar to toggle debugging lines   p5.js

//path is a straight line in this example
//Via Reynolds: http://www.red3d.com/cwr/steer/PathFollow.html

//ues this variable to decide whether to draw all the stuff
let debug = true;
//a path object (series of connected points)
let path;
//two vehicles car1 and car2
let car1;
let car2;

function setup() {
  let text = createP("Hit space bar to toggle debugging lines");
  text.position(15, 5);
  
  createCanvas(560,390);
  path = new Path();

  //each vehicle has different maxspeed and maxforce for demo purposes
  car1 = new Vehicle(0, height / 2, 2, 0.05);
  car2 = new Vehicle(0, height / 2, 3, 0.10);
}

function draw() {
  background(220);
  //display the path
  path.display();
  //the boids follow the path
  car1.follow(path);
  car2.follow(path);
  //call generic run method (update, borders, display, etc.)
  car1.run();
  car2.run();

  //check if it gets to the end of the path since it's not a loop
  car1.borders(path);
  car2.borders(path);
}

function keyPressed() {
  if (key == ' ') {
    debug = !debug;
  }
}
class Vehicle {
  constructor(x, y, ms, mf) {
    this.position = createVector(x, y);
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(2, 0);
    this.r = 6;
    this.maxspeed = ms || 100;
    this.maxforce = mf || 0.1;
  }

  run() {
    this.update();
    this.display();
  }

  //this function implements Craig Reynolds' path following algorithm
  // http://www.red3d.com/cwr/steer/PathFollow.html
  follow(p) {
    //predict position 30 (arbitrary choice) frames ahead
    let predict = this.velocity.copy();
    predict.normalize();
    predict.mult(30);
    let predictLoc = p5.Vector.add(this.position, predict);

    //look at the line segment
    let a = p.start;
    let b = p.end;

    //get the normal point to that line
    let normalPoint = getNormalPoint(predictLoc, a, b);

    //find target point a little further ahead of normal
    let dir = p5.Vector.sub(b, a);
    dir.normalize();
    //this could be based on velocity instead of just an arbitrary 10 pixels
    dir.mult(10);
    let target = p5.Vector.add(normalPoint, dir);

    //find how far away are we from the path
    let distance = p5.Vector.dist(predictLoc, normalPoint);
    //if the distance is greater than the path's radius do we bother to steer
    if (distance > p.radius) {
      this.seek(target);
    }

    //draw the debugging stuff
    if (debug) {
      fill(100);
      stroke(50);
      line(this.position.x, this.position.y, predictLoc.x, predictLoc.y);
      ellipse(predictLoc.x, predictLoc.y, 6, 6);

      //draw normal location
      fill(100);
      stroke(50);
      line(predictLoc.x, predictLoc.y, normalPoint.x, normalPoint.y);
      ellipse(normalPoint.x, normalPoint.y, 6, 6);
      stroke(50);
      if (distance > p.radius) fill(255, 0, 0);
      noStroke();
      ellipse(target.x + dir.x, target.y + dir.y, 8, 8);
    }
  }


  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //method that calculates and applies a steering force towards a target
  //steer = desired - velocity
  seek(target) {
    //a vector pointing from the position to the target
    let desired = p5.Vector.sub(target, this.position);

    //if the magnitude of desired = 0, skip out of here
    //we could optimize this to check if x and y are 0 to avoid mag() square root
    if (desired.mag() === 0) return;

    //normalize desired and scale to maximum speed
    desired.normalize();
    desired.mult(this.maxspeed);
    //steering = desired - Velocity
    let steer = p5.Vector.sub(desired, this.velocity);
    //limit to maximum steering force
    steer.limit(this.maxforce);
    this.applyForce(steer);
  }

  //method to update position
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelerationelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  //wraparound
  borders(p) {
    if (this.position.x > p.end.x + this.r) {
      this.position.x = p.start.x - this.r;
      this.position.y = p.start.y + (this.position.y - p.end.y);
    }
  }

  display() {
    //draw a triangle rotated in the direction of velocity
    let theta = this.velocity.heading() + PI / 2;
    fill(63,63,147);
    stroke(50);
    strokeWeight(1);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }

}

//a function to get the normal point from a point (p) to a line segment (a-b)
//this function could be optimized to make fewer new Vector objects
function getNormalPoint(p, a, b) {
  //vector from a to p
  let ap = p5.Vector.sub(p, a);
  //vector from a to b
  let ab = p5.Vector.sub(b, a);
  //normalize the line
  ab.normalize();
  //project vector "diff" onto line by using the dot product
  ab.mult(ap.dot(ab));
  let normalPoint = p5.Vector.add(a, ab);
  return normalPoint;
}
class Path {
  constructor() {
    //a path has a radius, i.e how far is it ok for the boid to wander off
    this.radius = 20;
    //a Path is line between two points (p5.Vector objects)
    this.start = createVector(0, height / 3);
    this.end = createVector(width, 2 * height / 3);
  }

  //draw the path
  display() {
    strokeWeight(this.radius * 2);
    stroke(100, 50);
    line(this.start.x, this.start.y, this.end.x, this.end.y);

    strokeWeight(1);
    stroke(100);
    line(this.start.x, this.start.y, this.end.x, this.end.y);
  }
}
                            

Example 6.6
Path Following

Hit space bar to toggle debugging lines & click to generate a new path   p5.js

//path is a straight line in this example
//Via Reynolds: http://www.red3d.com/cwr/steer/PathFollow.html

//use this variable to decide whether to draw all the stuff
let debug = true;
//a path object (series of connected points)
let path;
//two vehicles
let car1;
let car2;

function setup() {
  let text = createP("Hit space bar to toggle debugging lines
Click to generate a new path"); text.position(15, 5); createCanvas(560,390); newPath(); //each vehicle has different maxspeed and maxforce for demo purposes car1 = new Vehicle(0, height / 2, 2, 0.04); car2 = new Vehicle(0, height / 2, 3, 0.1); } function draw() { background(220); //display the path path.display(); //the boids follow the path car1.follow(path); car2.follow(path); //call the generic run method (update, borders, display, etc.) car1.run(); car2.run(); //check if it gets to the end of the path since it's not a loop car1.borders(path); car2.borders(path); } function newPath() { //a path is a series of connected points //a more sophisticated path might be a curve path = new Path(); path.addPoint(-20, height / 2); path.addPoint(random(0, width / 2), random(0, height)); path.addPoint(random(width / 2, width), random(0, height)); path.addPoint(width + 20, height / 2); } function keyPressed() { if (key == ' ') { debug = !debug; } } function mousePressed() { newPath(); } class Vehicle { constructor(x, y, ms, mf) { this.position = createVector(x, y); this.acceleration = createVector(0, 0); this.velocity = createVector(2, 0); this.r = 6; this.maxspeed = ms || 4; this.maxforce = mf || 0.1; } run() { this.update(); this.display(); } //this function implements Craig Reynolds' path following algorithm //http://www.red3d.com/cwr/steer/PathFollow.html follow(p) { //predict location 50 (arbitrary choice) frames ahead //this could be based on speed let predict = this.velocity.copy(); predict.normalize(); predict.mult(50); let predictLoc = p5.Vector.add(this.position, predict); //find the normal to the path from the predicted location //look at the normal for each line segment and pick out the closest one let normal = null; let target = null; //start with a very high record distance that can easily be beaten let worldRecord = 1000000; //loop through all points of the path for (let i = 0; i < p.points.length - 1; i++) { //look at a line segment let a = p.points[i]; let b = p.points[i + 1]; //println(b); //get the normal point to that line let normalPoint = getNormalPoint(predictLoc, a, b); //this only works because we know our path goes from left to right //we could have a more sophisticated test to tell if the point is in the line segment or not if (normalPoint.x < a.x || normalPoint.x > b.x) { //this is something of a hacky solution, but if it's not within the line segment //consider the normal to just be the end of the line segment (point b) normalPoint = b.copy(); } //find how far away are we from the path let distance = p5.Vector.dist(predictLoc, normalPoint); //find ifwe beat the record and find the closest line segment if (distance < worldRecord) { worldRecord = distance; //if so the target we want to steer towards is the normal normal = normalPoint; //look at the direction of the line segment so we can seek a little bit ahead of the normal let dir = p5.Vector.sub(b, a); dir.normalize(); //this is an oversimplification //should be based on distance to path & velocity dir.mult(10); target = normalPoint.copy(); target.add(dir); } } //if the distance is greater than the path's radius do we bother to steer if (worldRecord > p.radius && target !== null) { this.seek(target); } //draw the debugging stuff if (debug) { //draw predicted future location stroke(50); fill(100); line(this.position.x, this.position.y, predictLoc.x, predictLoc.y); ellipse(predictLoc.x, predictLoc.y, 4, 4); //draw normal location stroke(50); fill(100); ellipse(normal.x, normal.y, 4, 4); //draw actual target (red if steering towards it) line(predictLoc.x, predictLoc.y, normal.x, normal.y); if (worldRecord > p.radius) fill(255, 0, 0); noStroke(); ellipse(target.x, target.y, 8, 8); } } applyForce(force) { //add mass here if we want A = F / M this.acceleration.add(force); } //a method that calculates and applies a steering force towards a target //steer = desired - velocity seek(target) { //a vector pointing from the position to the target let desired = p5.Vector.sub(target, this.position); //if the magnitude of desired = 0, skip out of here //we could optimize this to check if x and y are 0 to avoid mag() square root if (desired.mag() === 0) return; //normalize desired and scale to maximum speed desired.normalize(); desired.mult(this.maxspeed); //steering = desired - velocity let steer = p5.Vector.sub(desired, this.velocity); //limit to maximum steering force steer.limit(this.maxforce); this.applyForce(steer); } //method to update position update() { //update velocity this.velocity.add(this.acceleration); //limit speed this.velocity.limit(this.maxspeed); this.position.add(this.velocity); //reset accelerationelertion to 0 each cycle this.acceleration.mult(0); } //wraparound borders(p) { if (this.position.x > p.getEnd().x + this.r) { this.position.x = p.getStart().x - this.r; this.position.y = p.getStart().y + (this.position.y - p.getEnd().y); } } display() { //draw a triangle rotated in the direction of velocity let theta = this.velocity.heading() + PI / 2; fill(63,63,147); stroke(50); strokeWeight(1); push(); translate(this.position.x, this.position.y); rotate(theta); beginShape(); vertex(0, -this.r * 2); vertex(-this.r, this.r * 2); vertex(this.r, this.r * 2); endShape(CLOSE); pop(); } } //a function to get the normal point from a point (p) to a line segment (a-b) //this function could be optimized to make fewer new Vector objects function getNormalPoint(p, a, b) { //vector from a to p let ap = p5.Vector.sub(p, a); //vector from a to b let ab = p5.Vector.sub(b, a); ab.normalize(); // Normalize the line //project vector "diff" onto line by using the dot product ab.mult(ap.dot(ab)); let normalPoint = p5.Vector.add(a, ab); return normalPoint; } class Path { constructor() { //a path has a radius, i.e how far is it ok for the vehicle to wander off this.radius = 20; //a Path is an array of points (p5.Vector objects) this.points = []; } //add a point to the path addPoint(x, y) { let point = createVector(x, y); this.points.push(point); } getStart() { return this.points[0]; } getEnd() { return this.points[this.points.length - 1]; } //draw the path display() { //draw thick line for radius stroke(100, 50); strokeWeight(this.radius * 2); noFill(); beginShape(); for (let i = 0; i < this.points.length; i++) { vertex(this.points[i].x, this.points[i].y); } endShape(); //draw thin line for center of path stroke(100); strokeWeight(1); noFill(); beginShape(); for (let i = 0; i < this.points.length; i++) { vertex(this.points[i].x, this.points[i].y); } endShape(); } }

Example 6.7
Group Behavior: Separation

Drag to generate new vehicles   p5.js

//separation, via Reynolds: http://www.red3d.com/cwr/steer/

//list of vehicles
let vehicles = [];

function setup() {
  let text = createP("Drag to generate new vehicles");
  text.position(15, 5);

  createCanvas(560, 390);
  //making random vehicles and storing them in an array
  for (let i = 0; i < 25; i++) {
    vehicles.push(new Vehicle(random(width), random(height)));
  }
}

function draw() {
  background(220);

  for (let v of vehicles) {
    v.separate(vehicles);
    v.update();
    v.borders();
    v.display();
  }
}

function mouseDragged() {
  vehicles.push(new Vehicle(mouseX, mouseY));
}
class Vehicle {
  constructor(x, y) {
    //all the usual stuff
    this.position = createVector(x, y);
    this.r = 12;
    this.maxspeed = 3; //max speed
    this.maxforce = 0.2; //max steering force
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(0, 0);
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //separation
  //method checks for nearby vehicles and steers away
  separate(vehicles) {
    let desiredseparation = this.r * 2;
    let sum = createVector();
    let count = 0;
    //for every boid in the system, check if it's too close
    for (let i = 0; i < vehicles.length; i++) {
      let d = p5.Vector.dist(this.position, vehicles[i].position);
      //if the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        //calculate vector pointing away from neighbor
        let diff = p5.Vector.sub(this.position, vehicles[i].position);
        diff.normalize();
        diff.div(d); //weight by distance
        sum.add(diff);
        count++; //keep track of how many
      }
    }
    //average: divide by how many
    if (count > 0) {
      sum.div(count);
      //our desired vector is the average scaled to max speed
      sum.normalize();
      sum.mult(this.maxspeed);
      //implement Reynolds: steering = desired - velocity
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(this.maxforce);
      this.applyForce(steer);
    }
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  display() {
    fill(63,63,147);
    stroke(50);
    strokeWeight(2);
    push();
    translate(this.position.x, this.position.y);
    ellipse(0, 0, this.r, this.r);
    pop();
  }

  //wraparound
  borders() {
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > width + this.r) this.position.x = -this.r;
    if (this.position.y > height + this.r) this.position.y = -this.r;
  }
}

                            

Example 6.8
Combining Steering Behaviors: Seek And Separate

Slide bar to see the difference   p5.js

//separation, via Reynolds: http://www.red3d.com/cwr/steer/

//list of vehicles
let vehicles = [];

let slider1;
let slider2;
let slider3;

function setup() {
  createCanvas(560, 390);
  //making random vehicles and storing them in an array
  for (let i = 0; i < 50; i++) {
    vehicles.push(new Vehicle(random(width), random(height)));
  }
  slider1 = createSlider(0, 8, 4);
  slider2 = createSlider(0, 8, 4);
  slider3 = createSlider(10, 160, 24);
}

function draw() {
  background(220);
  for (let v of vehicles) {
    v.applyBehaviors(vehicles);
    v.update();
    v.borders();
    v.display();
  }
}
class Vehicle {
  constructor(x, y) {
    //all the usual stuff
    this.position = createVector(x, y);
    this.r = 12;
    this.maxspeed = 3; //max speed
    this.maxforce = 0.2; //max steering force
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(0, 0);
  }

  applyBehaviors(vehicles) {
    let separateForce = this.separate(vehicles);
    let seekForce = this.seek(createVector(mouseX, mouseY));

    separateForce.mult(slider1.value());
    seekForce.mult(slider2.value());

    this.applyForce(separateForce);
    this.applyForce(seekForce);
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //separation
  //method checks for nearby vehicles and steers away
  separate(vehicles) {
    let desiredseparation = slider3.value();
    let sum = createVector();
    let count = 0;
    //for every boid in the system, check if it's too close
    for (let i = 0; i < vehicles.length; i++) {
      let d = p5.Vector.dist(this.position, vehicles[i].position);
      //if the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        //calculate vector pointing away from neighbor
        let diff = p5.Vector.sub(this.position, vehicles[i].position);
        diff.normalize();
        diff.div(d); //weight by distance
        sum.add(diff);
        count++; //keep track of how many
      }
    }
    //average: divide by how many
    if (count > 0) {
      sum.div(count);
      //our desired vector is the average scaled to maximum speed
      sum.normalize();
      sum.mult(this.maxspeed);
      //implement Reynolds: steering = desired - velocity
      sum.sub(this.velocity);
      sum.limit(this.maxforce);
    }
    return sum;
  }

  //a method that calculates a steering force towards a target
  //steer = desired - velocity
  seek(target) {
    //a vector pointing from the location to the target
    let desired = p5.Vector.sub(target, this.position);

    //normalize desired and scale to max speed
    desired.normalize();
    desired.mult(this.maxspeed);
    //steering = desired - velocity
    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(this.maxforce); //limit to maximum steering force
    return steer;
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  display() {
    fill(63,63,147);
    stroke(50);
    strokeWeight(2);
    push();
    translate(this.position.x, this.position.y);
    ellipse(0, 0, this.r, this.r);
    pop();
  }

  //wraparound
  borders() {
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > width + this.r) this.position.x = -this.r;
    if (this.position.y > height + this.r) this.position.y = -this.r;
  }
}

                            

Example 6.9
Flocking

Drag to generate new boids   p5.js

//separation, via Reynolds: http://www.red3d.com/cwr/steer/

//list of vehicles
let vehicles = [];

let slider1;
let slider2;
let slider3;

function setup() {
  createCanvas(560, 390);
  //making random vehicles and storing them in an array
  for (let i = 0; i < 50; i++) {
    vehicles.push(new Vehicle(random(width), random(height)));
  }
  slider1 = createSlider(0, 8, 4);
  slider2 = createSlider(0, 8, 4);
  slider3 = createSlider(10, 160, 24);
}

function draw() {
  background(220);
  for (let v of vehicles) {
    v.applyBehaviors(vehicles);
    v.update();
    v.borders();
    v.display();
  }
}

//demonstration of Craig Reynolds' "Flocking" behavior
//http://www.red3d.com/cwr/
//rules: cohesion, separation, alignment
let flock;
let text;

function setup() {
  text = createP("Drag to generate new boids");
  text.position(15, 5);

  createCanvas(560, 390);
  flock = new Flock();
  //add an initial set of boids into the system
  for (let i = 0; i < 60; i++) {
    let b = new Boid(width / 2, height / 2);
    flock.addBoid(b);
  }
}

function draw() {
  background(220);
  flock.run();
}

//add a new boid into the System
function mouseDragged() {
  flock.addBoid(new Boid(mouseX, mouseY));
}
//flock object
//simply manages the array of all the boids
class Flock {
  constructor() {
    //an array for all the boids
    this.boids = []; //initialize array
  }

  run() {
    for (let boid of this.boids) {
      //passing the entire list of boids to each boid individually
      boid.run(this.boids);
    }
  }

  addBoid(b) {
    this.boids.push(b);
  }
}
//methods for separation, cohesion, alignment added
class Boid {
  constructor(x, y) {
    this.acceleration = createVector(0, 0);
    this.velocity = createVector(random(-1, 1), random(-1, 1));
    this.position = createVector(x, y);
    this.r = 4.0;
    this.maxspeed = 3; //max speed
    this.maxforce = 0.05; //max steering force
  }

  run(boids) {
    this.flock(boids);
    this.update();
    this.borders();
    this.render();
  }

  applyForce(force) {
    //add mass here if we want A = F / M
    this.acceleration.add(force);
  }

  //accumulate a new acceleration each time based on three rules
  flock(boids) {
    let sep = this.separate(boids); //separation
    let ali = this.align(boids); //alignment
    let coh = this.cohesion(boids); //cohesion
    //arbitrarily weight these forces
    sep.mult(1.5);
    ali.mult(1.0);
    coh.mult(1.0);
    //add the force vectors to acceleration
    this.applyForce(sep);
    this.applyForce(ali);
    this.applyForce(coh);
  }

  //method to update location
  update() {
    //update velocity
    this.velocity.add(this.acceleration);
    //limit speed
    this.velocity.limit(this.maxspeed);
    this.position.add(this.velocity);
    //reset accelertion to 0 each cycle
    this.acceleration.mult(0);
  }

  //a method that calculates and applies a steering force towards a target
  //steer = desired - velocity
  seek(target) {
    //a vector pointing from the location to the target
    let desired = p5.Vector.sub(target, this.position);
    //normalize desired and scale to max speed
    desired.normalize();
    desired.mult(this.maxspeed);
    //steering = desired - velocity
    let steer = p5.Vector.sub(desired, this.velocity);
    //limit to max steering force
    steer.limit(this.maxforce);
    return steer;
  }

  render() {
    //draw a triangle rotated in the direction of velocity
    let theta = this.velocity.heading() + radians(90);
    fill(63,63,147);
    stroke(50);
    push();
    translate(this.position.x, this.position.y);
    rotate(theta);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }

  //wraparound
  borders() {
    if (this.position.x < -this.r) this.position.x = width + this.r;
    if (this.position.y < -this.r) this.position.y = height + this.r;
    if (this.position.x > width + this.r) this.position.x = -this.r;
    if (this.position.y > height + this.r) this.position.y = -this.r;
  }

  //separation
  //method checks for nearby boids and steers away
  separate(boids) {
    let desiredseparation = 25.0;
    let steer = createVector(0, 0);
    let count = 0;
    //for every boid in the system, check if it's too close
    for (let i = 0; i < boids.length; i++) {
      let d = p5.Vector.dist(this.position, boids[i].position);
      //if the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        //calculate vector pointing away from neighbor
        let diff = p5.Vector.sub(this.position, boids[i].position);
        diff.normalize();
        diff.div(d); //weight by distance
        steer.add(diff);
        count++; //keep track of how many
      }
    }
    //average: divide by how many
    if (count > 0) {
      steer.div(count);
    }

    //as long as the vector is greater than 0
    if (steer.mag() > 0) {
      //implement Reynolds: steering = desired - velocity
      steer.normalize();
      steer.mult(this.maxspeed);
      steer.sub(this.velocity);
      steer.limit(this.maxforce);
    }
    return steer;
  }

  //alignment
  //for every nearby boid in the system, calculate the average velocity
  align(boids) {
    let neighbordist = 50;
    let sum = createVector(0, 0);
    let count = 0;
    for (let i = 0; i < boids.length; i++) {
      let d = p5.Vector.dist(this.position, boids[i].position);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(boids[i].velocity);
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      sum.normalize();
      sum.mult(this.maxspeed);
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(this.maxforce);
      return steer;
    } else {
      return createVector(0, 0);
    }
  }

  //cohesion
  //for the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
  cohesion(boids) {
    let neighbordist = 50;
    //start with empty vector to accumulate all locations
    let sum = createVector(0, 0);
    let count = 0;
    for (let i = 0; i < boids.length; i++) {
      let d = p5.Vector.dist(this.position, boids[i].position);
      if ((d > 0) && (d < neighbordist)) {
        //add location
        sum.add(boids[i].position);
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      //steer towards the location
      return this.seek(sum);
    } else {
      return createVector(0, 0);
    }
  }
}

                            

The Nature Of Code

  • Introduction
  • Vectors
  • Forces
  • Oscillation
  • Particle Systems
  • Physics Libraries
  • Autonomous Agents
  • speng2@wpi.edu  (Liz Peng)