每日算法 — 使用java实现弹球物理:碰撞检测与反射向量算法

弹球游戏是物理模拟与几何算法的经典结合。球体在边界和障碍物之间反弹,需要实时计算碰撞检测、反射方向与能量衰减。本文用Java实现一个完整的2D弹球物理引擎,涵盖圆形碰撞检测、法线反射向量计算和能量衰减系统。

算法核心思路

1. 圆形与线段碰撞检测

将边界抽象为线段。球心到线段的最短距离若小于球半径,则发生碰撞。通过向量投影计算垂足位置,判断垂足是否落在线段范围内。

2. 反射向量计算

碰撞后速度向量的反射遵循入射角等于反射角定律。公式为:v’ = v – 2(v·n)n,其中n是碰撞点处的单位法向量。该公式来源于向量在法线方向上的分量取反。

3. 能量衰减模拟

真实碰撞存在能量损失。引入弹性系数e(0≤e≤1),碰撞后速度大小乘以e。e=1为完全弹性碰撞,e<1时球体逐渐减速停止。

4. 球与球碰撞

两球碰撞时,先检测球心距离是否小于半径之和。碰撞法线方向为两球心连线的单位向量,速度按动量守恒和能量守恒分解到法线方向和切线方向后分别处理。

Java实现

import java.util.ArrayList;
import java.util.List;

public class BallPhysicsEngine {
    // 画布尺寸
    private final double width;
    private final double height;
    // 重力加速度
    private final double gravity = 0.15;
    // 边界弹性系数
    private final double wallRestitution = 0.85;

    static class Ball {
        double x, y;      // 位置
        double vx, vy;    // 速度
        double radius;    // 半径
        double mass;      // 质量
        double restitution; // 弹性系数

        Ball(double x, double y, double vx, double vy, double r, double m, double e) {
            this.x = x; this.y = y; this.vx = vx; this.vy = vy;
            this.radius = r; this.mass = m; this.restitution = e;
        }
    }

    static class LineSegment {
        double x1, y1, x2, y2;
        LineSegment(double x1, double y1, double x2, double y2) {
            this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
        }
    }

    private List<Ball> balls = new ArrayList<>();
    private List<LineSegment> walls = new ArrayList<>();

    public BallPhysicsEngine(double w, double h) {
        this.width = w; this.height = h;
        // 初始化四周边界
        walls.add(new LineSegment(0, 0, w, 0));       // 上
        walls.add(new LineSegment(w, 0, w, h));       // 右
        walls.add(new LineSegment(w, h, 0, h));       // 下
        walls.add(new LineSegment(0, h, 0, 0));       // 左
    }

    public void addBall(Ball b) { balls.add(b); }
    public void addWall(LineSegment w) { walls.add(w); }

    /**
     * 计算点(px,py)到线段的最短距离及垂足参数t
     * 返回值[距离, 垂足x, 垂足y, t]
     */
    private double[] pointToSegment(double px, double py, LineSegment seg) {
        double dx = seg.x2 - seg.x1;
        double dy = seg.y2 - seg.y1;
        double lenSq = dx * dx + dy * dy;
        double t = lenSq == 0 ? 0 : Math.max(0, Math.min(1, ((px - seg.x1) * dx + (py - seg.y1) * dy) / lenSq));
        double projX = seg.x1 + t * dx;
        double projY = seg.y1 + t * dy;
        double dist = Math.hypot(px - projX, py - projY);
        return new double[]{dist, projX, projY, t};
    }

    /**
     * 计算反射后的速度向量
     * v: 原速度向量 (vx, vy)
     * nx, ny: 单位法向量(指向球外侧)
     * e: 弹性系数
     */
    private double[] reflect(double vx, double vy, double nx, double ny, double e) {
        double dot = vx * nx + vy * ny;
        double rx = vx - 2 * dot * nx;
        double ry = vy - 2 * dot * ny;
        return new double[]{rx * e, ry * e};
    }

