每日算法 — 使用java实现愤怒的小鸟:抛物线物理模拟与蒙特卡洛射击优化

愤怒的小鸟(Angry Birds)是全球现象级的物理弹射游戏,玩家拖动弹弓发射小鸟,利用抛物线轨迹撞击猪与木结构,以消灭所有敌方目标。其内核并非简单的动画播放,而是刚体物理引擎碰撞检测蒙特卡洛优化搜索的有机结合。本文将用Java从零构建一个可运行的简化版物理引擎,并讲解如何用蒙特卡洛模拟寻找最优发射角度与初速度。

一、场景建模与物理实体定义

1.1 为什么需要统一实体基类

游戏世界中的小鸟、猪、木块都具备位置、速度、质量、尺寸等属性,且参与相同的物理更新与碰撞检测流程。通过抽象基类统一接口,可大幅简化碰撞遍历与受力计算。

/**
 * 二维向量,用于位置、速度、加速度表示
 * 支持加减、数乘、点乘与模长计算
 */
class Vec2 {
    double x, y;

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

    Vec2 add(Vec2 v)   { return new Vec2(x + v.x, y + v.y); }
    Vec2 sub(Vec2 v)   { return new Vec2(x - v.x, y - v.y); }
    Vec2 mul(double s) { return new Vec2(x * s, y * s); }
    double dot(Vec2 v) { return x * v.x + y * v.y; }
    double length()    { return Math.sqrt(x * x + y * y); }

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

/**
 * 物理实体基类
 * 所有参与碰撞与运动的游戏对象(鸟、猪、方块)都继承此类
 */
abstract class Entity {
    // 中心位置(单位:米)
    Vec2 position;
    // 速度向量(单位:米/秒)
    Vec2 velocity;
    // 加速度(通常仅受重力影响)
    Vec2 acceleration;
    // 质量(千克),质量越大惯性越大
    double mass;
    // 尺寸:半宽与半高(用于AABB碰撞盒)
    double halfWidth, halfHeight;
    // 是否已被销毁(生命值归零)
    boolean destroyed;
    // 实体类型标识
    enum Type { BIRD, PIG, WOOD_BLOCK }
    final Type type;

    Entity(Type type, Vec2 pos, Vec2 vel, double mass, double hw, double hh) {
        this.type = type;
        this.position = pos;
        this.velocity = vel;
        this.acceleration = new Vec2(0, 0);
        this.mass = mass;
        this.halfWidth = hw;
        this.halfHeight = hh;
        this.destroyed = false;
    }

    /**
     * 获取轴对齐包围盒(AABB)的上下左右边界
     * AABB是碰撞检测中最快的粗判手段
     */
    double minX() { return position.x - halfWidth; }
    double maxX() { return position.x + halfWidth; }
    double minY() { return position.y - halfHeight; }
    double maxY() { return position.y + halfHeight; }

    /**
     * 承受冲击并计算伤害
     * @param impulse 冲击力大小
     */
    abstract void takeDamage(double impulse);
}

1.2 具体实体实现

/**
 * 小鸟实体
 * 小鸟的特点:质量轻、可被玩家发射、撞击后造成动能伤害
 */
class Bird extends Entity {
    // 生命值,小鸟撞击坚硬物体后也会消失
    double hp;

    Bird(Vec2 pos, double mass, double radius) {
        super(Type.BIRD, pos, new Vec2(0, 0), mass, radius, radius);
        this.hp = 10.0;
    }

    @Override
    void takeDamage(double impulse) {
        // 小鸟承受撞击时,按冲击力扣血
        hp -= impulse * 0.5;
        if (hp <= 0) destroyed = true;
    }

    /**
     * 从弹弓发射
     * @param angle 发射角度(弧度,0表示水平向右,正方向向上)
     * @param speed 初速度大小
     */
    void launch(double angle, double speed) {
        this.velocity = new Vec2(Math.cos(angle) * speed, Math.sin(angle) * speed);
    }
}

/**
 * 猪实体
 * 猪是目标单位,被击倒即视为消灭
 */
class Pig extends Entity {
    double hp;

    Pig(Vec2 pos, double mass, double radius) {
        super(Type.PIG, pos, new Vec2(0, 0), mass, radius, radius);
        this.hp = 15.0;
    }

