Quantcast
Channel: Active questions tagged javascript - Stack Overflow
Viewing all articles
Browse latest Browse all 138192

Elastic Collision Logic Leading to Twitchy Balls When Combined with Gravity

$
0
0

Problem Description

I'm currently creating a Canvas based app that simulates balls and gravity. After the balls collide, their position is resolved and their velocity is calculated depending upon the angle they collide. However, once the balls begin to settle, they become "twitchy" and won't stop moving. How can I solve this?

The Relevant code

bindToScreen(s) {
    if (this.position.y + this.radius > s.height) {
        this.position.y = s.height - this.radius;
        this.velocity.y *= -this.bounce;
    }

    if (this.position.y - this.radius < 0) {
        this.position.y = this.radius;
        this.velocity.y *= -1;
    }

    if (this.position.x + this.radius > s.width) {
        this.position.x = s.width - this.radius;
        this.velocity.x *= -1;
    }

    if (this.position.x - this.radius < 0) {
        this.position.x = this.radius;
        this.velocity.x *= -1;
    }
}

handleCollision(p2, s) {
    let d      = this.position.distanceVector(p2);
    let angle  = Math.atan2(d.y, d.x),
        spread = (this.radius + p2.radius) - this.position.distanceBetween(p2) + 1,
        ax     = spread * Math.cos(angle),
        ay     = spread * Math.sin(angle);

    // Separate
    this.position.x -= ax * 2;
    this.position.y -= ay * 2;


    // Bounce
    this.velocity.x -= Math.cos(angle);
    this.velocity.y -= Math.sin(angle);
    p2.velocity.x += Math.cos(angle);
    p2.velocity.y += Math.sin(angle);

    if (this.position.x + this.radius + this.velocity.x > s.width) {
        this.position.x = s.width - this.radius;
    }

    if (this.position.y + this.radius + this.velocity.y > s.height) {
        this.position.y = s.height - this.radius;
    }

    this.bindToScreen(s);
}

Resources I've used to put this code together

  1. A tutorial on ball collisions
  2. A stack overflow question that helped with the angles

Update

As per AlbertoSinigaglia's suggestion, I tried to null the velocity if it was sufficiently small after a collision. However, that results in even weirder behaviour like floaty balls after a couple of collisions. Fiddle showing this here. I thought this may have been the gravity continuously increasing the y velocity, so I tried setting that to 0 too. Same problem.

    // If velocity is very small, set it to 0
    if(this.velocity.y > -0.1 && this.velocity.y < 0.1){
        this.velocity.y = 0;
        this.gravity = 0;
    }

    if(this.velocity.x > -1 && this.velocity.x < 1){
        this.velocity.x = 0;
    }

A full snippet

The code is unfortunately pretty long, so here's a hidden snippet.

class Canvas{
    constructor(w, h){
        this.width = w;
        this.height = h;
        this.element = document.getElementById('canvas');
        this.ctx = this.element.getContext('2d');

        this.element.width = this.width;
        this.element.height = this.height;
    }

    clear(){
        this.element.width = this.width;
    }

    drawCircle(c){
        this.ctx.beginPath();
        this.ctx.arc(c.x, c.y, c.radius, 0, 2 * Math.PI);
        this.ctx.fillStyle = c.color;
        this.ctx.fill();
        this.ctx.closePath();
    }
}

class Vector2D{
    constructor(x = 0, y = 0){
        this.x = x;
        this.y = y;
    }

    add(v){
        this.x += v.x;
        this.y += v.y;
    }

    sub(v){
        this.x -= v.x;
        this.y -= v.y;
    }

    mul(v){
        this.x *= v.x;
        this.y *= v.y;
    }

    div(v){
        this.x /= v.x;
        this.y /= v.y;
    }
    
    distanceVector(v2){
        return new Vector2D(v2.x-this.x, v2.y-this.y);
    }

    distanceBetween(v2){
        let dx = v2.x - this.x;
        let dy = v2.y - this.y;
        return Math.sqrt(dx*dx+dy*dy);
    }
}

