Prashant Prakash
Prashant Prakash

Reputation: 135

The bouncing ball under gravity does not stop at bounds

I am making an application and need a ball to bounce under gravity. The ball bounces fine but it never stops. enter image description here

I tried printing the coordinate of the points where it stopped and what was the velocity. This is the output of one of the cases:

Found it... line 24. (0.00)i + (-0.01)j {14.14(0.79)}    (-1.79, 651.57)

So in the ball stopped at height 651.67 while the bound was 600. Here's another case:

Found it... line 24. (0.00)i + (-0.01)j {14.14(0.79)}    (-1.79, 1624.58)

Here's the code:

GUI.java

import java.util.Timer;
import java.util.TimerTask;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.stage.Stage;

public class GUI extends Application {

    @Override
    public void start(Stage theStage) throws Exception {
        theStage.setTitle("Bouncy Ball");

        Group root = new Group();
        Scene theScene = new Scene(root);
        theStage.setScene(theScene);

        Canvas canvas = new Canvas(750, 600);
        root.getChildren().add(canvas);

        CircleSprite sprite = new CircleSprite(30, new Point(50, 50));
        root.getChildren().add(sprite.image);
        sprite.setVelocity(new Vector(new Point(10, 10)));

        theStage.show();

        theStage.setOnCloseRequest(e -> {
            System.exit(0);
        });

        AnimationTimer gameLoop = new AnimationTimer() {

            @Override
            public void handle(long now) {
                if (sprite.update(new Bounds(700, 600))) {
                    this.stop();
                }
            }
        };

        gameLoop.start();

        new Timer().scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                Vector velocity = sprite.getVelocity();
                velocity.setYComponent(velocity.getYComponent() + 1);
                if (Math.abs(sprite.getVelocity().getYComponent()) <= 0.01
                        && sprite.getCentre().getY() + 2 * sprite.getRadius() >= 500) {
                            System.out.println("Found it... line 55");
                    cancel();
                }
            }
        }, 0, 100);
    }

    public static void main(String[] args) {
        launch(args);
    }

}

CircleSprite.java

public class CircleSprite extends Circle {
    public javafx.scene.shape.Circle image;

    public CircleSprite(long radius, Point centre) {
        super(radius, centre);
        image = new javafx.scene.shape.Circle(centre.getX(), centre.getY(), radius);
    }

    public boolean update(Bounds bounds) {
        Point pos = this.getCentre();
        Vector velocity = this.getVelocity();
        Point finalPos = new Point(pos.getX() + velocity.getXComponent(), pos.getY() + velocity.getYComponent());
        image.setLayoutX(finalPos.getX());
        image.setLayoutY(finalPos.getY());
        setCentre(finalPos);
        if (finalPos.getX() <= 0 || finalPos.getX() + 2 * getRadius() >= bounds.maxX) {
            velocity.setXComponent(velocity.getXComponent() * (-1) * 0.75);
        }
        if (finalPos.getY() <= 0 || finalPos.getY() + 2 * getRadius() >= bounds.maxY) {
            velocity.setYComponent(velocity.getYComponent() * (-1) * 0.75);
        }
        if (Math.abs(velocity.getYComponent()) <= 0.01 && getCentre().getY() + 2 * getRadius() >= bounds.maxY) {
            System.out.println("Found it... line 24" + velocity + "    " + getCentre());
            return true;
        }
        return false;
    }
}

Bounds.java

public class Bounds {
    public double maxX;
    public double maxY;

    public Bounds(double maxX, double maxY) {
        this.maxX = maxX;
        this.maxY = maxY;
    }
}

Circle.java

public class Circle extends Shape {
    private long radius;
    private Point centre;

    public Circle(long radius, Point centre) {
        this.radius = radius;
        this.centre = centre;
        this.setVelocity(new Vector(new Point(0.0d, 0.0d), new Point(0, 0)));
        this.setMass(0);
        this.setRestitution(1);
        this.setAcceleration(new Vector(new Point(0, 0), new Point(0, 0)));
    }

    /*
     * Testing for whether or not two circles intersect is very simple: take the
     * radii of the two circles and add them together, then check to see if this sum
     * is greater than the distance between the two circles.
     */
    public boolean isColliding(Circle a, Circle b) {
        long dist = a.getRadius() + b.getRadius();

        // In general multiplication is a much cheaper operation than taking the square
        // root of a value.

        return ((dist * dist) < (a.getCentre().getX() - b.getCentre().getX())
                * (a.getCentre().getX() - b.getCentre().getX())
                + (a.getCentre().getY() - b.getCentre().getY()) * (a.getCentre().getY() - b.getCentre().getY()));
    }