    @Override
    void takeDamage(double impulse) {
        hp -= impulse;
        if (hp <= 0) destroyed = true;
    }
}

/**
 * 木块障碍物
 * 木块用于搭建防御工事,受撞击后可能碎裂
 */
class WoodBlock extends Entity {
    double hp;
    // 木块材质强度系数
    static final double STRENGTH = 2.0;

    WoodBlock(Vec2 pos, double mass, double hw, double hh) {
        super(Type.WOOD_BLOCK, pos, new Vec2(0, 0), mass, hw, hh);
        this.hp = 30.0;
    }

    @Override
    void takeDamage(double impulse) {
        hp -= impulse / STRENGTH;
        if (hp <= 0) destroyed = true;
    }
}

二、抛物线运动模拟

2.1 物理更新核心

游戏采用显式欧拉积分进行离散时间步进。每帧执行:

  • 速度更新:v = v + a * dt
  • 位置更新:p = p + v * dt

重力加速度固定为 g = 9.8 m/s²,方向竖直向下。为简化计算,世界坐标系以弹弓位置为原点,X轴向右,Y轴向上。

/**
 * 物理世界管理器
 * 负责所有实体的重力施加、运动积分、碰撞检测与响应
 */
class PhysicsWorld {
    // 重力加速度(向下为负Y方向)
    static final Vec2 GRAVITY = new Vec2(0, -9.8);
    // 时间步长(秒),越小模拟越精确但计算量越大
    static final double DT = 0.016; // 约60FPS
    // 地面高度,实体落到地面以下即视为出界
    static final double GROUND_Y = 0.0;

    private final List<Entity> entities;

    PhysicsWorld() {
        this.entities = new ArrayList<>();
    }

    void addEntity(Entity e) {
        entities.add(e);
    }

    List<Entity> getEntities() {
        return entities;
    }

    /**
     * 单步物理模拟
     * 1. 对所有存活实体施加重力并更新速度与位置
     * 2. 执行碰撞检测与响应
     * 3. 移除已被销毁的实体
     */
    void step() {
        // 阶段一:积分运动
        for (Entity e : entities) {
            if (e.destroyed) continue;
            // 施加重力(仅对空中的鸟和飞起的碎片有效,猪和木块默认静止)
            if (e.type == Entity.Type.BIRD || e.velocity.length() > 0.1) {
                e.acceleration = GRAVITY;
                e.velocity = e.velocity.add(GRAVITY.mul(DT));
                e.position = e.position.add(e.velocity.mul(DT));
            }
            // 地面碰撞简单处理:落地点以下停止
            if (e.position.y - e.halfHeight < GROUND_Y) {
                e.position.y = GROUND_Y + e.halfHeight;
                e.velocity.y = Math.max(e.velocity.y, 0);
                // 地面摩擦减速
                e.velocity.x *= 0.9;
            }
        }

        // 阶段二:碰撞检测与响应(O(n^2) 暴力遍历,小规模场景足够)
        for (int i = 0; i < entities.size(); i++) {
            for (int j = i + 1; j < entities.size(); j++) {
                Entity a = entities.get(i);
                Entity b = entities.get(j);
                if (a.destroyed || b.destroyed) continue;
                resolveCollision(a, b);
            }
        }

        // 阶段三:清理已销毁实体
        entities.removeIf(e -> e.destroyed);
    }

2.2 碰撞响应模型

当两个实体发生AABB重叠时,我们采用简化冲量模型:根据相对速度的法向分量计算冲击力,并按质量比例分配速度变化。冲击力同时作为伤害输入。

