如何正确检查2个移动椭圆(圆圈)之间的碰撞并更新位置+速度

时间:2014-03-23 18:57:02

标签: java math collision-detection collision game-physics

O'所以我试图在2个移动的椭圆(圆圈)之间编写一个简单的碰撞检测,并使用我发现的this指南更新它们的位置和速度。 然而,我得到的结果并不完美,对象正在碰撞和更新,但并不总是正确,它们会纠结一些,我很沮丧,因为我无法弄清楚为什么会发生这种情况。
注意:请忽略物体与边界的碰撞,它们有时会卡住(我需要更改它而不是我遇到问题的问题)

以下是我使用的2个类的代码:

package letifer.com;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class MaxwellsDemon extends JPanel{
    public static final long serialVersionUID = 1L;

    private final Dimension DIMS = new Dimension(800, 500);
    private final int UPDATE_INTERVAL = 20, BALLS_COUNT = 20;
    private final JFrame FRAME = new JFrame("MaxwellsDemon");
    private int leftCountR, leftCountB, rightCountR, rightCountB;
    private Ball balls[];
    private Random random;
    private Timer timer;

    public MaxwellsDemon(){
        super();
        initThis();
        initFrame();
        initBalls();
        registerTimer();
        timer.start();
    }

    private void initThis(){
        setSize(DIMS);
        setBackground(Color.white);
        random = new Random();
    }

    private void initFrame(){
        FRAME.setContentPane(new Container(){
            public void paint(Graphics g){
                super.paint(g);
                Graphics2D g2d = (Graphics2D) g.create();
                g2d.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
                for(int i=0; i<2; i++)
                    g2d.drawLine(0, DIMS.height+i, DIMS.width-1, DIMS.height+i);
                g2d.setColor(Color.blue);
                g2d.fillOval(25, DIMS.height+20, Ball.radius*2, Ball.radius*2);
                g2d.fillOval(DIMS.width-90, DIMS.height+20, Ball.radius*2, Ball.radius*2);
                g2d.setColor(Color.red);
                g2d.fillOval(25, DIMS.height+60, Ball.radius*2, Ball.radius*2);
                g2d.fillOval(DIMS.width-90, DIMS.height+60, Ball.radius*2, Ball.radius*2);
                g2d.setFont(new Font("Serif", Font.BOLD, 18));
                g2d.setColor(Color.black);
                g2d.drawString(String.format("= %d", leftCountB), 45, DIMS.height+32);
                g2d.drawString(String.format("= %d", leftCountR), 45, DIMS.height+72);
                g2d.drawString(String.format("= %d", rightCountB), DIMS.width-70, DIMS.height+32);
                g2d.drawString(String.format("= %d", rightCountR), DIMS.width-70, DIMS.height+72);
            }
        });
        FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        FRAME.getContentPane().setPreferredSize(new Dimension(DIMS.width, DIMS.height+100));
        FRAME.getContentPane().add(this);
        FRAME.setResizable(false);
        FRAME.pack();
        FRAME.setLocationRelativeTo(null);
        FRAME.setVisible(true);
    }

    private void initBalls(){
        balls = new Ball[BALLS_COUNT];
        leftCountR = 0; leftCountB = 0; rightCountR = 0; rightCountB = 0;
        for(int i=0, f=1; i<BALLS_COUNT; i++, f=-f){
            balls[i] = new Ball(new Point2D.Double(random.nextInt(DIMS.width-Ball.radius*2-1)+Ball.radius+1,
                                random.nextInt(DIMS.height-Ball.radius*2-1)+Ball.radius+1),
                                0.15,
                                random.nextDouble()*(Math.PI*2),
                                (f>0?Color.blue:Color.red));
            if(balls[i].c == Color.blue){
                if(balls[i].locOnScreen.x < DIMS.width/2){
                    leftCountB++;
                }else{
                    rightCountB++;
                }
            }else{
                if(balls[i].locOnScreen.x < DIMS.width/2){
                    leftCountR++;
                }else{
                    rightCountR++;
                }
            }
        }
    }

    private void update(){
        checkForCollisions();
        updateBalls();      
    }

    private void checkForCollisions(){
        for(int i=0; i<BALLS_COUNT-1; i++){
            for(int j=i+1; j<BALLS_COUNT; j++){
                double dx = balls[i].getX()-balls[j].getX();
                double dy = balls[i].getY()-balls[j].getY();
                Point2D.Double difference = new Point2D.Double(dx, dy);
                double distanceAtFrameEnd = Math.sqrt(dx*dx+dy*dy);
                double collisionDistance = Ball.radius*2;
                if(distanceAtFrameEnd < collisionDistance){

                    //go back in time :)
                    double millisecondsAfterCollision = moveBackToCollisionPoint(balls[i], balls[j], distanceAtFrameEnd, collisionDistance);

                    //calculate the new distance vector.
                    dx = balls[i].getX()-balls[j].getX();
                    dy = balls[i].getY()-balls[j].getY();
                    difference.setLocation(dx, dy);
                    Point2D.Double normalPlane = new Point2D.Double(difference.x, difference.y);
                    //normalize it
                    double distance = Math.sqrt(dx*dx+dy*dy);
                    normalPlane.x /= distance;
                    normalPlane.y /= distance;

                    //calculate the collision vector(rotate by 90deg PI/2).
                    Point2D.Double collisionPlane = new Point2D.Double(-normalPlane.y, normalPlane.x);

                    //calculate prior velocities relative the the collision plane and normal.
                    double velIx = balls[i].getVX(), velIy = balls[i].getVY();
                    double velJx = balls[j].getVX(), velJy = balls[j].getVY();
                    double n_vel1 = (velIx * normalPlane.x) + (velIy * normalPlane.y);
                    double c_vel1 = (velIx * collisionPlane.x) + (velIy * collisionPlane.y);
                    double n_vel2 = (velJx * normalPlane.x) + (velJy * normalPlane.y);
                    double c_vel2 = (velJx * collisionPlane.x) + (velJy * collisionPlane.y);

                    //calculate the scaler velocities of each object after the collision.
                    double n_vel1_after = ((n_vel1 * (1/*ballI mass*/ - 1/*ballJ mass*/)) + (2 * 1/*ballJ mass*/ * n_vel2)) / (1/*ballJ mass*/ + 1/*ballJ mass*/);
                    double n_vel2_after = ((n_vel2 * (1/*ballJ mass*/ - 1/*ballI mass*/)) + (2 * 1/*ballJ mass*/ * n_vel1)) / (1/*ballJ mass*/ + 1/*ballJ mass*/);
                    //double velObject2Tangent_After = c_vel2;
                    //double velObject1Tangent_After = c_vel1;

                    //convert the scalers to vectors by multiplying by the normalized plane vectors.
                    Point2D.Double vec_n_vel2_after = new Point2D.Double(n_vel2_after * normalPlane.x, n_vel2_after * normalPlane.y);
                    Point2D.Double vec_c_vel2 = new Point2D.Double(c_vel2 * collisionPlane.x, c_vel2 * collisionPlane.y);
                    Point2D.Double vec_n_vel1_after = new Point2D.Double(n_vel1_after * normalPlane.x, n_vel1_after * normalPlane.y);
                    Point2D.Double vec_c_vel1 = new Point2D.Double(c_vel1 * collisionPlane.x, c_vel1 * collisionPlane.y);

                    //combine the vectors back into a single vector in world space.
                    Point2D.Double vel1_after = new Point2D.Double(vec_n_vel1_after.x + vec_c_vel1.x, vec_n_vel1_after.y + vec_c_vel1.y);
                    Point2D.Double vel2_after = new Point2D.Double(vec_n_vel2_after.x + vec_c_vel2.x, vec_n_vel2_after.y + vec_c_vel2.y);

                    //reapply the move-back from before the collision (using the post collision velocity)
                    Point2D.Double object1AdjustedPositionAfterCollision = new Point2D.Double(balls[i].getX() + vel1_after.x * millisecondsAfterCollision, balls[i].getY() + vel1_after.y * millisecondsAfterCollision);
                    Point2D.Double object2AdjustedPositionAfterCollision = new Point2D.Double(balls[j].getX() + vel2_after.x * millisecondsAfterCollision, balls[j].getY() + vel2_after.y * millisecondsAfterCollision);

                    //set the objects new positions and velocities.
                    balls[i].setX(object1AdjustedPositionAfterCollision.x);
                    balls[i].setY(object1AdjustedPositionAfterCollision.y);
                    balls[j].setX(object2AdjustedPositionAfterCollision.x);
                    balls[j].setY(object2AdjustedPositionAfterCollision.y);

                    balls[i].setVX(vel1_after.x);
                    balls[i].setVY(vel1_after.y);
                    balls[j].setVX(vel2_after.x);
                    balls[j].setVY(vel2_after.y);                   
                }
            }
        }
    }

    private double moveBackToCollisionPoint(Ball object1, Ball object2, double distanceAtFrameEnd, double collisionDistance){
        //calc the position at the start of the frame.
        double object1PosAtFrameStart_X = (object1.getX() - object1.getVX() * (double)UPDATE_INTERVAL);
        double object1PosAtFrameStart_Y = (double)(object1.getY() - object1.getVY() * (double)UPDATE_INTERVAL);
        Point2D.Double object1PosAtFrameStart = new Point2D.Double(object1PosAtFrameStart_X, object1PosAtFrameStart_Y);

        double object2PosAtFrameStart_X = (object2.getX() - object2.getVX() * (double)UPDATE_INTERVAL);
        double object2PosAtFrameStart_Y = (object2.getY() - object2.getVY() * (double)UPDATE_INTERVAL);
        Point2D.Double object2PosAtFrameStart = new Point2D.Double(object2PosAtFrameStart_X, object2PosAtFrameStart_Y);

        //calc the distance between the objects at the start of the frame.
        Point2D.Double differenceAtFrameStart = new Point2D.Double(object2PosAtFrameStart.x - object1PosAtFrameStart.x, object2PosAtFrameStart.y - object1PosAtFrameStart.y);
        double distanceAtFrameStart = Math.sqrt(differenceAtFrameStart.x*differenceAtFrameStart.x + differenceAtFrameStart.y*differenceAtFrameStart.y);

        //calculate the total distance change during the frame and the required change to reach the collision.
        double distanceTotalDelta = distanceAtFrameEnd - distanceAtFrameStart;
        double distanceDeltaToCollision = collisionDistance - distanceAtFrameStart;

        // Calculate the percentage change to the collision and after the collision.
        double percentageDeltaToCollision = distanceDeltaToCollision / distanceTotalDelta;
        double percentageDeltaAfterCollision = 1 - percentageDeltaToCollision;

        // Calculate the time before and after the collision in the frame.
        double millisecondsToCollision = (double)UPDATE_INTERVAL * percentageDeltaToCollision;
        double millisecondsAfterCollision = (double)UPDATE_INTERVAL * percentageDeltaAfterCollision;

        // Calculate and move the objects to their positions at the point of collision.
        double object1PosAtCollision_X = (object1PosAtFrameStart_X + object1.getVX() * millisecondsToCollision);
        double object1PosAtCollision_Y = (object1PosAtFrameStart_Y + object1.getVY() * millisecondsToCollision);
        Point2D.Double object1PosAtCollision = new Point2D.Double(object1PosAtCollision_X, object1PosAtCollision_Y);
        object1.setX(object1PosAtCollision.x);
        object1.setY(object1PosAtCollision.y);

        double object2PosAtCollision_X = (object2PosAtFrameStart_X + object2.getVX() * millisecondsToCollision);
        double object2PosAtCollision_Y = (object2PosAtFrameStart_Y + object2.getVY() * millisecondsToCollision);
        Point2D.Double object2PosAtCollision = new Point2D.Double(object2PosAtCollision_X, object2PosAtCollision_Y);
        object2.setX(object2PosAtCollision.x);
        object2.setY(object2PosAtCollision.y);

        return millisecondsAfterCollision;
    }

    private void updateBalls(){
        leftCountR = 0; leftCountB = 0; rightCountR = 0; rightCountB = 0;
        for(int i=0; i<BALLS_COUNT; i++){
            balls[i].update(UPDATE_INTERVAL, DIMS);
            if(balls[i].c == Color.blue){
                if(balls[i].locOnScreen.x < DIMS.width/2){
                    leftCountB++;
                }else{
                    rightCountB++;
                }
            }else{
                if(balls[i].locOnScreen.x < DIMS.width/2){
                    leftCountR++;
                }else{
                    rightCountR++;
                }
            }
        }
    }

    private void registerTimer(){
        timer = new Timer(UPDATE_INTERVAL, new ActionListener() {           
            @Override
            public void actionPerformed(ActionEvent arg0) {
                update();
                FRAME.repaint();
            }
        });
        timer.setRepeats(true);
        timer.setDelay(UPDATE_INTERVAL);
    }

    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));

        for(int i=0; i<BALLS_COUNT; i++){
            g2d.setColor(balls[i].c);
            g2d.fillOval(balls[i].locOnScreen.x-Ball.radius, balls[i].locOnScreen.y-Ball.radius, Ball.radius*2, Ball.radius*2);
        }
    }

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

}