    /**
     * 更新一帧物理状态
     */
    public void update() {
        // 1. 应用重力并更新位置
        for (Ball b : balls) {
            b.vy += gravity;
            b.x += b.vx;
            b.y += b.vy;
        }

        // 2. 球与边界/墙壁碰撞检测与响应
        for (Ball b : balls) {
            for (LineSegment wall : walls) {
                double[] res = pointToSegment(b.x, b.y, wall);
                double dist = res[0];
                if (dist < b.radius) {
                    // 计算法向量(从线段指向球心)
                    double nx = (b.x - res[1]) / dist;
                    double ny = (b.y - res[2]) / dist;
                    // 将球推出墙壁
                    double overlap = b.radius - dist;
                    b.x += nx * overlap;
                    b.y += ny * overlap;
                    // 反射速度
                    double[] rv = reflect(b.vx, b.vy, nx, ny, b.restitution * wallRestitution);
                    b.vx = rv[0];
                    b.vy = rv[1];
                }
            }
        }

        // 3. 球与球碰撞
        for (int i = 0; i < balls.size(); i++) {
            for (int j = i + 1; j < balls.size(); j++) {
                Ball a = balls.get(i);
                Ball c = balls.get(j);
                double dx = c.x - a.x;
                double dy = c.y - a.y;
                double dist = Math.hypot(dx, dy);
                double minDist = a.radius + c.radius;

                if (dist < minDist && dist > 0) {
                    // 法线方向(从a指向c)
                    double nx = dx / dist;
                    double ny = dy / dist;
                    // 推出重叠
                    double overlap = (minDist - dist) / 2;
                    a.x -= nx * overlap; a.y -= ny * overlap;
                    c.x += nx * overlap; c.y += ny * overlap;
                    // 相对速度在法线方向的分量
                    double dvx = a.vx - c.vx;
                    double dvy = a.vy - c.vy;
                    double velAlongNormal = dvx * nx + dvy * ny;
                    if (velAlongNormal > 0) continue; // 正在分离
                    double e = Math.min(a.restitution, c.restitution);
                    double impulse = -(1 + e) * velAlongNormal / (1 / a.mass + 1 / c.mass);
                    double ix = impulse * nx;
                    double iy = impulse * ny;
                    a.vx += ix / a.mass; a.vy += iy / a.mass;
                    c.vx -= ix / c.mass; c.vy -= iy / c.mass;
                }
            }
        }
    }

    public void printState() {
        for (int i = 0; i < balls.size(); i++) {
            Ball b = balls.get(i);
            System.out.printf("Ball%d: pos=(%.2f, %.2f) vel=(%.2f, %.2f)%n", i, b.x, b.y, b.vx, b.vy);
        }
    }

    public static void main(String[] args) {
        BallPhysicsEngine engine = new BallPhysicsEngine(800, 600);
        engine.addBall(new Ball(400, 100, 3, 0, 20, 1.0, 0.9));
        engine.addBall(new Ball(500, 300, -2, 1, 15, 0.8, 0.85));

        System.out.println("=== 弹球物理模拟 ===");
        for (int frame = 0; frame < 20; frame++) {
            System.out.println("Frame " + frame + ":");
            engine.printState();
            engine.update();
        }
    }
}

复杂度分析

  • 时间复杂度:每帧更新为O(B² + B·W),B为球数量,W为墙壁线段数。球与球碰撞需要双重循环,球与墙碰撞为单重循环。
  • 空间复杂度:O(B + W),仅需存储球和墙壁的状态信息。

扩展方向

  1. 空间分区优化:使用四叉树或均匀网格加速碰撞检测,将O(B²)降为O(B·logB)或O(B)。
  2. 旋转体碰撞:扩展为带角速度的刚体,引入转动惯量和冲量矩计算。
  3. 连续碰撞检测(CCD):高速运动时避免穿透,计算TOI(Time of Impact)。