class Circle{
    constructor(x, y, r, c){
        this.position = new Vector2D(x, y);
        this.radius = r;
        this.color = c;
    }

    get x(){
        return this.position.x;
    }

    get y(){
        return this.position.y;
    }  

    collision(c2){
        return this.position.distanceBetween(c2) < this.radius + c2.radius;
    }

    render(s){
        s.drawCircle(this);
    }
}

class Particle extends Circle {
    constructor(x, y, r, c, v) {
        super(x, y, r, c);
        this.velocity = new Vector2D(v.x, v.y);
        this.mass     = this.radius;
        this.gravity  = 0.8;
        this.bounce   = 0.8;
        this.resistance = 0.5;
    }
    
    bindToScreen(s) {
        if (this.position.y + this.radius > s.height) {
            this.position.y = s.height - this.radius;
            this.velocity.y *= -this.bounce;
        }
        if (this.position.y - this.radius < 0) {
            this.position.y = this.radius;
            this.velocity.y *= -1;
        }
        if (this.position.x + this.radius > s.width) {
            this.position.x = s.width - this.radius;
            this.velocity.x *= -1;
        }
        if (this.position.x - this.radius < 0) {
            this.position.x = this.radius;
            this.velocity.x *= -1;
        }
    }
    checkCollisions(p, s) {
        p.forEach(p2 => {
            if (p2 !== this) {
                if (this.collision(p2)) {
                    this.handleCollision(p2, s);
                }
            }
        });
    }
    handleCollision(p2, s) {
        let d      = this.position.distanceVector(p2);
        let angle  = Math.atan2(d.y, d.x),
            spread = (this.radius + p2.radius) - this.position.distanceBetween(p2) + 1,
            ax     = spread * Math.cos(angle),
            ay     = spread * Math.sin(angle);
        // Separate
        this.position.x -= ax * 2;
        this.position.y -= ay * 2;
        // Bounce
        this.velocity.x -= Math.cos(angle);
        this.velocity.y -= Math.sin(angle);
        p2.velocity.x += Math.cos(angle);
        p2.velocity.y += Math.sin(angle);
        if (this.position.x + this.radius + this.velocity.x > s.width) {
            this.position.x = s.width - this.radius;
        }
        if (this.position.y + this.radius + this.velocity.y > s.height) {
            this.position.y = s.height - this.radius;
        }
        this.bindToScreen(s);
    }
    update(s) {
        this.velocity.y += this.gravity;
        this.velocity.x *= -this.resistance;
        this.position.add(this.velocity);
        this.bindToScreen(s);
    }
}

class Game{
    constructor(){
        this.screen = new Canvas(800, 600);
        this.particles = [];
        this.particleCount = 10;
        this.particleSize = 20;
        this.delay = 25;
    }

    run(){
        for(let i = 0; i < this.particleCount; i++){
            let _this = this;

            setTimeout(() => {
                _this.particles.push(new Particle(this.particleSize, this.particleSize, this.particleSize, 'red', {x: 7, y: 7}));
            }, _this.delay * i);
        }

        this.loop();
    };

    update(){
        this.particles.forEach(particle => particle.update(this.screen));
    };

    checkCollisions(){
        this.particles.forEach(particle => particle.checkCollisions(this.particles, this.screen));
    }

    render(){
        this.screen.clear();
        this.particles.forEach(particle => particle.render(this.screen));
    };

    loop(){
        this.update();
        this.checkCollisions();
        this.render();
        requestAnimationFrame(this.loop.bind(this));
    }
}

let game = new Game();
game.run();
*{
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

html, body{
    background-color: #efefef;
}

canvas{
    width: 800px;
    height : 600px;
    background-color: rgba(255, 255, 255, 0.75);
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    margin: auto;
    box-shadow: 0 0 40px #dddddd;
}
<canvas id="canvas" width="800" height="600"></canvas>

Viewing all articles
Browse latest Browse all 138192

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>