每日算法 — 使用java实现弹珠:向量碰撞检测与反弹轨迹计算

弹珠游戏(Pinball/Marble Game)是街机厅的经典代表,其核心魅力在于逼真的物理碰撞和不可预测的弹道轨迹。本文将使用Java实现一个弹珠物理引擎,重点讲解二维向量运算、圆形碰撞检测以及弹性碰撞后的速度分解与反弹计算。

物理模型

弹珠游戏中的核心物理交互包括:

  1. 运动学更新:弹珠受重力加速,位置每帧更新
  2. 边界碰撞:弹珠与挡板、墙壁的碰撞检测
  3. 圆形碰撞:弹珠与圆形障碍物(缓冲器)的碰撞
  4. 弹性反弹:碰撞后的速度向量分解与反射

向量运算基础

所有物理计算基于二维向量:

位置: p = (x, y)
速度: v = (vx, vy)
加速度: a = (ax, ay) = (0, g)  // 仅重力

核心类实现

二维向量类

/**
 * 二维向量类
 * 封装所有向量运算,是物理引擎的数学基础
 */
public class Vector2D {

    public double x;
    public double y;

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

    /**
     * 向量加法
     */
    public Vector2D add(Vector2D other) {
        return new Vector2D(this.x + other.x, this.y + other.y);
    }

    /**
     * 向量减法
     */
    public Vector2D subtract(Vector2D other) {
        return new Vector2D(this.x - other.x, this.y - other.y);
    }

    /**
     * 数乘
     */
    public Vector2D multiply(double scalar) {
        return new Vector2D(this.x * scalar, this.y * scalar);
    }

    /**
     * 点积
     * a·b = |a||b|cos(θ),用于计算投影和夹角
     */
    public double dot(Vector2D other) {
        return this.x * other.x + this.y * other.y;
    }

    /**
     * 向量长度(模)
     */
    public double magnitude() {
        return Math.sqrt(x * x + y * y);
    }

    /**
     * 向量长度的平方(避免开方运算,用于比较)
     */
    public double magnitudeSquared() {
        return x * x + y * y;
    }

    /**
     * 单位向量(方向不变,长度为1)
     */
    public Vector2D normalize() {
        double mag = magnitude();
        if (mag < 1e-10) return new Vector2D(0, 0);
        return new Vector2D(x / mag, y / mag);
    }

    /**
     * 向量在另一向量上的投影
     * proj_b(a) = (a·b / |b|^2) * b
     */
    public Vector2D projectOnto(Vector2D other) {
        double dot = this.dot(other);
        double otherMagSq = other.magnitudeSquared();
        if (otherMagSq < 1e-10) return new Vector2D(0, 0);
        return other.multiply(dot / otherMagSq);
    }

    /**
     * 获取垂直向量(顺时针旋转90度)
     */
    public Vector2D perpendicular() {
        return new Vector2D(-y, x);
    }

    /**
     * 向量反射
     * r = d - 2(d·n)n,其中d为入射方向,n为法线单位向量
     */
    public Vector2D reflect(Vector2D normal) {
        Vector2D n = normal.normalize();
        double dot = this.dot(n);
        return this.subtract(n.multiply(2 * dot));
    }

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

弹珠实体

/**
 * 弹珠实体
 * 包含位置、速度、半径和质量
 */
public class Marble {

    private Vector2D position;
    private Vector2D velocity;
    private final double radius;
    private final double mass;
    private final double restitution;  // 弹性系数 (0-1)
    private final double friction;      // 摩擦系数

    public Marble(double x, double y, double radius) {
        this.position = new Vector2D(x, y);
        this.velocity = new Vector2D(0, 0);
        this.radius = radius;
        this.mass = radius * radius;  // 质量与面积成正比
        this.restitution = 0.85;      // 保留85%速度
        this.friction = 0.995;        // 每帧速度衰减
    }

    /**
     * 物理更新
     * @param gravity 重力加速度向量
     * @param dt 时间步长
     */
    public void update(Vector2D gravity, double dt) {
        // 应用重力
        velocity = velocity.add(gravity.multiply(dt));

        // 应用摩擦
        velocity = velocity.multiply(friction);

        // 更新位置: p = p + v * dt
        position = position.add(velocity.multiply(dt));
    }

    /**
     * 应用冲量改变速度
     */
    public void applyImpulse(Vector2D impulse) {
        // v = v + J / m
        velocity = velocity.add(impulse.multiply(1.0 / mass));
    }

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

    public void setPosition(Vector2D pos) { this.position = pos; }
    public void setVelocity(Vector2D vel) { this.velocity = vel; }

