Search code examples
javascriptcollision-detectiongame-physicscollisionp5.js

Changing direction of the ball after collision


I have written this code to demonstrate a basic visual p5js project. In here there are 10 balls of varying sizes and colors that spawn at random positions, move around in the canvas and might collide with each other. I am not looking for elastic collision or "realistic" collision physics for that matter. I just want the balls to change to a different direction (can be random as long as it works) and work accordingly.

Here is my code :

class Ball {
  //create new ball using given arguments
  constructor(pos, vel, radius, color) {
    this.pos = pos;
    this.vel = vel;
    this.radius = radius;
    this.color = color;
  }
  //collision detection
  collide(check) {
    if (check == this) {
      return;
    }
    let relative = p5.Vector.sub(check.pos, this.pos);
    let dist = relative.mag() - (this.radius + check.radius);

    if (dist < 0) { //HELP HERE! <--
      this.vel.mult(-1);
      check.vel.mult(-1);
    }
  }

  //give life to the ball
  move() {
    this.pos.add(this.vel);

    if (this.pos.x < this.radius) {
      this.pos.x = this.radius;
      this.vel.x = -this.vel.x;
    }
    if (this.pos.x > width - this.radius) {
      this.pos.x = width - this.radius;
      this.vel.x = -this.vel.x;
    }
    if (this.pos.y < this.radius) {
      this.pos.y = this.radius;
      this.vel.y = -this.vel.y;
    }
    if (this.pos.y > height - this.radius) {
      this.pos.y = height - this.radius;
      this.vel.y = -this.vel.y;
    }
  }
  //show the ball on the canvas
  render() {
    fill(this.color);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.radius * 2);
  }
}

let balls = []; //stores all the balls

function setup() {
  createCanvas(window.windowWidth, window.windowHeight);
  let n = 10;
  //loop to create n balls
  for (i = 0; i < n; i++) {
    balls.push(
      new Ball(
        createVector(random(width), random(height)),
        p5.Vector.random2D().mult(random(5)),
        random(20, 50),
        color(random(255), random(255), random(255))
      )
    );
  }
}

function draw() {
  background(0);
  //loop to detect collision at all instances
  for (let i = 0; i < balls.length; i++) {
    for (let j = 0; j < i; j++) {
      balls[i].collide(balls[j]);
    }
  }
  //loop to render and move all balls
  for (let i = 0; i < balls.length; i++) {
    balls[i].move();
    balls[i].render();
  }
}

Here is a link to the project : https://editor.p5js.org/AdilBub/sketches/TNn2OREsN

All I need is the collision to change the direction of the ball to a random direction and not get stuck. Any help would be appreciated. I am teaching kids this program so I just want basic collision, doesnot have to be "realistic".

Any help is appreciated. Thank you.