    /**
     * AABB碰撞检测 + 冲量响应
     * @return 若发生碰撞返回true
     */
    boolean resolveCollision(Entity a, Entity b) {
        // AABB粗判:检查X轴与Y轴投影是否均重叠
        boolean overlapX = a.maxX() > b.minX() && a.minX() < b.maxX();
        boolean overlapY = a.maxY() > b.minY() && a.minY() < b.maxY();

        if (!overlapX || !overlapY) return false;

        // 计算碰撞法线(从a指向b的最短分离方向)
        Vec2 normal;
        double overlapLeft   = a.maxX() - b.minX();
        double overlapRight  = b.maxX() - a.minX();
        double overlapTop    = a.maxY() - b.minY();
        double overlapBottom = b.maxY() - a.minY();

        double minOverlap = Math.min(Math.min(overlapLeft, overlapRight),
                                     Math.min(overlapTop, overlapBottom));

        if (minOverlap == overlapLeft)       normal = new Vec2(-1, 0);
        else if (minOverlap == overlapRight) normal = new Vec2(1, 0);
        else if (minOverlap == overlapTop)   normal = new Vec2(0, 1);
        else                                 normal = new Vec2(0, -1);

        // 相对速度
        Vec2 relVel = a.velocity.sub(b.velocity);
        double velAlongNormal = relVel.dot(normal);

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

        // 弹性系数(0为完全非弹性,1为完全弹性)
        double restitution = 0.4;
        double impulseScalar = -(1 + restitution) * velAlongNormal;
        impulseScalar /= (1.0 / a.mass + 1.0 / b.mass);

        Vec2 impulse = normal.mul(impulseScalar);
        a.velocity = a.velocity.add(impulse.mul(1.0 / a.mass));
        b.velocity = b.velocity.sub(impulse.mul(1.0 / b.mass));

        // 位置分离,防止粘连
        double percent = 0.8; //  penetration percentage to correct
        Vec2 correction = normal.mul(minOverlap * percent);
        a.position = a.position.sub(correction.mul(1.0 / a.mass));
        b.position = b.position.add(correction.mul(1.0 / b.mass));

        // 计算冲击力并施加伤害
        double impact = Math.abs(impulseScalar);
        a.takeDamage(impact);
        b.takeDamage(impact);

        return true;
    }
}

三、蒙特卡洛射击优化

3.1 问题建模

玩家的决策空间是一个二维连续空间:(发射角度 θ,初速度 v)。直接遍历所有组合代价极高。蒙特卡洛方法通过随机采样大量 (θ, v) 组合,模拟飞行轨迹,统计摧毁猪的数量与剩余结构稳定度,从而逼近最优解。

/**
 * 射击方案
 */
class Shot {
    double angle;   // 弧度
    double speed;   // 米/秒
    double score;   // 模拟得分

    Shot(double angle, double speed) {
        this.angle = angle;
        this.speed = speed;
        this.score = -1;
    }
}

/**
 * 蒙特卡洛射击优化器
 * 在有限的模拟步数内,通过随机采样寻找最佳发射参数
 */
class MonteCarloOptimizer {
    // 角度搜索范围:0 ~ 80度(避免朝地面发射)
    static final double ANGLE_MIN = Math.toRadians(5);
    static final double ANGLE_MAX = Math.toRadians(80);
    // 速度搜索范围
    static final double SPEED_MIN = 5.0;
    static final double SPEED_MAX = 25.0;
    // 每次评估的最大模拟时长(秒)
    static final double MAX_SIM_TIME = 5.0;
    // 随机数生成器
    private final Random random;

    MonteCarloOptimizer(long seed) {
        this.random = new Random(seed);
    }

    /**
     * 寻找最优射击方案
     * @param worldTemplate 初始世界状态(将被复制用于每次模拟)
     * @param iterations 蒙特卡洛采样次数
     * @return 最优射击方案
     */
    Shot findBestShot(WorldSnapshot worldTemplate, int iterations) {
        Shot bestShot = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (int i = 0; i < iterations; i++) {
            double angle = ANGLE_MIN + random.nextDouble() * (ANGLE_MAX - ANGLE_MIN);
            double speed = SPEED_MIN + random.nextDouble() * (SPEED_MAX - SPEED_MIN);
            Shot shot = new Shot(angle, speed);

            // 复制世界进行独立模拟,避免污染原状态
            PhysicsWorld simWorld = worldTemplate.cloneWorld();
            double score = evaluateShot(simWorld, shot);
            shot.score = score;

            if (score > bestScore) {
                bestScore = score;
                bestShot = shot;
            }
        }
        return bestShot;
    }

3.2 评分函数设计

评分是蒙特卡洛的核心。好的评分函数应同时激励:消灭更多猪、用更少鸟、造成更大结构破坏、更快完成。