    @Override
    public String toString() {
        return String.format("Marble(pos=%s, vel=%s, r=%.1f)", position, velocity, radius);
    }
}

碰撞检测系统

圆形与圆形碰撞

/**
 * 碰撞检测工具类
 */
public class CollisionDetector {

    /**
     * 检测两个弹珠是否碰撞
     * 原理:两球心距离 < 半径之和
     * 
     * @return 碰撞信息,若未碰撞返回null
     */
    public static CollisionResult checkCircleCollision(Marble a, Marble b) {
        Vector2D diff = b.getPosition().subtract(a.getPosition());
        double distSq = diff.magnitudeSquared();
        double radiusSum = a.getRadius() + b.getRadius();

        if (distSq >= radiusSum * radiusSum) {
            return null;  // 未碰撞
        }

        double dist = Math.sqrt(distSq);
        Vector2D normal = dist > 1e-10 ? diff.multiply(1.0 / dist) : new Vector2D(1, 0);
        double penetration = radiusSum - dist;  // 穿透深度

        return new CollisionResult(normal, penetration);
    }

    /**
     * 检测弹珠与线段的碰撞
     * 用于弹珠与挡板/墙壁的碰撞
     * 
     * @param marble 弹珠
     * @param lineStart 线段起点
     * @param lineEnd 线段终点
     * @return 碰撞信息
     */
    public static CollisionResult checkLineCollision(Marble marble, 
                                                      Vector2D lineStart, 
                                                      Vector2D lineEnd) {
        Vector2D lineVec = lineEnd.subtract(lineStart);
        Vector2D toCircle = marble.getPosition().subtract(lineStart);

        // 计算圆心在线段上的投影点
        double lineLenSq = lineVec.magnitudeSquared();
        if (lineLenSq < 1e-10) {
            // 线段退化为点
            double dist = toCircle.magnitude();
            if (dist < marble.getRadius()) {
                Vector2D normal = toCircle.normalize();
                return new CollisionResult(normal, marble.getRadius() - dist);
            }
            return null;
        }

        // 投影参数 t = ((C - A) · (B - A)) / |B - A|^2
        double t = toCircle.dot(lineVec) / lineLenSq;
        t = Math.max(0, Math.min(1, t));  // 限制在线段范围内

        // 最近点 = A + t * (B - A)
        Vector2D closest = lineStart.add(lineVec.multiply(t));
        Vector2D diff = marble.getPosition().subtract(closest);
        double distSq = diff.magnitudeSquared();

        if (distSq >= marble.getRadius() * marble.getRadius()) {
            return null;
        }

        double dist = Math.sqrt(distSq);
        Vector2D normal = dist > 1e-10 ? diff.multiply(1.0 / dist) : lineVec.perpendicular().normalize();
        double penetration = marble.getRadius() - dist;

        return new CollisionResult(normal, penetration);
    }

    /**
     * 碰撞结果
     */
    public static class CollisionResult {
        public final Vector2D normal;      // 碰撞法线(指向弹珠外部)
        public final double penetration;    // 穿透深度

        public CollisionResult(Vector2D normal, double penetration) {
            this.normal = normal;
            this.penetration = penetration;
        }
    }
}

碰撞响应与反弹

弹性碰撞求解

/**
 * 碰撞响应处理器
 * 处理碰撞后的速度更新和位置修正
 */
public class CollisionResolver {

    /**
     * 处理两个弹珠之间的弹性碰撞
     * 基于动量守恒和能量守恒(考虑弹性系数)
     * 
     * 一维弹性碰撞公式:
     * v1' = (m1-m2)/(m1+m2)*v1 + 2*m2/(m1+m2)*v2
     * v2' = 2*m1/(m1+m2)*v1 + (m2-m1)/(m1+m2)*v2
     * 
     * 二维情况:将速度分解为法向和切向分量,仅法向分量交换
     */
    public static void resolveMarbleCollision(Marble a, Marble b, 
                                               CollisionDetector.CollisionResult collision) {
        Vector2D normal = collision.normal;

        // 1. 位置修正(防止重叠)
        double totalMass = a.getMass() + b.getMass();
        Vector2D correction = normal.multiply(collision.penetration);
        a.setPosition(a.getPosition().subtract(correction.multiply(b.getMass() / totalMass)));
        b.setPosition(b.getPosition().add(correction.multiply(a.getMass() / totalMass)));

        // 2. 相对速度
        Vector2D relativeVel = b.getVelocity().subtract(a.getVelocity());
        double velAlongNormal = relativeVel.dot(normal);

        // 如果物体正在分离,不处理
        if (velAlongNormal > 0) return;

        // 3. 计算冲量标量
        double e = Math.min(a.getRestitution(), b.getRestitution());
        double j = -(1 + e) * velAlongNormal;
        j /= (1.0 / a.getMass() + 1.0 / b.getMass());

        // 4. 应用冲量
        Vector2D impulse = normal.multiply(j);
        a.applyImpulse(impulse.multiply(-1));
        b.applyImpulse(impulse);
    }