    public long getRadius() {
        return this.radius;
    }

    public void setRadius(long radius) {
        this.radius = radius;
    }

    public Point getCentre() {
        return this.centre;
    }

    public void setCentre(Point centre) {
        this.centre = centre;
    }
}

Shape.java

public class Shape {
    private Vector velocity;
    private Vector acceleration;
    private long mass;
    private double invMass;
    private float restitution;

    public Vector getAcceleration() {
        return this.acceleration;
    }

    public void setAcceleration(Vector acceleration) {
        this.acceleration = acceleration;
    }

    public long getMass() {
        return this.mass;
    }

    public void setMass(long mass) {
        this.mass = mass;
        this.setInvMass(mass);
    }

    public Vector getVelocity() {
        return this.velocity;
    }

    public void setVelocity(Vector velocity) {
        this.velocity = velocity;
    }

    public double getInvMass() {
        return this.invMass;
    }

    private void setInvMass(long mass) {
        if (mass == 0) {
            invMass = Long.MAX_VALUE;
            return;
        }
        this.invMass = 1.0d / (double) mass;
    }

    public float getRestitution() {
        return this.restitution;
    }

    public void setRestitution(float restitution) {
        this.restitution = restitution;
    }

}

Vector.java

public class Vector {
    private Point p1;
    private Point p2;
    private double xComponent;
    private double yComponent;
    private double angle;
    private double magnitude;

    /*
     * The constructor makes a vector crossing through two points p1 and p2.
     * 
     * @param p1 The source point(x1, x2)
     */
    public Vector(Point p1, Point p2) {
        this.p1 = p1;
        this.p2 = p2;
        this.xComponent = this.p2.getX() - this.p1.getX();
        this.yComponent = this.p2.getY() - this.p1.getY();
        this.angle = Math.atan2(this.yComponent, this.xComponent);
        this.magnitude = Math.sqrt(this.xComponent * this.xComponent + this.yComponent * this.yComponent);
    }

    public Vector(Point p2) {
        Point p1 = new Point(0, 0);
        this.p1 = p1;
        this.p2 = p2;
        this.xComponent = this.p2.getX() - this.p1.getX();
        this.yComponent = this.p2.getY() - this.p1.getY();
        this.angle = Math.atan2(this.yComponent, this.xComponent);
        this.magnitude = Math.sqrt(this.xComponent * this.xComponent + this.yComponent * this.yComponent);
    }

    public Vector(double magnitude, Vector unitVector) {
        scaledProduct(magnitude, unitVector);
    }

    private void scaledProduct(double magnitude, Vector unitVector) {
        Point point = new Point(magnitude * unitVector.getXComponent(), magnitude * unitVector.getYComponent());
        new Vector(point);
    }

    public static Vector scalarProduct(double magnitude, Vector unitVector) {
        Point point = new Point(magnitude * unitVector.getXComponent(), magnitude * unitVector.getYComponent());
        return new Vector(point);
    }

    public static double dotProduct(Vector v1, Vector v2) {
        return (v1.xComponent * v2.xComponent + v1.yComponent * v2.yComponent);
    }

    public static Vector sum(Vector v1, Vector v2) {
        return new Vector(new Point(v1.getXComponent() + v2.getXComponent(), v1.getYComponent() + v2.getYComponent()));
    }

    public static Vector difference(Vector from, Vector vector) {
        return new Vector(new Point(from.getXComponent() - vector.getXComponent(),
                from.getYComponent() - vector.getYComponent()));
    }

    public static double angleBetween(Vector v1, Vector v2) {
        return Math.acos(Vector.dotProduct(v1, v2) / (v1.getMagnitude() * v2.getMagnitude()));
    }

    public Point getP1() {
        return this.p1;
    }

    public void setP1(Point p1) {
        this.p1 = p1;
    }

    public Point getP2() {
        return this.p2;
    }

    public void setP2(Point p2) {
        this.p2 = p2;
    }

    public double getXComponent() {
        return this.xComponent;
    }

    public void setXComponent(double d) {
        this.xComponent = d;
    }

    public double getYComponent() {
        return this.yComponent;
    }

    public void setYComponent(double d) {
        this.yComponent = d;
    }

    public double getAngle() {
        return this.angle;
    }

    public void setAngle(double angle) {
        this.angle = angle;
    }

    public double getMagnitude() {
        return this.magnitude;
    }

    public void setMagnitude(double length) {
        this.magnitude = length;
    }

    @Override
    public boolean equals(Object v) {
        Vector vector = (Vector) v;
        return ((this.xComponent == vector.xComponent) && (this.yComponent == vector.yComponent));
    }