    /**
     * 评估一次射击的得分
     * 评分维度:
     * 1. 消灭猪的数量(权重最高)
     * 2. 造成的总伤害(结构破坏度)
     * 3. 飞行效率(水平位移奖励)
     * 4. 时间惩罚(飞太久说明轨迹低效)
     */
    double evaluateShot(PhysicsWorld world, Shot shot) {
        // 找到待发射的小鸟(假设第一个未被销毁的鸟是当前鸟)
        Bird bird = null;
        for (Entity e : world.getEntities()) {
            if (e.type == Entity.Type.BIRD && !e.destroyed) {
                bird = (Bird) e;
                break;
            }
        }
        if (bird == null) return Double.NEGATIVE_INFINITY;

        // 执行发射
        bird.launch(shot.angle, shot.speed);

        int initialPigs = countPigs(world);
        double totalDamage = 0;
        double maxX = bird.position.x;
        int steps = 0;
        int maxSteps = (int) (MAX_SIM_TIME / PhysicsWorld.DT);

        // 物理模拟直到超时或鸟停止
        for (; steps < maxSteps; steps++) {
            world.step();
            if (bird.destroyed) break;
            maxX = Math.max(maxX, bird.position.x);
            // 累计所有实体受到的伤害(通过HP下降近似)
            totalDamage += collectDamage(world);
        }

        int remainingPigs = countPigs(world);
        int pigsKilled = initialPigs - remainingPigs;

        // 综合评分
        double score = pigsKilled * 1000.0      // 杀猪奖励
                     + totalDamage * 10.0       // 破坏奖励
                     + maxX * 2.0               // 射程奖励
                     - steps * 0.1;             // 时间惩罚
        return score;
    }

    private int countPigs(PhysicsWorld world) {
        int count = 0;
        for (Entity e : world.getEntities()) {
            if (e.type == Entity.Type.PIG && !e.destroyed) count++;
        }
        return count;
    }

    private double collectDamage(PhysicsWorld world) {
        // 简化:统计当前世界中被销毁的非鸟实体数量作为破坏指标
        double dmg = 0;
        for (Entity e : world.getEntities()) {
            if (e.type != Entity.Type.BIRD && e.destroyed) dmg += e.mass;
        }
        return dmg;
    }
}

四、世界快照与克隆

为了让蒙特卡洛优化器能在不污染真实游戏状态的前提下反复试验,需要实现世界的深拷贝。

/**
 * 世界快照
 * 保存某一时刻的完整实体状态,支持克隆出独立可运行的物理世界
 */
class WorldSnapshot {
    private final List<Entity> entities;

    WorldSnapshot(List<Entity> entities) {
        this.entities = new ArrayList<>();
        for (Entity e : entities) {
            this.entities.add(cloneEntity(e));
        }
    }

    private Entity cloneEntity(Entity e) {
        Vec2 pos = new Vec2(e.position.x, e.position.y);
        Vec2 vel = new Vec2(e.velocity.x, e.velocity.y);
        switch (e.type) {
            case BIRD:
                Bird b = new Bird(pos, e.mass, e.halfWidth);
                b.velocity = vel;
                b.hp = ((Bird) e).hp;
                return b;
            case PIG:
                Pig p = new Pig(pos, e.mass, e.halfWidth);
                p.velocity = vel;
                p.hp = ((Pig) e).hp;
                return p;
            case WOOD_BLOCK:
                WoodBlock w = new WoodBlock(pos, e.mass, e.halfWidth, e.halfHeight);
                w.velocity = vel;
                w.hp = ((WoodBlock) e).hp;
                return w;
            default:
                throw new IllegalStateException("Unknown entity type");
        }
    }

    PhysicsWorld cloneWorld() {
        PhysicsWorld w = new PhysicsWorld();
        for (Entity e : entities) {
            w.addEntity(cloneEntity(e));
        }
        return w;
    }
}

五、完整主程序与场景测试

import java.util.*;

/**
 * 愤怒的小鸟简化版主程序
 * 构建一个经典场景:弹弓在左侧,右侧堆叠木块与猪
 */
public class AngryBirdsAI {

