每日算法 — 使用java实现壁球:圆形碰撞检测与反射向量物理模拟

一、壁球运动与算法概述

壁球(Squash)是一项在四面封闭场地内进行的球类运动,球在墙壁、地板和天花板之间高速反弹。要在计算机中模拟壁球运动,核心算法挑战在于:实时碰撞检测与反射向量计算。

本文将用Java实现一个完整的壁球物理模拟系统,涵盖以下核心算法:
– 圆形-线段碰撞检测:判断球是否与墙壁发生碰撞
– 反射向量计算:根据入射向量和法向量计算反弹方向
– 运动轨迹预测:基于初始速度和重力模拟球的运动路径
– 帧间插值:确保高速运动下碰撞不会穿透墙壁

二、物理模型建立

2.1 基本物理量定义

壁球场地可简化为一个矩形区域,球为圆形刚体。我们需要定义:
– 球的位置向量 P = (x, y)
– 球的速度向量 V = (vx, vy)
– 球的半径 r
– 场地边界 (0, 0) 到 (width, height)
– 重力加速度 g(竖直向下)
– 弹性系数 e(0 < e ≤ 1,控制能量损失)

2.2 运动学方程

在不发生碰撞时,球的运动遵循简单的匀加速运动:

P(t+Δt) = P(t) + V(t) * Δt
V(t+Δt) = V(t) + g * Δt

三、核心算法实现

3.1 向量类与基础运算

public class Vector2D {
    public double x, y;

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

    public Vector2D add(Vector2D v) {
        return new Vector2D(x + v.x, y + v.y);
    }

    public Vector2D sub(Vector2D v) {
        return new Vector2D(x - v.x, y - v.y);
    }

    public Vector2D multiply(double scalar) {
        return new Vector2D(x * scalar, y * scalar);
    }

    public double dot(Vector2D v) {
        return x * v.x + y * v.y;
    }

    public double magnitude() {
        return Math.sqrt(x * x + y * y);
    }

    public Vector2D normalize() {
        double mag = magnitude();
        if (mag < 1e-10) return new Vector2D(0, 0);
        return new Vector2D(x / mag, y / mag);
    }

    /**
     * 计算反射向量
     * R = I - 2 * (I·N) * N
     * I为入射向量,N为表面法向量(单位向量)
     */
    public Vector2D reflect(Vector2D normal) {
        Vector2D n = normal.normalize();
        double dot = this.dot(n);
        return this.sub(n.multiply(2 * dot));
    }

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

3.2 壁球类定义

public class SquashBall {
    private Vector2D position;  // 位置
    private Vector2D velocity;  // 速度
    private double radius;      // 半径
    private double mass;        // 质量
    private double elasticity;  // 弹性系数

    public SquashBall(double x, double y, double vx, double vy, 
                      double radius, double elasticity) {
        this.position = new Vector2D(x, y);
        this.velocity = new Vector2D(vx, vy);
        this.radius = radius;
        this.mass = radius * radius * Math.PI; // 质量与面积成正比
        this.elasticity = elasticity;
    }

    // Getters and setters
    public Vector2D getPosition() { return position; }
    public Vector2D getVelocity() { return velocity; }
    public double getRadius() { return radius; }

    public void setPosition(Vector2D p) { this.position = p; }
    public void setVelocity(Vector2D v) { this.velocity = v; }

    /**
     * 应用重力加速度
     */
    public void applyGravity(double g, double dt) {
        velocity.y += g * dt;
    }

    /**
     * 更新位置(欧拉积分)
     */
    public void updatePosition(double dt) {
        position = position.add(velocity.multiply(dt));
    }

    /**
     * 根据弹性系数反弹后速度衰减
     */
    public void dampenVelocity() {
        velocity = velocity.multiply(elasticity);
    }
}

3.3 碰撞检测引擎

public class CollisionEngine {
    private double courtWidth;
    private double courtHeight;
    private static final double EPSILON = 1e-9;

    public CollisionEngine(double width, double height) {
        this.courtWidth = width;
        this.courtHeight = height;
    }