    @Override
    public String toString() {
        return String.format("(%.2f)i + (%.2f)j {%.2f(%.2f)}", this.xComponent, this.yComponent, this.magnitude,
                this.angle);
    }
}

Point.java

public class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public static double distance(Point p1, Point p2) {
        return Math.sqrt(
                (p1.getX() - p2.getX()) * (p1.getX() - p2.getX()) + (p1.getY() - p2.getY()) * (p1.getY() - p2.getY()));
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return String.format("(%.2f, %.2f)", this.x, this.y);
    }
}

What's wrong with the code? I know something is wrong with the exit condition in the GUI.java and CircleSprite.java but can't figure out what. There's two thing that is bothering me:

1. Why is the height out of bound when it stops?

2. Why does the x goes negative?

Upvotes: 0

Views: 583

Answers (1)

fabian
fabian

Reputation: 82461

What happens here is the following (considering x dimension only; same applies to y as well):

The ball passes through the bounds at a speed v. You reverse the speed and decrease its magnitude. This can result in the new velocity not being high enough to get back into the bounds in the next update step and you reverse the velocity again decreasing it even more. This results in the ball moving back and forth in smaller and smaller steps outside of the bounds effectively giving the impression of it stopping.

Example with values:

Frame 1
 vx = -16
 x  = 1

Frame 2
 vx = 12
  x = -15

Frame 3
 vx = -9
  x = -3

Frame 4
 vx = -6.75
  x = -12

...

There are 2 ways of fixing this:

  1. Only update the speed, if the ball is moving in the direction that made it exceede the bounds.

     public boolean update(Bounds bounds) {
         Point pos = this.getCentre();
         Vector velocity = this.getVelocity();
         Point finalPos = new Point(pos.getX() + velocity.getXComponent(), pos.getY() + velocity.getYComponent());
         image.setLayoutX(finalPos.getX());
         image.setLayoutY(finalPos.getY());
         setCentre(finalPos);
    
         if ((finalPos.getX() <= 0 && velocity.getXComponent() < 0) || (finalPos.getX() + 2 * getRadius() >= bounds.maxX && velocity.getXComponent() > 0)) {
             velocity.setXComponent(velocity.getXComponent() * (-1) * 0.75);
         }
         if ((finalPos.getY() <= 0 && velocity.getYComponent() < 0) || (finalPos.getY() + 2 * getRadius() >= bounds.maxY && velocity.getYComponent() > 0)) {
             velocity.setYComponent(velocity.getYComponent() * (-1) * 0.75);
         }
         if (Math.abs(velocity.getYComponent()) <= 0.01 && getCentre().getY() + 2 * getRadius() >= bounds.maxY) {
             System.out.println("Found it... line 24" + velocity + "    " + getCentre());
             return true;
         }
         return false;
     }
    
  2. Prevent the ball from ending up outside of the bounds in the first place

     public boolean update(Bounds bounds) {
         Point pos = this.getCentre();
         Vector velocity = this.getVelocity();
         Point finalPos = new Point(pos.getX() + velocity.getXComponent(), pos.getY() + velocity.getYComponent());
    
         boolean invertX = true;
         if (finalPos.getX() <= 0) {
             // mirror on left
             finalPos.setX(-finalPos.getX());
         } else if (finalPos.getX() + 2 * getRadius() >= bounds.maxX) {
             // mirror on right
             finalPos.setX(2 * (bounds.maxX - 2 * getRadius()) - finalPos.getX());
         } else {
             invertX = false;
         }
    
         if (invertX) {
             velocity.setXComponent(velocity.getXComponent() * (-1) * 0.75);
         }
    
         boolean invertY = true;
         if (finalPos.getY() <= 0) {
             // mirror on top
             finalPos.setY(-finalPos.getY());
         } else if (finalPos.getY() + 2 * getRadius() >= bounds.maxY) {
             // mirror on bottom
             finalPos.setY(2 * (bounds.maxY - 2 * getRadius()) - finalPos.getY());
         } else {
             invertY = false;
         }
    
         if (invertY) {
             velocity.setYComponent(velocity.getYComponent() * (-1) * 0.75);
         }
    
         setCentre(finalPos);
         image.setLayoutX(finalPos.getX());
         image.setLayoutY(finalPos.getY());
    
         if (Math.abs(velocity.getYComponent()) <= 0.01 && getCentre().getY() + 2 * getRadius() >= bounds.maxY) {
             System.out.println("Found it... line 24" + velocity + "    " + getCentre());
             return true;
         }
         return false;
     }
    

Upvotes: 2

Related Questions