一、壁球运动与算法概述
壁球(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%的动能。
六、扩展方向
- 球拍交互:加入玩家控制的球拍,实现AABB-圆形碰撞检测
- 旋转模拟:引入角速度和摩擦力,模拟球的旋转效应(马格努斯效应)
- 多人对战:网络同步多个球的位置状态
- AI对手:基于弹道预测算法实现自动接球
七、总结
壁球模拟的核心在于向量的反射运算和高效的碰撞检测。通过将物理问题转化为向量运算,我们不仅实现了一个流畅的模拟系统,也掌握了游戏物理引擎的基础原理。反射公式 R = I - 2(I·N)N 是计算机图形学和物理模拟中最常用的基础公式之一,值得深入理解。