    /**
     * 处理弹珠与静态边界的碰撞
     * 静态物体质量视为无穷大,仅弹珠速度改变
     */
    public static void resolveBoundaryCollision(Marble marble, 
                                                 CollisionDetector.CollisionResult collision) {
        // 1. 位置修正
        Vector2D correction = collision.normal.multiply(collision.penetration);
        marble.setPosition(marble.getPosition().add(correction));

        // 2. 速度反射
        Vector2D reflected = marble.getVelocity().reflect(collision.normal);
        marble.setVelocity(reflected.multiply(marble.getRestitution()));
    }

    /**
     * 处理弹珠与圆形缓冲器( bumper )的碰撞
     * 缓冲器通常带有额外的"推力"效果
     */
    public static void resolveBumperCollision(Marble marble, 
                                               Vector2D bumperCenter,
                                               double bumperForce,
                                               CollisionDetector.CollisionResult collision) {
        // 先进行标准边界碰撞处理
        resolveBoundaryCollision(marble, collision);

        // 额外施加缓冲器推力(沿法线方向加速)
        Vector2D extraForce = collision.normal.multiply(bumperForce / marble.getMass());
        marble.setVelocity(marble.getVelocity().add(extraForce));
    }
}

弹珠台场景

import java.util.*;

/**
 * 弹珠台游戏场景
 * 管理所有物理实体和碰撞检测
 */
public class PinballTable {

    // 物理常量
    private static final Vector2D GRAVITY = new Vector2D(0, 300.0);  // 向下重力
    private static final double TABLE_WIDTH = 400;
    private static final double TABLE_HEIGHT = 600;

    private final List<Marble> marbles;
    private final List<Wall> walls;
    private final List<Bumper> bumpers;

    public PinballTable() {
        this.marbles = new ArrayList<>();
        this.walls = new ArrayList<>();
        this.bumpers = new ArrayList<>();
        setupDefaultTable();
    }

    /**
     * 设置默认弹珠台布局
     */
    private void setupDefaultTable() {
        // 边界墙壁
        walls.add(new Wall(new Vector2D(0, 0), new Vector2D(TABLE_WIDTH, 0)));        // 上
        walls.add(new Wall(new Vector2D(0, TABLE_HEIGHT), new Vector2D(TABLE_WIDTH, TABLE_HEIGHT))); // 下
        walls.add(new Wall(new Vector2D(0, 0), new Vector2D(0, TABLE_HEIGHT)));        // 左
        walls.add(new Wall(new Vector2D(TABLE_WIDTH, 0), new Vector2D(TABLE_WIDTH, TABLE_HEIGHT)));  // 右

        // 三角形导向坡
        walls.add(new Wall(new Vector2D(0, 400), new Vector2D(80, 500)));
        walls.add(new Wall(new Vector2D(TABLE_WIDTH, 400), new Vector2D(TABLE_WIDTH - 80, 500)));

        // 圆形缓冲器
        bumpers.add(new Bumper(new Vector2D(150, 200), 25, 150));
        bumpers.add(new Bumper(new Vector2D(250, 200), 25, 150));
        bumpers.add(new Bumper(new Vector2D(200, 300), 20, 200));
    }

    /**
     * 发射新弹珠
     */
    public Marble launchMarble(double launchX) {
        Marble marble = new Marble(launchX, 50, 8);
        marble.setVelocity(new Vector2D(0, 100));  // 初始向下速度
        marbles.add(marble);
        return marble;
    }