和Ball类:

package letifer.com;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.geom.Point2D;

public class Ball{

    public static int radius = 7;

    private Point2D.Double loc;
    private double speed;
    private double dir;
    Point locOnScreen;  
    Color c;

    public Ball(Point2D.Double loc, double speed, double dir, Color c){
        this.loc = loc;
        this.speed = speed;
        this.dir = dir;
        this.c = c;
        this.locOnScreen = new Point((int)(loc.x+0.5f), (int)(loc.y+0.5f));
    }

    public void update(int timePassed, Dimension dims){     
        loc.x = loc.x + speed * timePassed * Math.cos(dir);
        loc.y = loc.y + speed * timePassed * Math.sin(dir);
        if(loc.x <= 0 + radius){
            dir = (-dir+Math.PI+2*Math.PI)%(2*Math.PI);
        }
        if(loc.x >= dims.width - radius - 1){
            dir = (-dir+Math.PI+2*Math.PI)%(2*Math.PI);
        }
        if(loc.y <= 0 + radius){
            dir = (-dir+2*Math.PI)%(2*Math.PI);
        }
        if(loc.y >= dims.height - radius - 1){
            dir = (-dir+2*Math.PI)%(2*Math.PI);
        }   
        locOnScreen.setLocation((int)(loc.x+0.5f), (int)(loc.y+0.5f));
    }