    /**
     * 检测球与墙壁的碰撞并处理反弹
     * 返回是否发生了碰撞
     */
    public boolean handleWallCollision(SquashBall ball) {
        Vector2D pos = ball.getPosition();
        Vector2D vel = ball.getVelocity();
        double r = ball.getRadius();
        boolean collided = false;

        // 左墙碰撞检测:x - r < 0
        if (pos.x - r < EPSILON) {
            pos.x = r; // 将球移回场内
            if (vel.x < 0) {
                vel = vel.reflect(new Vector2D(1, 0)); // 法向量指向右侧
                collided = true;
            }
        }
        // 右墙碰撞检测:x + r > width
        else if (pos.x + r > courtWidth - EPSILON) {
            pos.x = courtWidth - r;
            if (vel.x > 0) {
                vel = vel.reflect(new Vector2D(-1, 0)); // 法向量指向左侧
                collided = true;
            }
        }

        // 天花板碰撞检测:y - r < 0
        if (pos.y - r < EPSILON) {
            pos.y = r;
            if (vel.y < 0) {
                vel = vel.reflect(new Vector2D(0, 1)); // 法向量指向下
                collided = true;
            }
        }
        // 地板碰撞检测:y + r > height
        else if (pos.y + r > courtHeight - EPSILON) {
            pos.y = courtHeight - r;
            if (vel.y > 0) {
                vel = vel.reflect(new Vector2D(0, -1)); // 法向量指向上
                collided = true;
            }
        }

        if (collided) {
            ball.setPosition(pos);
            ball.setVelocity(vel);
            ball.dampenVelocity();
        }

        return collided;
    }

    /**
     * 连续碰撞检测(CCD):防止高速球穿透墙壁
     * 使用射线-线段相交检测预测碰撞时间
     */
    public double predictCollisionTime(SquashBall ball) {
        Vector2D pos = ball.getPosition();
        Vector2D vel = ball.getVelocity();
        double r = ball.getRadius();

        double minTime = Double.MAX_VALUE;

        // 计算到达每个边界的时间
        if (vel.x > EPSILON) {
            minTime = Math.min(minTime, (courtWidth - r - pos.x) / vel.x);
        } else if (vel.x < -EPSILON) {
            minTime = Math.min(minTime, (r - pos.x) / vel.x);
        }

        if (vel.y > EPSILON) {
            minTime = Math.min(minTime, (courtHeight - r - pos.y) / vel.y);
        } else if (vel.y < -EPSILON) {
            minTime = Math.min(minTime, (r - pos.y) / vel.y);
        }

        return minTime > 0 ? minTime : Double.MAX_VALUE;
    }
}

3.4 主模拟器与可视化

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;

public class SquashSimulator extends JPanel implements ActionListener {
    private static final double WIDTH = 800;
    private static final double HEIGHT = 600;
    private static final double GRAVITY = 300.0;  // 像素/秒²
    private static final double DT = 0.016;       // 60 FPS
    private static final int TRAIL_LENGTH = 50;   // 轨迹长度

    private SquashBall ball;
    private CollisionEngine engine;
    private Timer timer;
    private List<Vector2D> trail; // 运动轨迹

