Search code examples
javascriptgame-physicsp5.js

Separation Behaviour not working correctly


enter image description here

I have taken average displacement vector of each particle from a single particle and then reversed the direction and saved it as desired velocity,also applied this to every other particle but still the particles instead of separating away join each other at corners leaving large empty space in between

The seek method takes in the index of the particle currently being processed from the array and then calculates the average displacement vector of it from all other particles, then divided by the number of particles except itself. But still the particles behave in a very different way than i anticipated.

let spots = [];
let target;

function setup() {
  createCanvas(530, 530);
  for (let i = 0; i < 300; i++) {
    spots[i] = new Spots();
  }


}

class Spots {
  constructor() {
    this.x = random(0, 530);
    this.y = random(0, 530)
    this.pos = createVector(this.x, this.y);
    this.vel = p5.Vector.random2D();
    this.acc = createVector(0, 0);
    this.desiredvel = createVector();
    this.magn = 0;
    this.steeringForce = createVector();
    this.avg = createVector(0, 0);
  }

  seek(index) {
    let sum = createVector();
    let d;

    for (let h = 0; h < spots.length; h++) {
      d = dist(spots[h].pos.x, spots[h].pos.y, this.pos.x, this.pos.y)
      //console.log(d.mag())
      if ((h !== index)) {
        sum = p5.Vector.add(sum, p5.Vector.sub(spots[h].pos, this.pos))
        sum = sum.div(d)
      }
    }
    this.avg = sum.div(spots.length - 1);
    this.desiredvel = this.avg.mult(-2);
    this.steeringForce = p5.Vector.sub(this.desiredvel, this.vel);
    this.acc = this.steeringForce;
    this.magn = this.acc.mag();

  }
  edge() {
    if (this.pos.x < 0 || this.pos.x > 530) {
      this.vel.x = this.vel.x * (-1);
    } else if (this.pos.y < 0 || this.pos.y > 530) {
      this.vel.y = this.vel.y * (-1);
    }
  }

  move() {
    //console.log(this.pos);
    //console.log(this.vel);

    this.pos = p5.Vector.add(this.pos, this.vel);
    this.vel = p5.Vector.add(this.vel, this.acc);
    this.vel.setMag(1);
  }
  show() {
    stroke(255);
    strokeWeight(2);
    noFill();
    ellipse(this.pos.x, this.pos.y, 5, 5);
  }


}

class Targets {
  constructor(x, y) {
    this.pos = createVector(x, y);
  }
  show() {
    stroke(255);
    strokeWeight(4);
    fill(200, 0, 220);
    ellipse(this.pos.x, this.pos.y, 10, 10);
  }
}