    public double getX(){
        return loc.x;
    }

    public double getY(){
        return loc.y;
    }

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

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

    public double getVX(){
        return speed * Math.cos(dir);
    }

    public double getVY(){
        return speed * Math.sin(dir);
    }

    public void setVX(double vx){
        double vy = getVY();
        speed = Math.sqrt(vy*vy+vx*vx);
        dir = Math.atan2(vx, vy);
    }

    public void setVY(double vy){
        double vx = getVX();
        speed = Math.sqrt(vy*vy+vx*vx);
        dir = Math.atan2(vx, vy);
    }
}

PS:我不是一个java编码器,所以任何关于好/坏编码实践的注释都会受到赞赏。

1 个答案:

答案 0 :(得分:1)

设u i 并且v i 是当前时间步之前和之后的圆i的位置,假设i = 1,2分别没有碰撞。让r i 是圆i的半径,i = 1,2。设R = r 1 + r 2

假设圆圈在时间步长期间以恒定速度移动。也就是说,球i的路径由p i (t)=(1-t)u i + tv i 描述,t在[0,1]中,假设没有碰撞。

我们可以通过求解方程检测碰撞|| p 2 (t) - p 1 (t)|| = r 1 + r 2 = R代表t。让我们简化这个等式。

|| p 2 (t) - p 1 (t)|| = R

||(1-t)(u 2 - u 1 )+ t(v 2 - v 1 < / sub>)|| 2 = R 2

||(1 - t)U + tV || 2 = R 2 其中U = u 2 - u 1 和V = v 2 - v 1

&lt;(1-t)U + tV,(1-t)U + tV&gt; = R 2

(1-t)&lt; U,(1-t)U + tV&gt; + t&lt; V,(1-t)U + tV&gt; = R 2

(1-t) 2 &lt; U,U&gt; +(1-t)t&lt; U,V&gt; + t(1-t)&lt; V,U&gt; + t 2 &lt; V,V&gt; = R 2

(1-t) 2 &lt; U,U&gt; + 2(1-t)t&lt; U,V&gt; + t 2 &lt; V,V&gt; = R 2

此时应该清楚:这只是t中的二次方程式!解决t的可能值。如果区间[0,1]中没有真正的解,那么就没有碰撞。如果区间[0,1]中有任何实际解,则区间[0,1]中最早的(最小)解决方案描述了碰撞时间。

令T为区间[0,1]中描述的最早的实解。然后p 1 (T)和p 2 (T)是碰撞时的位置。这些位置之间的向量是碰撞的法线。