Download the Tracking Agents App here – Agent_PathFollow_APP

A simple 2D Tracking Agent System built in Processing 3.0 inspired by Nature of Code.

Behavioral Classes
AppletObjectGUIPath
import java.util.List;
import toxi.geom.*;
import controlP5.*;
//-------------------------------------------------------------------------------------------
//---------------------------------------Globals--------------------------------------------
//-------------------------------------------------------------------------------------------
Vec3D locStart;

Agent a;
ControlP5 cp5;
GUI cgui;
Path tempPath;

boolean simulate;
boolean debug = false;
int resetAmount = 0; 
int agentCount = 250;

List<Agent> agentList;
//-------------------------------------------------------------------------------------------
//---------------------------------------Settings--------------------------------------------
//-------------------------------------------------------------------------------------------
void settings() {
  size(1920, 1080, FX2D);
  smooth();
}
//-------------------------------------------------------------------------------------------
//---------------------------------------Setup-----------------------------------------------
//-------------------------------------------------------------------------------------------
void setup() {
  background(0);
  this.agentList = new ArrayList<Agent>();
  this.simulate = true;
  if (this.resetAmount == 0) {
    this.cgui = new GUI();
    this.cgui.run(this);
  }
  newPath();
  for (int i = 0; i < this.agentCount; i ++) {
    this.locStart = new Vec3D(0, random(height), 0);
    this.a = new Agent(this.locStart);
    this.agentList.add(this.a);
  }
}
//-------------------------------------------------------------------------------------------
//---------------------------------------Draw------------------------------------------------
//-------------------------------------------------------------------------------------------
void draw() {
  background(0);

  this.agentCount = (int)this.cgui.s7.getValue();
  this.tempPath.display();
  this.tempPath.radius = this.cgui.s0.getValue();

  for (Agent ag : this.agentList) { //set the agent field values
    ag.maxforce = this.cgui.s6.getValue();
    ag.max = this.cgui.s5.getValue();
    ag.amp = this.cgui.s9.getValue();
    ag.vel = this.cgui.s4.getValue();
    ag.run();
  }
}
//-------------------------------------------------------------------------------------------
//---------------------------------------Create New Path-------------------------------------
//-------------------------------------------------------------------------------------------
void newPath() {
  this.tempPath = new Path();
  this.tempPath.addPoint(-20, height/2);
  this.tempPath.addPoint(random(0, width/2), random(0, height));
  this.tempPath.addPoint(random(width/2, width), random(0, height));
  tempPath.addPoint(width+20, height/2);
}
//-------------------------------------------------------------------------------------------
//---------------------------------------Keys------------------------------------------------
//-------------------------------------------------------------------------------------------
void keyPressed() {
  if (key == 'R') {
    this.resetAmount ++;
    setup();
  }
  if (key == 'S') this.simulate = !this.simulate;
  if (key == 'W') this.newPath();
  if (key == 'Q')this.debug = !this.debug; 
  if (key == 'S') {
    simulate = !simulate;
    saveFrame("img-######.png");
  }
}
class Agent {

  float vel = 1.0;
  float r, amp;
  float maxforce = 0.01;
  float max = 4.0f;

  List<Vec3D> objectTrails;