function draw() {
  background(0);
  //spot.seek(target);
  for (let k = 0; k < spots.length; k++) {
    spots[k].seek(k);
    spots[k].edge();
    spots[k].move();
    spots[k].show();

  }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>


Solution

  • I made a tutorial walking through some issues with the current sketch and some ideas for how to achieve a better result.

    Here is the content of the tutorial, but it is better viewed on OpenProcessing.org.

    Part 1. Misc Cleanup

    The first thing I'm going to do for this tutorial is reduce the number of spots so that the sketch isn't so slow. I'll also introduce a constant for this value.

    Additionally to reduce duplication and potential errors in the Spots class I'm going to get rid of the x/y properties since they are the same values as pos.x and pos.y.

    Since the Targets class isn't being used I've removed that.

    Lastly I've moved the draw function above the Spots class declaration.

    Part 2. Fixing the Sum Calculation

    The following line of code in Spots.seek really didn't make any sense to me:

    sum = sum.div(d)
    

    This has the effect of massively decreasing the impact of all but the last direction vector on the average vector. To demonstrate this, here is what this formula looks like if we rewrite it in expanded form:

    sum = (((v1 / d1) + v2) / d2) ... + vn) / dn
    

    This can be rewritten as:

    sum =
      v1 / (d1 * d2 ... * dn) +
        v2 / (d2 ... * dn) +
        ... +
        vn / dn
    

    As you can see every v prior to vn is being divided by the product of all of the distances, which doesn't make sense.

    Perhaps you were trying to normalize each of these vectors before adding them to the sum?

    sum =
      v1 / d1 +
        v2 / d2 +
        ...
        vn / dn
    

    I've corrected the code using the Vector.normalize function:

    sum = p5.Vector.add(
      sum,
      p5.Vector.sub(spots[h].pos, this.pos)
          .normalize()
    );
    

    In order to account for the increased magnitude of the average vector (it should be magnitude 1 now), I decreased the multiplier used when calculating desiredvel.

    Part 3. Visualizing The Average Vector

    Now, the issue with the spots all clumping together in the corner is that when you take the average of all of the vectors from a given particle to all of the other particles you basically get a vector to the center of mass of the entire set of particles. And since we're reversing that to calculate the desired velocity, as particles move farther from the center of the group they will start to have similar desired velocity vectors.

    To exemplify this I've added graphical representations of both the average vectors and the velocity vectors.

    Particle Motion with Average Vectors

    Notice how all of the average vectors, shown in red, basically point towards the center. This trend continues and you eventually get clumps of particles in the corners.

    Here's the code so far:

    const spotCount = 100;
    
    let spots = [];
    let target;
    
    function setup() {
        createCanvas(530, 530);
        for (let i = 0; i < spotCount; i++) {
            spots[i] = new Spots();
        }
    }
    
    function draw() {
        background(0);
        //spot.seek(target);
        for (let k = 0; k < spots.length; k++) {
            spots[k].seek(k);
            spots[k].edge();
            spots[k].move();
            spots[k].show();
        }
    }
    
    class Spots {
        constructor() {
            this.pos = createVector(random(0, 530), random(0, 530));
            this.vel = p5.Vector.random2D();
            this.acc = createVector(0, 0);
            this.desiredvel = createVector();
            this.magn = 0;
            this.steeringForce = createVector();
            this.avg = createVector(0, 0);
        }
    
        seek(index) {
            let sum = createVector();
    
            for (let h = 0; h < spots.length; h++) {
                if ((h !== index)) {
                    sum = p5.Vector.add(sum, p5.Vector.sub(spots[h].pos, this.pos).normalize());
                }
            }
            this.avg = sum.div(spots.length - 1);
            this.desiredvel = this.avg.copy().mult(-0.1);
            this.steeringForce = p5.Vector.sub(this.desiredvel, this.vel);
            this.acc = this.steeringForce;
            this.magn = this.acc.mag();
    
        }
        edge() {
            if (this.pos.x < 0 || this.pos.x > 530) {
                this.vel.x = this.vel.x * (-1);
            } else if (this.pos.y < 0 || this.pos.y > 530) {
                this.vel.y = this.vel.y * (-1);
            }
        }
        move() {
            //console.log(this.pos);
            //console.log(this.vel);
    
            this.pos = p5.Vector.add(this.pos, this.vel);
            this.vel = p5.Vector.add(this.vel, this.acc);
            this.vel.setMag(1);
        }
        show() {
            stroke(255);
            strokeWeight(2);
            noFill();
            ellipse(this.pos.x, this.pos.y, 5, 5);
            push();
            strokeWeight(1);
            stroke('red');
            line(this.pos.x, this.pos.y, this.pos.x + this.avg.x * 100, this.pos.y + this.avg.y * 100);
            stroke('green');
            line(this.pos.x, this.pos.y, this.pos.x + this.vel.x * 10, this.pos.y + this.vel.y * 10);
            pop();
        }
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.js"></script>

    Part 4. A Better Model For Particle Motion

    In order to change this we need to change the amount of influence particles have on each other based on their distance from one another. So if particle A and particle B are on opposite sides of the screen they really shouldn't have any influence on each other, but if those to particles are right next to each other they should exert a substantial repulsive force. A reasonable choice to model this would be an inverse square relationship (force = max-force / distance²) which shows up in the formulas for gravitational force and magnetic force. We can also add an optimization to ignore particles that are greater than a certain distance away.

    Note: In order to get a stable result I had to limit the maximum speed of the particles. I believe this is necessitated by the discreet nature of the updates (when two particles are moving towards each other, and their positions are updated in discreet increments, they are able to get very close at which point they exert a massive repulsive force on each other.

    And here's the final code:

    const spotCount = 100;
    const maxForce = 1;
    const particleMass = 10;
    const pixelsPerMeter = 10;
    const maxSpeed = 2;
    
    let spots = [];
    let target;
    
    function setup() {
      createCanvas(530, 530);
      for (let i = 0; i < spotCount; i++) {
        spots[i] = new Spots();
      }
    }
    
    function draw() {
      background(0);
      //spot.seek(target);
      for (let k = 0; k < spots.length; k++) {
        spots[k].seek(k);
        spots[k].edge();
        spots[k].move();
        spots[k].show();
      }
    }
    
    class Spots {
      constructor() {
        this.pos = createVector(random(0, 530), random(0, 530));
        this.vel = p5.Vector.random2D();
        this.acc = createVector(0, 0);
      }
    
      seek(index) {
        let force = createVector();
        for (let h = 0; h < spots.length; h++) {
          if ((h !== index)) {
            // find the vector from the neighbor particle to the current particle
            let v = p5.Vector.sub(this.pos, spots[h].pos);
            let m = v.mag() / pixelsPerMeter;
            // If it is within 20 units
            if (m < 20) {
              // Add force in that direction according to the inverse square law
              force = force.add(v.normalize().mult(maxForce).div(m * m));
            }
          }
        }
        this.acc = force.div(particleMass);
    
      }
      edge() {
        if (this.pos.x < 0 || this.pos.x > 530) {
          this.vel.x = this.vel.x * (-1);
        } else if (this.pos.y < 0 || this.pos.y > 530) {
          this.vel.y = this.vel.y * (-1);
        }
      }
      move() {
        //console.log(this.pos);
        //console.log(this.vel);
    
        this.pos.add(this.vel);
        this.vel.add(this.acc);
        if (this.vel.magSq() > maxSpeed * maxSpeed) {
          this.vel.setMag(2);
        }
      }
      show() {
        stroke(255);
        strokeWeight(2);
        noFill();
        ellipse(this.pos.x, this.pos.y, 5, 5);
        push();
        strokeWeight(1);
        stroke('red');
        line(this.pos.x, this.pos.y, this.pos.x + this.acc.x * 100, this.pos.y + this.acc.y * 100);
        stroke('green');
        line(this.pos.x, this.pos.y, this.pos.x + this.vel.x * 10, this.pos.y + this.vel.y * 10);
        pop();
      }
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.js"></script>