    /**
     * 物理帧更新
     * @param dt 时间步长(秒)
     */
    public void update(double dt) {
        // 1. 更新所有弹珠位置
        for (Marble marble : marbles) {
            marble.update(GRAVITY, dt);
        }

        // 2. 弹珠间碰撞
        for (int i = 0; i < marbles.size(); i++) {
            for (int j = i + 1; j < marbles.size(); j++) {
                Marble a = marbles.get(i);
                Marble b = marbles.get(j);

                CollisionDetector.CollisionResult collision = 
                    CollisionDetector.checkCircleCollision(a, b);

                if (collision != null) {
                    CollisionResolver.resolveMarbleCollision(a, b, collision);
                }
            }
        }

        // 3. 弹珠与墙壁碰撞
        for (Marble marble : marbles) {
            for (Wall wall : walls) {
                CollisionDetector.CollisionResult collision = 
                    CollisionDetector.checkLineCollision(marble, wall.start, wall.end);

                if (collision != null) {
                    CollisionResolver.resolveBoundaryCollision(marble, collision);
                }
            }
        }

        // 4. 弹珠与缓冲器碰撞
        for (Marble marble : marbles) {
            for (Bumper bumper : bumpers) {
                // 缓冲器视为静态圆形
                double dist = marble.getPosition().subtract(bumper.center).magnitude();
                if (dist < marble.getRadius() + bumper.radius) {
                    Vector2D normal = marble.getPosition().subtract(bumper.center).normalize();
                    double penetration = marble.getRadius() + bumper.radius - dist;

                    CollisionDetector.CollisionResult collision = 
                        new CollisionDetector.CollisionResult(normal, penetration);

                    CollisionResolver.resolveBumperCollision(
                        marble, bumper.center, bumper.force, collision
                    );
                }
            }
        }

        // 5. 移除落出底边的弹珠
        marbles.removeIf(m -> m.getPosition().y > TABLE_HEIGHT + 20);
    }

    public List<Marble> getMarbles() { return marbles; }
    public List<Wall> getWalls() { return walls; }
    public List<Bumper> getBumpers() { return bumpers; }

    /**
     * 墙壁线段
     */
    public static class Wall {
        public final Vector2D start;
        public final Vector2D end;

        public Wall(Vector2D start, Vector2D end) {
            this.start = start;
            this.end = end;
        }
    }

    /**
     * 圆形缓冲器
     */
    public static class Bumper {
        public final Vector2D center;
        public final double radius;
        public final double force;  // 弹力加成

        public Bumper(Vector2D center, double radius, double force) {
            this.center = center;
            this.radius = radius;
            this.force = force;
        }
    }
}

控制台模拟演示

/**
 * 弹珠物理引擎控制台演示
 */
public class PinballDemo {

    public static void main(String[] args) {
        PinballTable table = new PinballTable();

        // 发射3个弹珠
        Marble m1 = table.launchMarble(100);
        m1.setVelocity(new Vector2D(50, 80));

        Marble m2 = table.launchMarble(200);
        m2.setVelocity(new Vector2D(-30, 120));

        Marble m3 = table.launchMarble(300);
        m3.setVelocity(new Vector2D(0, 60));

        System.out.println("===== 弹珠物理引擎模拟 =====\n");
        System.out.println("弹珠台: 400x600 | 重力: 300 | 弹性: 0.85 | 摩擦: 0.995\n");

        double dt = 0.016;  // 约60FPS
        int steps = 600;     // 模拟10秒
        int printInterval = 20;

        for (int i = 0; i < steps; i++) {
            table.update(dt);

            if (i % printInterval == 0) {
                double time = i * dt;
                System.out.printf("t=%4.2fs | 弹珠数: %d%n", time, table.getMarbles().size());

                for (int j = 0; j < table.getMarbles().size(); j++) {
                    Marble m = table.getMarbles().get(j);
                    System.out.printf("  [%d] pos=(%6.1f, %6.1f) vel=(%6.1f, %6.1f) speed=%5.1f%n",
                        j + 1, m.getPosition().x, m.getPosition().y,
                        m.getVelocity().x, m.getVelocity().y,
                        m.getVelocity().magnitude());
                }
                System.out.println();
            }

            if (table.getMarbles().isEmpty()) {
                System.out.println("所有弹珠已落出,模拟结束");
                break;
            }
        }

        System.out.println("模拟完成");
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
运动更新 O(m) O(1) m为弹珠数
弹珠间碰撞 O(m^2) O(1) 两两检测
边界碰撞 O(m * w) O(1) w为墙壁数
缓冲器碰撞 O(m * b) O(1) b为缓冲器数
碰撞响应 O(1) O(1) 每对碰撞常量计算

扩展方向

  1. 空间分区优化:使用四叉树或均匀网格减少碰撞检测复杂度从O(n^2)到O(n)
  2. 挡板交互:添加可控制的底部挡板,根据按键时机改变弹珠反弹角度
  3. 旋转物体:处理弹珠与旋转圆形物体的碰撞(角动量守恒)
  4. 多边形碰撞:扩展至任意多边形障碍物的碰撞检测(Separating Axis Theorem)

总结

本文从向量运算出发,构建了完整的弹珠物理引擎,涵盖圆形碰撞检测、弹性碰撞响应和场景管理。通过将物理问题转化为向量计算,读者可以直观地理解碰撞背后的数学原理,并将这些技术应用于更复杂的游戏物理系统开发中。