  Vec3D speed = new Vec3D(vel, 0, 0);
  Vec3D acc = new Vec3D(0, 0, 0);
  Vec3D loc;
  //-------------------------------------------------------------------------------------------
  Agent(Vec3D location) {
    loc = location;
    this.objectTrails = new ArrayList<Vec3D>();
    r = 12;
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Runs all other methods------------------------------
  //-------------------------------------------------------------------------------------------
  void run() {
    pathFollow();
    separate();
    if (simulate) {
      move();
    }
    viz();
    trail();
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Path Follow Method----------------------------------
  //-------------------------------------------------------------------------------------------
  void pathFollow() {
    
    Vec3D predict = this.speed.copy();
    predict.normalize();
    predict.scaleSelf(this.amp);
    Vec3D nextPosPrev = this.loc.add(predict);

    Vec3D target = null;
    Vec3D normal = null;
    float worldRecord = 1000000;

    for (int i = 0; i < tempPath.points.size()-1; i++) { 
      Vec3D a = tempPath.points.get(i);
      Vec3D b = tempPath.points.get(i+1);
      Vec3D normalPoint = getNormalPoint(nextPosPrev, a, b); //Finding the normals for each line segment
      if (normalPoint.x < min(a.x, b.x) || normalPoint.x > max(a.x, b.x)) {
        normalPoint = b.copy();
      }
      float distance = nextPosPrev.distanceTo(normalPoint);
      if (distance < worldRecord) {
        worldRecord = distance;
        normal = normalPoint;
        Vec3D dir = b.sub(a); // Look at the direction of the line segment so we can seek a little bit ahead of the normal
        dir.normalize();
        dir.scaleSelf(10);
        target = normalPoint.copy();
        target.add(dir);
      }
    }
    if (worldRecord > tempPath.radius ) {
      seek(target);
    } else {
      Vec3D zero = new Vec3D(0, 0, 0);
      zero.scaleSelf(3);
      this.acc.addSelf(zero);
    }
    // Draw the debugging stuff
    if (debug) {
      // Draw predicted future location
      stroke(255);
      fill(0);
      line(this.loc.x, this.loc.y, this.loc.x, this.loc.y);
      ellipse(nextPosPrev.x, nextPosPrev.y, 4, 4);
      // Draw normal location
      stroke(255);
      fill(0);
      ellipse(normal.x, normal.y, 4, 4);
      // Draw actual target (red if steering towards it)
      line(nextPosPrev.x, nextPosPrev.y, normal.x, normal.y);
      if (worldRecord > tempPath.radius) fill(255, 0, 0);
      noStroke();
      ellipse(target.x, target.y, 8, 8);
    }
  }
  //-------------------------------------------------------------------------------------------
  //-------------------Get the Normal Point on the Path Method---------------------------------
  //-------------------------------------------------------------------------------------------
  Vec3D getNormalPoint(Vec3D p, Vec3D a, Vec3D b) {
    Vec3D ap = p.sub(a);
    Vec3D ab = b.sub(a);
    ab.normalize();
    ab.scaleSelf(ap.dot(ab)); //Using the dot product for scalar projection
    Vec3D normalPoint = a.add(ab); // Finding the normal point along the line segment
    return normalPoint;
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Seek Target Method----------------------------------
  //-------------------------------------------------------------------------------------------
  void seek(Vec3D target) {
    Vec3D desired = target.sub(loc);
    desired.normalize();
    desired.scaleSelf(max);
    Vec3D steer = desired.sub(speed);
    steer.limit(maxforce); // Limit the magnitude of the steering force.
    steer.scaleSelf(3);
    applyForce(steer);
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Apply Force Method----------------------------------
  //-------------------------------------------------------------------------------------------
  void applyForce(Vec3D force) {
    this.acc.addSelf(force);
  }
  //-------------------------------------------------------------------------------------------
  //-------------------------------------------Move Method-------------------------------------
  //-------------------------------------------------------------------------------------------
  void move() {
    this.speed.addSelf(this.acc);
    this.speed.limit(this.max);
    this.loc.addSelf(this.speed);
    this.objectTrails.add(new Vec3D(this.loc));
    this.acc.clear();
  }
  //-------------------------------------------------------------------------------------------
  //-------------------------------------------Viz Method--------------------------------------
  //-------------------------------------------------------------------------------------------
  void viz() {
    stroke(255, 0, 0);
    strokeWeight(6);
    point(this.loc.x, this.loc.y);
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Separation Method----------------------------------
  //-------------------------------------------------------------------------------------------
  void separate () {
    float desiredseparation = r*2;
    Vec3D steer = new Vec3D(0, 0, 0);
    int count = 0;

    for (int i = 0; i < agentList.size(); i++) { // For every agent in the system, check if it's too close
      Agent other = (Agent) agentList.get(i);
      float d = this.loc.distanceTo(other.loc);
      if ((d > 0) && (d < desiredseparation)) { // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
        Vec3D diff = loc.sub(other.loc); // Calculate vector pointing away from neighbor
        diff.normalize();
        diff.normalizeTo(1/d); // Weight by distance
        steer.addSelf(diff);
        count++;// Keep track of how many
      }
    }
    if (count > 0) {
      steer.scaleSelf(1.0/(float)count); // Average
    }
    if (steer.magnitude() > 0) { // As long as the vector is greater than 0
      // Implement Reynolds: Steering = Desired - Velocity
      steer.normalize();
      steer.scaleSelf(this.max);
      steer.subSelf(this.speed);
      steer.limit(this.maxforce);
    }
    steer.scaleSelf(1);
    applyForce(steer);
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Trail Method----------------------------------------
  //-------------------------------------------------------------------------------------------
  void trail() {
    if (objectTrails.size() > 0) {
      for (int j = 0; j < objectTrails.size(); j++) {
        if (j != 0) {
          Vec3D pos = objectTrails.get(j);
          Vec3D prevpos = objectTrails.get(j - 1);
          stroke(255, 0, 0, map(j, 0, objectTrails.size(), 0, 200));
          strokeWeight(map(j, 0, objectTrails.size(), 0.45, 1.0));
          line(pos.x, pos.y, prevpos.x, prevpos.y);
        }
      }
    }
  }
}
class GUI {

  PApplet parent;
  ControlP5 cp5;
  int abc = 100;

  Slider s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
  Toggle t0;

  public GUI () {
  }

  public void run(PApplet _parent) {
    this.parent = _parent;
    this.cp5 = new ControlP5(this.parent);
    this.cp5.setFont(createFont("Arial", 10));

    this.s0 = cp5.addSlider("PathRad").plugTo(parent).setRange(0, 100).setPosition(10, 10).setValue(30.0).setDecimalPrecision(2).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40));
    this.s4 = cp5.addSlider("InitSpeed").plugTo(parent).setRange(0.00, 10.00).setPosition(10, 22).setValue(2.0).setDecimalPrecision(3).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40));
    this.s5 = cp5.addSlider("MaxSpeed").plugTo(parent).setRange(0.00, 10.00).setPosition(10, 34).setValue(4.0).setDecimalPrecision(3).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40));
    this.s6 = cp5.addSlider("MaxForce").plugTo(parent).setRange(0.00, 2.00).setPosition(10, 46).setValue(0.10).setDecimalPrecision(3).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40));
    this.s7 = cp5.addSlider("AgentCount").plugTo(parent).setRange(0.00, 500.00).setPosition(10, 58).setValue(250).setDecimalPrecision(3).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40));
    this.s9 = cp5.addSlider("Amplitude").plugTo(parent).plugTo(parent).setRange(0.00, 100.00).setPosition(10, 70).setValue(25).setDecimalPrecision(3).setSize(50, 10).setHandleSize(10).setColorForeground(color(255, 40)).setColorBackground(color(255, 40)) ;
  }

  public void draw() {
    background(0);
  }
}
class Path {

  ArrayList<Vec3D> points;
  float radius = 20; //path Radius

  Path() {
    points = new ArrayList<Vec3D>();
  }
  void addPoint(float x, float y) {   
    Vec3D point = new Vec3D(x, y, 0);
    points.add(point);
  }
  //-------------------------------------------------------------------------------------------
  //---------------------------------------Draw the Path---------------------------------------
  //-------------------------------------------------------------------------------------------
  void display() {
    // Draw thin line for center of path
    stroke(255);
    strokeWeight(1);
    noFill();
    beginShape();
    for (Vec3D v : points) {
      vertex(v.x, v.y);
    }
    endShape();
  }
}

Controls
  • R – reset simulation
  • W – change path
  • S – pause/resume simulation
  • P – save screenshot