Solution

  • The issues you are currently encountering with balls being stuck has to do with randomly generating balls that overlap such that after one iteration of movement they still overlap. When this happens both balls will simply oscillate in place repeatedly colliding with each other. You can prevent this simply by checking for collisions before adding new balls:

    class Ball {
      //create new ball using given arguments
      constructor(pos, vel, radius, color) {
        this.pos = pos;
        this.vel = vel;
        this.radius = radius;
        this.color = color;
      }
      
      isColliding(check) {
        if (check == this) {
          return;
        }
        let relative = p5.Vector.sub(check.pos, this.pos);
        let dist = relative.mag() - (this.radius + check.radius);
        return dist < 0;
      }
      
      //collision detection
      collide(check) {
        if (this.isColliding(check)) {
          this.vel.x *= -1;
          this.vel.y *= -1;
          check.vel.x *= -1;
          check.vel.y *= -1;
        }
      }
    
      //give life to the ball
      move() {
        this.pos.add(this.vel);
    
        if (this.pos.x < this.radius) {
          this.pos.x = this.radius;
          this.vel.x = -this.vel.x;
        }
        if (this.pos.x > width - this.radius) {
          this.pos.x = width - this.radius;
          this.vel.x = -this.vel.x;
        }
        if (this.pos.y < this.radius) {
          this.pos.y = this.radius;
          this.vel.y = -this.vel.y;
        }
        if (this.pos.y > height - this.radius) {
          this.pos.y = height - this.radius;
          this.vel.y = -this.vel.y;
        }
      }
      //show the ball on the canvas
      render() {
        fill(this.color);
        noStroke();
        ellipse(this.pos.x, this.pos.y, this.radius * 2);
      }
    }
    
    let balls = []; //stores all the balls
    
    function setup() {
      createCanvas(500, 500);
      let n = 10;
      //loop to create n balls
      for (i = 0; i < n; i++) {
        let newBall =
          new Ball(
            createVector(random(width), random(height)),
            p5.Vector.random2D().mult(random(5)),
            random(20, 40),
            color(random(255), random(255), random(255))
          );
        let isOk = true;
        // check for collisions with existing balls
        for (let j = 0; j < balls.length; j++) {
          if (newBall.isColliding(balls[j])) {
            isOk = false;
            break;
          }
        }
        if (isOk) {
          balls.push(newBall);
        } else {
          // try again
          i--;
        }
      }
    }
    
    function draw() {
      background(0);
      //loop to detect collision at all instances
      for (let i = 0; i < balls.length; i++) {
        for (let j = 0; j < i; j++) {
          balls[i].collide(balls[j]);
        }
      }
      //loop to render and move all balls
      for (let i = 0; i < balls.length; i++) {
        balls[i].move();
        balls[i].render();
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>


    That said, fully elastic collisions (which means collisions are instantaneous and involve no loss of energy due to deformation and resulting heat emission) are actually quite simple to simulate. Here's a tutorial I made on OpenProcessing demonstrating the necessary concepts using p5.js: Elastic Ball Collision Tutorial.

    Here's the final version of the code from that tutorial:

    const radius = 30;
    const speed = 100;
    
    let time;
    
    let balls = []
    let boundary = [];
    let obstacles = [];
    
    let paused = false;
    
    function setup() {
        createCanvas(400, 400);
        angleMode(DEGREES);
        ellipseMode(RADIUS);
    
        boundary.push(createVector(60, 4));
        boundary.push(createVector(width - 4, 60));
        boundary.push(createVector(width - 60, height - 4));
        boundary.push(createVector(4, height - 60));
    
        obstacles.push(createVector(width / 2, height / 2));
    
        balls.push({
            pos: createVector(width * 0.25, height * 0.25),
            vel: createVector(speed, 0).rotate(random(0, 360))
        });
        balls.push({
            pos: createVector(width * 0.75, height * 0.75),
            vel: createVector(speed, 0).rotate(random(0, 360))
        });
        balls.push({
            pos: createVector(width * 0.25, height * 0.75),
            vel: createVector(speed, 0).rotate(random(0, 360))
        });
    
        time = millis();
    }
    
    function keyPressed() {
        if (key === "p") {
            paused = !paused;
            time = millis();
        }
    }
    
    function draw() {
        if (paused) {
            return;
        }
        
        deltaT = millis() - time;
        time = millis();
    
        background('dimgray');
    
        push();
        fill('lightgray');
        stroke('black');
        strokeWeight(2);
        beginShape();
        for (let v of boundary) {
            vertex(v.x, v.y);
        }
        endShape(CLOSE);
        pop();
    
        push();
        fill('dimgray');
        for (let obstacle of obstacles) {
            circle(obstacle.x, obstacle.y, radius);
        }
        pop();
    
        for (let i = 0; i < balls.length; i++) {
            let ball = balls[i];
    
            // update position
            ball.pos = createVector(
                min(max(0, ball.pos.x + ball.vel.x * (deltaT / 1000)), width),
                min(max(0, ball.pos.y + ball.vel.y * (deltaT / 1000)), height)
            );
    
            // check for collisions
            for (let i = 0; i < boundary.length; i++) {
                checkCollision(ball, boundary[i], boundary[(i + 1) % boundary.length]);
            }
    
            for (let obstacle of obstacles) {
                // Find the tangent plane that is perpendicular to a line from the obstacle to
                // the moving circle
    
                // A vector pointing in the direction of the moving object
                let dirVector = p5.Vector.sub(ball.pos, obstacle).normalize().mult(radius);
    
                // The point on the perimiter of the obstacle that is in the direction of the
                // moving object
                let p1 = p5.Vector.add(obstacle, dirVector);
                checkCollision(ball, p1, p5.Vector.add(p1, p5.Vector.rotate(dirVector, -90)));
            }
            
            // Check for collisions with other balls
            for (let j = 0; j < i; j++) {
                let other = balls[j];
                
                let distance = dist(ball.pos.x, ball.pos.y, other.pos.x, other.pos.y);
                if (distance / 2 < radius) {
                    push();
                    let midPoint = p5.Vector.add(ball.pos, other.pos).div(2);
                    let boundaryVector = p5.Vector.sub(other.pos, ball.pos).rotate(-90);
                    
                    let v1Parallel = project(ball.vel, boundaryVector);
                    let v2Parallel = project(other.vel, boundaryVector);
                    
                    let v1Perpendicular = p5.Vector.sub(ball.vel, v1Parallel);
                    let v2Perpendicular = p5.Vector.sub(other.vel, v2Parallel);
                    
                    ball.vel = p5.Vector.add(v1Parallel, v2Perpendicular);
                    other.vel = p5.Vector.add(v2Parallel, v1Perpendicular);
                    
                    let bounce = min(radius, 2 * radius - distance);
                    ball.pos.add(p5.Vector.rotate(boundaryVector, -90).normalize().mult(bounce));
                    other.pos.add(p5.Vector.rotate(boundaryVector, 90).normalize().mult(bounce));
                    
                    pop();
                }
            }
        }
    
        // Only draw balls after all position updates are complete
        for (let ball of balls) {
            circle(ball.pos.x, ball.pos.y, radius);
        }
    }
    
    function drawLine(origin, offset) {
        line(origin.x, origin.y, origin.x + offset.x, origin.y + offset.y);
    }
    
    // Handles collision with a plane given two points on the plane.
    // It is assumed that given a vector from p1 to p2, roating that vector
    // clockwise 90 degrees will give a vector pointing to the in-bounds side of the
    // plane (i.e. a "normal").
    function checkCollision(ball, p1, p2) {
        let boundaryVector = p5.Vector.sub(p2, p1);
        let objVector = p5.Vector.sub(ball.pos, p1);
        let angle = boundaryVector.angleBetween(objVector);
        let distance = objVector.mag() * sin(angle);
    
        if (distance <= radius) {
            // Collision
            let vParallel = project(ball.vel, boundaryVector);
            let vPerpendicular = p5.Vector.sub(ball.vel, vParallel);
    
            ball.vel = p5.Vector.add(vParallel, p5.Vector.mult(vPerpendicular, -1));
    
            let bounce = min(radius, (radius - distance) * 2);
            // If the ball has crossed over beyond the plane we want to offset it to be on
            // the in-bounds side of the plane.
            let bounceOffset = p5.Vector.rotate(boundaryVector, 90).normalize().mult(bounce);
            ball.pos.add(bounceOffset);
        }
    }
    
    // p5.Vector helpers
    function project(vect1, vect2) {
        vect2 = p5.Vector.normalize(vect2);
        return p5.Vector.mult(vect2, p5.Vector.dot(vect1, vect2));
    }
    
    function reject(vect1, vect2) {
        return p5.Vector.sub(vect1, project(vect1, vect2));
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>