// Path Following // Daniel Shiffman // The Nature of Code, Spring 2009 // Vehicle class class Vehicle { // All the usual stuff PVector location; PVector velocity; PVector acceleration; float r; float maxforce; // Maximum steering force float maxspeed; // Maximum speed // Constructor initialize all values Vehicle( PVector l, float ms, float mf) { location = l.get(); r = 12; maxspeed = ms; maxforce = mf; acceleration = new PVector(0, 0); velocity = new PVector(maxspeed, 0); } // A function to deal with path following and separation void applyBehaviors(ArrayList vehicles, Path path) { // Follow path force PVector f = follow(path); // Separate from other boids force PVector s = separate(vehicles); // Arbitrary weighting f.mult(3); s.mult(1); // Accumulate in acceleration applyForce(f); applyForce(s); } void applyForce(PVector force) { // We could add mass here if we want A = F / M acceleration.add(force); } // Main "run" function public void run() { update(); borders(); render(); } // This function implements Craig Reynolds' path following algorithm // http://www.red3d.com/cwr/steer/PathFollow.html PVector follow(Path p) { // Predict location 25 (arbitrary choice) frames ahead PVector predict = velocity.get(); predict.normalize(); predict.mult(25); PVector predictLoc = PVector.add(location, predict); // Now we must find the normal to the path from the predicted location // We look at the normal for each line segment and pick out the closest one PVector normal = null; PVector target = null; float worldRecord = 1000000; // Start with a very high worldRecord distance that can easily be beaten // Loop through all points of the path for (int i = 0; i < p.points.size(); i++) { // Look at a line segment PVector a = p.points.get(i); PVector b = p.points.get((i+1)%p.points.size()); // Note Path has to wraparound // Get the normal point to that line PVector normalPoint = getNormalPoint(predictLoc, a, b); // Check if normal is on line segment PVector dir = PVector.sub(b, a); // If it's not within the line segment, consider the normal to just be the end of the line segment (point b) //if (da + db > line.mag()+1) { if (normalPoint.x < min(a.x,b.x) || normalPoint.x > max(a.x,b.x) || normalPoint.y < min(a.y,b.y) || normalPoint.y > max(a.y,b.y)) { normalPoint = b.get(); // If we're at the end we really want the next line segment for looking ahead a = p.points.get((i+1)%p.points.size()); b = p.points.get((i+2)%p.points.size()); // Path wraps around dir = PVector.sub(b, a); } // How far away are we from the path? float d = PVector.dist(predictLoc, normalPoint); // Did we beat the worldRecord and find the closest line segment? if (d < worldRecord) { worldRecord = d; normal = normalPoint; // Look at the direction of the line segment so we can seek a little bit ahead of the normal dir.normalize(); // This is an oversimplification // Should be based on distance to path & velocity dir.mult(25); target = normal.get(); target.add(dir); } } // Draw the debugging stuff if (debug) { // Draw predicted future location stroke(0); fill(0); line(location.x, location.y, predictLoc.x, predictLoc.y); ellipse(predictLoc.x, predictLoc.y, 4, 4); // Draw normal location stroke(0); fill(0); ellipse(normal.x, normal.y, 4, 4); // Draw actual target (red if steering towards it) line(predictLoc.x, predictLoc.y, target.x, target.y); if (worldRecord > p.radius) fill(255, 0, 0); noStroke(); ellipse(target.x, target.y, 8, 8); } // Only if the distance is greater than the path's radius do we bother to steer if (worldRecord > p.radius) { return seek(target); } else { return new PVector(0, 0); } } // 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 PVector getNormalPoint(PVector p, PVector a, PVector b) { // Vector from a to p PVector ap = PVector.sub(p, a); // Vector from a to b PVector ab = PVector.sub(b, a); ab.normalize(); // Normalize the line // Project vector "diff" onto line by using the dot product ab.mult(ap.dot(ab)); PVector normalPoint = PVector.add(a, ab); return normalPoint; } // Separation // Method checks for nearby boids and steers away PVector separate (ArrayList boids) { float desiredseparation = r*2; PVector steer = new PVector(0, 0, 0); int count = 0; // For every boid in the system, check if it's too close for (int i = 0 ; i < boids.size(); i++) { Vehicle other = (Vehicle) boids.get(i); float d = PVector.dist(location, other.location); // 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 PVector diff = PVector.sub(location, other.location); 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((float)count); } // As long as the vector is greater than 0 if (steer.mag() > 0) { // Implement Reynolds: Steering = Desired - Velocity steer.normalize(); steer.mult(maxspeed); steer.sub(velocity); steer.limit(maxforce); } return steer; } // Method to update location void update() { // Update velocity velocity.add(acceleration); // Limit speed velocity.limit(maxspeed); location.add(velocity); // Reset accelertion to 0 each cycle acceleration.mult(0); } // A method that calculates and applies a steering force towards a target // STEER = DESIRED MINUS VELOCITY PVector seek(PVector target) { PVector desired = PVector.sub(target, location); // A vector pointing from the location to the target // Normalize desired and scale to maximum speed desired.normalize(); desired.mult(maxspeed); // Steering = Desired minus Velocationity PVector steer = PVector.sub(desired, velocity); steer.limit(maxforce); // Limit to maximum steering force return steer; } void render() { // Simpler boid is just a circle fill(75); stroke(0); pushMatrix(); translate(location.x, location.y); ellipse(0, 0, r, r); popMatrix(); } // Wraparound void borders() { if (location.x < -r) location.x = width+r; //if (location.y < -r) location.y = height+r; if (location.x > width+r) location.x = -r; //if (location.y > height+r) location.y = -r; } }