每日算法 — 使用java实现保龄球:物理碰撞模拟与最优投球策略

保龄球是一款经典的体育竞技游戏,玩家通过控制投球的角度和力度,将排列成三角形的球瓶击倒。本文将用Java实现一个完整的保龄球游戏,重点讲解物理碰撞检测、反射向量计算、瓶倒连锁反应传播以及AI最优投球策略。

核心算法概览

保龄球游戏的核心算法包含四个关键部分:

  1. 物理运动模型:球在球道上的直线运动,包含初速度、摩擦力和重力加速度
  2. 碰撞检测:球与球瓶、球瓶与球瓶之间的圆形碰撞检测
  3. 反射向量计算:碰撞后的速度方向与大小变化,基于动量守恒和能量损失
  4. 最优投球策略:AI计算最佳投球角度和力度,最大化击倒球瓶数量

物理模型与运动轨迹

保龄球在球道上的运动可以简化为二维平面运动。球受到初始推力后,在摩擦力作用下逐渐减速。我们用向量来表示球的位置和速度。

/**
 * 二维向量类,用于表示位置和速度
 */
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 multiply(double scalar) {
        return new Vector2D(this.x * scalar, this.y * scalar);
    }

    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);
    }

    public double dot(Vector2D other) {
        return 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 class BowlingBall {
    private Vector2D position;    // 位置
    private Vector2D velocity;    // 速度
    private double radius;        // 球半径
    private double mass;          // 球质量
    private static final double FRICTION = 0.015; // 摩擦系数
    private static final double MIN_SPEED = 0.1;  // 最小速度阈值

    public BowlingBall(double x, double y, double radius, double mass) {
        this.position = new Vector2D(x, y);
        this.velocity = new Vector2D(0, 0);
        this.radius = radius;
        this.mass = mass;
    }

    /**
     * 设置初始投球速度
     * @param speed 速度大小
     * @param angle 角度(弧度)
     */
    public void throwBall(double speed, double angle) {
        this.velocity = new Vector2D(
            speed * Math.sin(angle),
            speed * Math.cos(angle)
        );
    }

    /**
     * 更新物理状态,应用摩擦力
     */
    public void update() {
        double speed = velocity.magnitude();
        if (speed < MIN_SPEED) {
            velocity = new Vector2D(0, 0);
            return;
        }

        // 摩擦力方向与速度方向相反
        Vector2D friction = velocity.normalize().multiply(-FRICTION);
        velocity = velocity.add(friction);

        // 更新位置
        position = position.add(velocity);
    }

    public Vector2D getPosition() { return position; }
    public Vector2D getVelocity() { return velocity; }
    public double getRadius() { return radius; }
    public double getMass() { return mass; }
    public boolean isMoving() { return velocity.magnitude() > MIN_SPEED; }
}

碰撞检测算法

保龄球和球瓶都近似为圆形,因此使用圆形碰撞检测最为高效。两个圆形物体发生碰撞的条件是:圆心距离小于两半径之和。

/**
 * 球瓶类
 */
public class Pin {
    private Vector2D position;
    private Vector2D velocity;
    private double radius;
    private double mass;
    private boolean isStanding;   // 是否站立
    private boolean isActive;     // 是否仍在参与碰撞
    private static final double PIN_RADIUS = 15.0;
    private static final double PIN_MASS = 1.0;
    private static final double FRICTION = 0.02;

    public Pin(double x, double y) {
        this.position = new Vector2D(x, y);
        this.velocity = new Vector2D(0, 0);
        this.radius = PIN_RADIUS;
        this.mass = PIN_MASS;
        this.isStanding = true;
        this.isActive = true;
    }

    /**
     * 检测与另一个物体的碰撞
     * @param otherPos 另一物体位置
     * @param otherRadius 另一物体半径
     * @return 是否发生碰撞
     */
    public boolean checkCollision(Vector2D otherPos, double otherRadius) {
        double distance = position.subtract(otherPos).magnitude();
        return distance < (this.radius + otherRadius);
    }

    /**
     * 获取碰撞深度(用于分离重叠物体)
     */
    public double getPenetrationDepth(Vector2D otherPos, double otherRadius) {
        double distance = position.subtract(otherPos).magnitude();
        return (this.radius + otherRadius) - distance;
    }

    public void update() {
        if (!isStanding || !isActive) return;

        double speed = velocity.magnitude();
        if (speed < 0.05) {
            velocity = new Vector2D(0, 0);
            return;
        }

        Vector2D friction = velocity.normalize().multiply(-FRICTION);
        velocity = velocity.add(friction);
        position = position.add(velocity);

        // 检测是否已倒下(速度方向偏离垂直轴超过45度或位移过大)
        if (speed > 0.5 && position.subtract(new Vector2D(position.x, position.y)).magnitude() > 5) {
            // 简化处理:移动距离过大则认为已倒下
        }
    }

    public void knockDown() { this.isStanding = false; }
    public boolean isStanding() { return isStanding; }
    public Vector2D getPosition() { return position; }
    public void setVelocity(Vector2D v) { this.velocity = v; }
    public Vector2D getVelocity() { return velocity; }
    public double getRadius() { return radius; }
    public double getMass() { return mass; }
}

反射向量计算

碰撞后的速度变化基于动量守恒和能量守恒定律。对于完全弹性碰撞,我们使用以下公式计算碰撞后的速度:

/**
 * 碰撞响应处理器
 */
public class CollisionResolver {
    private static final double RESTITUTION = 0.7; // 弹性系数(0-1,1为完全弹性)

    /**
     * 处理两个物体之间的碰撞
     * @param posA 物体A位置
     * @param velA 物体A速度
     * @param massA 物体A质量
     * @param posB 物体B位置
     * @param velB 物体B速度
     * @param massB 物体B质量
     * @return 包含碰撞后速度的数组 [newVelA, newVelB]
     */
    public static Vector2D[] resolveCollision(
            Vector2D posA, Vector2D velA, double massA,
            Vector2D posB, Vector2D velB, double massB) {

        // 碰撞法线方向(从A指向B)
        Vector2D normal = posB.subtract(posA).normalize();
        Vector2D tangent = new Vector2D(-normal.y, normal.x);

        // 将速度分解到法线和切线方向
        double vANormal = velA.dot(normal);
        double vATangent = velA.dot(tangent);
        double vBNormal = velB.dot(normal);
        double vBTangent = velB.dot(tangent);

        // 切线方向速度不变(忽略摩擦力)
        // 法线方向使用一维弹性碰撞公式
        double vANormalAfter = (vANormal * (massA - massB) + 2 * massB * vBNormal) 
                               / (massA + massB);
        double vBNormalAfter = (vBNormal * (massB - massA) + 2 * massA * vANormal) 
                               / (massA + massB);

        // 应用弹性系数模拟能量损失
        vANormalAfter *= RESTITUTION;
        vBNormalAfter *= RESTITUTION;

        // 重新组合速度向量
        Vector2D newVelA = normal.multiply(vANormalAfter).add(tangent.multiply(vATangent));
        Vector2D newVelB = normal.multiply(vBNormalAfter).add(tangent.multiply(vBTangent));

        return new Vector2D[]{newVelA, newVelB};
    }

    /**
     * 分离重叠的物体,防止穿透
     */
    public static void separateObjects(Vector2D posA, double radiusA, 
                                       Vector2D posB, double radiusB) {
        Vector2D diff = posB.subtract(posA);
        double distance = diff.magnitude();
        double overlap = (radiusA + radiusB) - distance;

        if (overlap > 0 && distance > 1e-10) {
            Vector2D separation = diff.normalize().multiply(overlap / 2);
            posA.x -= separation.x;
            posA.y -= separation.y;
            posB.x += separation.x;
            posB.y += separation.y;
        }
    }
}

瓶倒连锁反应

当球击倒一个球瓶后,被击倒的球瓶可能与其他球瓶发生碰撞,形成连锁反应。我们需要在每帧更新时检测所有活跃的碰撞对。

/**
 * 游戏主逻辑类
 */
public class BowlingGame {
    private BowlingBall ball;
    private List<Pin> pins;
    private static final double LANE_WIDTH = 120.0;
    private static final double LANE_LENGTH = 600.0;
    private static final double PIN_SPACING = 30.0;

    public BowlingGame() {
        this.ball = new BowlingBall(LANE_WIDTH / 2, 50, 20, 7);
        this.pins = initializePins();
    }

    /**
     * 初始化10个球瓶,排列成三角形
     * 第1行1个,第2行2个...第4行4个
     */
    private List<Pin> initializePins() {
        List<Pin> pinList = new ArrayList<>();
        double startX = LANE_WIDTH / 2;
        double startY = LANE_LENGTH - 150;

        for (int row = 0; row < 4; row++) {
            for (int col = 0; col <= row; col++) {
                double x = startX - (row * PIN_SPACING / 2) + col * PIN_SPACING;
                double y = startY - row * (PIN_SPACING * 0.866); // 0.866 = sqrt(3)/2
                pinList.add(new Pin(x, y));
            }
        }
        return pinList;
    }

    /**
     * 执行投球
     */
    public void throwBall(double speed, double angle) {
        ball.throwBall(speed, angle);
    }

    /**
     * 更新一帧游戏状态
     * @return 当前帧倒下的球瓶数量
     */
    public int updateFrame() {
        int knockedDownThisFrame = 0;

        // 更新球的位置
        ball.update();

        // 检测球与球瓶的碰撞
        for (Pin pin : pins) {
            if (!pin.isStanding()) continue;

            if (pin.checkCollision(ball.getPosition(), ball.getRadius())) {
                // 分离重叠物体
                CollisionResolver.separateObjects(
                    ball.getPosition(), ball.getRadius(),
                    pin.getPosition(), pin.getRadius()
                );

                // 计算碰撞后的速度
                Vector2D[] newVelocities = CollisionResolver.resolveCollision(
                    ball.getPosition(), ball.getVelocity(), ball.getMass(),
                    pin.getPosition(), pin.getVelocity(), pin.getMass()
                );

                ball.getVelocity().x = newVelocities[0].x;
                ball.getVelocity().y = newVelocities[0].y;
                pin.setVelocity(newVelocities[1]);

                // 球瓶受到足够大的冲击则倒下
                if (newVelocities[1].magnitude() > 1.5) {
                    pin.knockDown();
                    knockedDownThisFrame++;
                }
            }
        }

        // 检测球瓶之间的碰撞(连锁反应)
        for (int i = 0; i < pins.size(); i++) {
            Pin pinA = pins.get(i);
            if (!pinA.isStanding()) continue;

            for (int j = i + 1; j < pins.size(); j++) {
                Pin pinB = pins.get(j);
                if (!pinB.isStanding()) continue;

                if (pinA.checkCollision(pinB.getPosition(), pinB.getRadius())) {
                    CollisionResolver.separateObjects(
                        pinA.getPosition(), pinA.getRadius(),
                        pinB.getPosition(), pinB.getRadius()
                    );

                    Vector2D[] newVelocities = CollisionResolver.resolveCollision(
                        pinA.getPosition(), pinA.getVelocity(), pinA.getMass(),
                        pinB.getPosition(), pinB.getVelocity(), pinB.getMass()
                    );

                    pinA.setVelocity(newVelocities[0]);
                    pinB.setVelocity(newVelocities[1]);

                    // 检测是否倒下
                    if (newVelocities[0].magnitude() > 1.5) {
                        pinA.knockDown();
                        knockedDownThisFrame++;
                    }
                    if (newVelocities[1].magnitude() > 1.5) {
                        pinB.knockDown();
                        knockedDownThisFrame++;
                    }
                }
            }
        }

        // 更新所有球瓶状态
        for (Pin pin : pins) {
            pin.update();
        }

        return knockedDownThisFrame;
    }

    /**
     * 模拟直到球停止运动
     * @return 总共击倒的球瓶数
     */
    public int simulateThrow(double speed, double angle) {
        throwBall(speed, angle);
        int totalKnocked = 0;
        int maxFrames = 1000; // 防止无限循环
        int frame = 0;

        while ((ball.isMoving() || pins.stream().anyMatch(p -> p.getVelocity().magnitude() > 0.05)) 
               && frame < maxFrames) {
            totalKnocked += updateFrame();
            frame++;
        }

        return (int) pins.stream().filter(p -> !p.isStanding()).count();
    }

    public int getStandingPinCount() {
        return (int) pins.stream().filter(Pin::isStanding).count();
    }

    public void reset() {
        ball = new BowlingBall(LANE_WIDTH / 2, 50, 20, 7);
        pins = initializePins();
    }
}

AI最优投球策略

为了让AI能够自动投球,我们需要计算最佳的投球角度和力度。核心思路是:找到能够击倒最多球瓶的投球参数。

/**
 * AI投球策略计算器
 */
public class BowlingAI {
    private static final double MAX_SPEED = 25.0;
    private static final double MIN_SPEED = 10.0;
    private static final double SPEED_STEP = 1.0;
    private static final double ANGLE_RANGE = Math.toRadians(30); // ±30度
    private static final double ANGLE_STEP = Math.toRadians(2);

    /**
     * 搜索最优投球参数
     * 使用网格搜索遍历角度和力度的组合
     * @param game 当前游戏状态
     * @return 最优参数 [speed, angle]
     */
    public static double[] findBestThrow(BowlingGame game) {
        double bestSpeed = 0;
        double bestAngle = 0;
        int bestScore = -1;

        for (double speed = MIN_SPEED; speed <= MAX_SPEED; speed += SPEED_STEP) {
            for (double angle = -ANGLE_RANGE; angle <= ANGLE_RANGE; angle += ANGLE_STEP) {
                // 创建游戏副本进行模拟
                BowlingGame simulation = new BowlingGame();
                int knocked = simulation.simulateThrow(speed, angle);

                if (knocked > bestScore) {
                    bestScore = knocked;
                    bestSpeed = speed;
                    bestAngle = angle;
                }
            }
        }

        return new double[]{bestSpeed, bestAngle};
    }

    /**
     * 快速启发式策略:瞄准三角形重心
     * 计算球瓶排列的几何中心,直接瞄准该区域
     */
    public static double[] heuristicThrow(BowlingGame game) {
        // 标准三角形排列的重心大约在第2-3排之间
        double targetX = 60.0;  // 球道中心
        double targetY = 480.0; // 球瓶区域中心

        double startX = 60.0;
        double startY = 50.0;

        double dx = targetX - startX;
        double dy = targetY - startY;

        double angle = Math.atan2(dx, dy);
        double distance = Math.sqrt(dx * dx + dy * dy);

        // 根据距离计算合适的速度
        double speed = Math.min(MAX_SPEED, Math.max(MIN_SPEED, distance * 0.05));

        return new double[]{speed, angle};
    }

    /**
     * 高级策略:考虑剩余球瓶分布
     * 针对剩余球瓶的位置,计算最佳打击点
     */
    public static double[] adaptiveThrow(BowlingGame game, List<Pin> remainingPins) {
        if (remainingPins.isEmpty()) {
            return new double[]{15.0, 0};
        }

        // 计算剩余球瓶的质心
        double centerX = 0, centerY = 0;
        for (Pin pin : remainingPins) {
            centerX += pin.getPosition().x;
            centerY += pin.getPosition().y;
        }
        centerX /= remainingPins.size();
        centerY /= remainingPins.size();

        // 添加微调偏移,利用连锁反应
        // 如果球瓶集中在左侧,稍微偏左瞄准以产生更大散射
        double spreadX = 0;
        for (Pin pin : remainingPins) {
            spreadX += Math.abs(pin.getPosition().x - centerX);
        }
        spreadX /= remainingPins.size();

        double adjustment = spreadX > 15 ? Math.toRadians(3) : 0;

        double startX = 60.0;
        double startY = 50.0;
        double dx = centerX - startX;
        double dy = centerY - startY;
        double baseAngle = Math.atan2(dx, dy);
        double angle = baseAngle + adjustment;
        double distance = Math.sqrt(dx * dx + dy * dy);
        double speed = Math.min(MAX_SPEED, Math.max(MIN_SPEED, distance * 0.05));

        return new double[]{speed, angle};
    }
}

完整项目结构与演示

/**
 * 主程序入口
 */
public class BowlingGameDemo {
    public static void main(String[] args) {
        System.out.println("=== 保龄球游戏演示 ===\n");

        // 演示1:手动投球
        BowlingGame game1 = new BowlingGame();
        System.out.println("演示1:手动投球(速度20,角度0度)");
        int knocked1 = game1.simulateThrow(20.0, 0);
        System.out.println("击倒球瓶数:" + knocked1 + "/10\n");

        // 演示2:AI启发式策略
        BowlingGame game2 = new BowlingGame();
        System.out.println("演示2:AI启发式策略投球");
        double[] heuristic = BowlingAI.heuristicThrow(game2);
        int knocked2 = game2.simulateThrow(heuristic[0], heuristic[1]);
        System.out.println("投球参数:速度=" + String.format("%.2f", heuristic[0]) 
                          + ", 角度=" + String.format("%.2f", Math.toDegrees(heuristic[1])) + "度");
        System.out.println("击倒球瓶数:" + knocked2 + "/10\n");

        // 演示3:AI最优搜索策略(较慢,仅用于演示)
        BowlingGame game3 = new BowlingGame();
        System.out.println("演示3:AI最优搜索策略(网格搜索)");
        System.out.println("正在搜索最优参数...");
        long startTime = System.currentTimeMillis();
        double[] best = BowlingAI.findBestThrow(game3);
        long endTime = System.currentTimeMillis();
        int knocked3 = game3.simulateThrow(best[0], best[1]);
        System.out.println("搜索耗时:" + (endTime - startTime) + "ms");
        System.out.println("最优参数:速度=" + String.format("%.2f", best[0]) 
                          + ", 角度=" + String.format("%.2f", Math.toDegrees(best[1])) + "度");
        System.out.println("击倒球瓶数:" + knocked3 + "/10\n");

        System.out.println("=== 演示结束 ===");
    }
}

复杂度分析

模块 时间复杂度 空间复杂度 说明
碰撞检测 O(n) O(1) 每帧检测球与n个球瓶的碰撞
瓶间碰撞 O(n²) O(1) 双重循环检测球瓶对
AI网格搜索 O(k·m·n²) O(1) k为速度步数,m为角度步数,n²为模拟复杂度
启发式策略 O(n) O(1) 仅需计算质心

其中n为球瓶数量(固定为10),因此实际运行效率很高。AI网格搜索虽然复杂度较高,但由于搜索空间可控(速度步数约15,角度步数约30),在普通计算机上可在数秒内完成。

扩展方向

  1. 3D物理扩展:引入Z轴高度、球的旋转(hook球效果),使用更复杂的物理引擎
  2. 机器学习策略:用强化学习训练AI,让AI通过大量对局学习最优投球策略
  3. 多玩家对战:实现轮流投球机制,支持完整10轮计分规则
  4. 视觉渲染:集成JavaFX或LibGDX实现3D球道渲染

总结

本文实现了保龄球游戏的核心物理引擎,包括运动模型、圆形碰撞检测、反射向量计算和连锁反应模拟。通过三种AI策略(网格搜索、启发式、自适应)展示了如何让AI自动找到最优投球方案。该实现将经典体育游戏与物理算法结合,为理解碰撞检测和动量守恒提供了直观的代码示例。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注