    public SquashSimulator() {
        setPreferredSize(new Dimension((int)WIDTH, (int)HEIGHT));
        setBackground(new Color(30, 40, 50));

        // 初始化球:从中心以随机角度射出
        double angle = Math.toRadians(45);
        double speed = 400;
        ball = new SquashBall(
            WIDTH / 2, HEIGHT / 2,
            speed * Math.cos(angle), -speed * Math.sin(angle),
            15, 0.85  // 半径15,弹性系数0.85
        );

        engine = new CollisionEngine(WIDTH, HEIGHT);
        trail = new ArrayList<>();

        // 点击重置球的位置和速度
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                double dx = e.getX() - ball.getPosition().x;
                double dy = e.getY() - ball.getPosition().y;
                ball.setPosition(new Vector2D(e.getX(), e.getY()));
                ball.setVelocity(new Vector2D(dx * 2, dy * 2));
                trail.clear();
            }
        });

        timer = new Timer((int)(DT * 1000), this);
        timer.start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        updatePhysics();
        repaint();
    }

    private void updatePhysics() {
        // 连续碰撞检测:如果下一帧会穿透墙壁,提前处理碰撞
        double predictTime = engine.predictCollisionTime(ball);

        if (predictTime < DT) {
            // 先移动到碰撞点
            ball.updatePosition(predictTime);
            // 应用重力到碰撞点
            ball.applyGravity(GRAVITY, predictTime);
            // 处理碰撞
            engine.handleWallCollision(ball);
            // 继续剩余时间的运动
            double remaining = DT - predictTime;
            ball.applyGravity(GRAVITY, remaining);
            ball.updatePosition(remaining);
        } else {
            ball.applyGravity(GRAVITY, DT);
            ball.updatePosition(DT);
        }

        // 再次检测(处理数值误差)
        engine.handleWallCollision(ball);

        // 记录轨迹
        trail.add(new Vector2D(ball.getPosition().x, ball.getPosition().y));
        if (trail.size() > TRAIL_LENGTH) {
            trail.remove(0);
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                            RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制轨迹
        for (int i = 1; i < trail.size(); i++) {
            float alpha = (float) i / trail.size();
            g2d.setColor(new Color(0.5f, 0.8f, 1.0f, alpha * 0.5f));
            Vector2D p1 = trail.get(i - 1);
            Vector2D p2 = trail.get(i);
            g2d.drawLine((int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y);
        }

        // 绘制球
        Vector2D pos = ball.getPosition();
        double r = ball.getRadius();

        // 球体渐变效果
        RadialGradientPaint gradient = new RadialGradientPaint(
            (float)(pos.x - r * 0.3), (float)(pos.y - r * 0.3), (float)(r * 1.5),
            new float[]{0.0f, 1.0f},
            new Color[]{new Color(255, 200, 100), new Color(200, 100, 50)}
        );
        g2d.setPaint(gradient);
        g2d.fillOval((int)(pos.x - r), (int)(pos.y - r), (int)(r * 2), (int)(r * 2));

        // 绘制速度向量
        Vector2D vel = ball.getVelocity();
        g2d.setColor(Color.GREEN);
        g2d.drawLine((int)pos.x, (int)pos.y, 
                     (int)(pos.x + vel.x * 0.1), (int)(pos.y + vel.y * 0.1));

        // 绘制信息
        g2d.setColor(Color.WHITE);
        g2d.drawString(String.format("Speed: %.1f | Position: %s", 
                       vel.magnitude(), pos), 10, 20);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("壁球物理模拟 - Squash Physics Simulation");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new SquashSimulator());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

四、算法复杂度分析

操作 时间复杂度 空间复杂度 说明
单帧碰撞检测 O(1) O(1) 仅需检测4个边界
反射向量计算 O(1) O(1) 固定公式运算
连续碰撞检测 O(1) O(1) 计算到各边界的时间
轨迹渲染 O(T) O(T) T为轨迹长度(常数)

五、关键优化技巧

5.1 连续碰撞检测(CCD)

当球速极高时,单帧内可能穿透墙壁。连续碰撞检测通过预测球到达边界的精确时间,在穿透前提前处理碰撞,避免视觉上的”穿墙”现象。

5.2 数值稳定性处理

碰撞后将球移回场内并加上微小偏移量(EPSILON),防止浮点精度误差导致球卡在墙内反复碰撞。

5.3 弹性系数调节

弹性系数 e < 1 模拟能量损失,使球最终停下。在真实壁球中,球与墙壁碰撞会损失约10%-20%的动能。

六、扩展方向

  1. 球拍交互:加入玩家控制的球拍,实现AABB-圆形碰撞检测
  2. 旋转模拟:引入角速度和摩擦力,模拟球的旋转效应(马格努斯效应)
  3. 多人对战:网络同步多个球的位置状态
  4. AI对手:基于弹道预测算法实现自动接球

七、总结

壁球模拟的核心在于向量的反射运算和高效的碰撞检测。通过将物理问题转化为向量运算,我们不仅实现了一个流畅的模拟系统,也掌握了游戏物理引擎的基础原理。反射公式 R = I - 2(I·N)N 是计算机图形学和物理模拟中最常用的基础公式之一,值得深入理解。