    public static void main(String[] args) {
        // 构建初始世界
        PhysicsWorld world = new PhysicsWorld();

        // 弹弓位置作为参考原点 (0, 2)
        Vec2 slingPos = new Vec2(0, 2.0);

        // 放置待发射的小鸟
        Bird redBird = new Bird(new Vec2(slingPos.x, slingPos.y), 1.0, 0.3);
        world.addEntity(redBird);

        // 搭建敌方阵地:三层木块 + 中间夹猪
        // 底层平台
        world.addEntity(new WoodBlock(new Vec2(8.0, 0.5), 5.0, 1.5, 0.5));
        world.addEntity(new WoodBlock(new Vec2(11.0, 0.5), 5.0, 1.5, 0.5));

        // 中层立柱
        world.addEntity(new WoodBlock(new Vec2(8.0, 1.5), 2.0, 0.3, 0.5));
        world.addEntity(new WoodBlock(new Vec2(11.0, 1.5), 2.0, 0.3, 0.5));

        // 中层平台
        world.addEntity(new WoodBlock(new Vec2(9.5, 2.0), 5.0, 2.0, 0.3));

        // 放置猪
        world.addEntity(new Pig(new Vec2(9.5, 2.6), 2.0, 0.4));
        world.addEntity(new Pig(new Vec2(8.0, 1.1), 2.0, 0.4));

        // 顶层装饰块
        world.addEntity(new WoodBlock(new Vec2(9.5, 2.8), 1.5, 0.4, 0.3));

        System.out.println("=== 初始场景 ===");
        printWorld(world);

        // 创建世界快照
        WorldSnapshot snapshot = new WorldSnapshot(world.getEntities());

        // 蒙特卡洛优化:采样5000次寻找最优射击参数
        System.out.println("\n=== 开始蒙特卡洛优化(5000次采样)===");
        MonteCarloOptimizer optimizer = new MonteCarloOptimizer(System.currentTimeMillis());
        Shot best = optimizer.findBestShot(snapshot, 5000);

        System.out.printf("最优方案:角度=%.2f度, 速度=%.2f m/s, 预估得分=%.2f%n",
                Math.toDegrees(best.angle), best.speed, best.score);

        // 在真实世界中执行最优射击
        System.out.println("\n=== 执行最优射击 ===");
        redBird.launch(best.angle, best.speed);

        // 模拟5秒
        int totalSteps = (int) (5.0 / PhysicsWorld.DT);
        for (int i = 0; i < totalSteps; i++) {
            world.step();
            if (redBird.destroyed) {
                System.out.println("小鸟已撞击销毁,模拟结束于第 " + i + " 步");
                break;
            }
        }

        System.out.println("\n=== 最终场景 ===");
        printWorld(world);
    }

    static void printWorld(PhysicsWorld world) {
        for (Entity e : world.getEntities()) {
            String status = e.destroyed ? "[已销毁]" : "";
            System.out.printf("%s %s pos=%s vel=%s %s%n",
                    e.type, status, e.position, e.velocity,
                    e.type == Entity.Type.PIG ? "HP=" + ((Pig)e).hp :
                    e.type == Entity.Type.BIRD ? "HP=" + ((Bird)e).hp :
                    e.type == Entity.Type.WOOD_BLOCK ? "HP=" + ((WoodBlock)e).hp : "");
        }
    }
}

六、复杂度分析与总结

模块 时间复杂度 空间复杂度 说明
物理单步积分 O(n) O(n) n为实体数量,每实体一次向量更新
碰撞检测 O(n²) O(1) 暴力遍历所有实体对,适合小规模场景
蒙特卡洛采样 O(k × n² × t) O(n) k为采样次数,t为每轮最大步数

本文实现了一个可独立运行的愤怒的小鸟简化物理引擎,核心包含三大算法模块:

  • 抛物线物理模拟:基于显式欧拉积分,精确追踪小鸟在重力场中的飞行轨迹。
  • AABB碰撞检测:以轴对齐包围盒实现快速碰撞粗判,配合冲量模型完成反弹与伤害计算。
  • 蒙特卡洛射击优化:在角度-速度二维参数空间中随机采样,通过评分函数自动搜索最优发射策略。

读者可在此基础上继续扩展:引入更精确的圆形/多边形碰撞、添加旋转与角动量、用遗传算法或模拟退火替代纯随机采样、甚至训练神经网络进行端到端射击决策。

发表回复

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