每日算法 — 使用java实现打鸭子:目标追踪与概率射击优化

引言

打鸭子(Duck Hunt)是一款经典的射击游戏,玩家需要在鸭子飞出屏幕前将其击落。本文用Java实现带AI的自动射击系统,核心算法包括目标运动轨迹预测、概率射击模型与反应时间优化,让AI能够像高手玩家一样精准追踪并命中移动目标。

游戏机制与坐标系统

游戏场景设定为800×600像素的射击区域。鸭子从屏幕底部或两侧随机位置生成,以一定初速度和方向飞行。玩家(或AI)控制准星,在鸭子飞出屏幕或时间耗尽前完成射击。

核心算法设计

1. 目标运动轨迹预测

鸭子飞行可建模为匀速直线运动(忽略重力影响)。AI通过观察鸭子最近几帧的位置,用线性回归拟合运动轨迹,预测未来位置:

x(t) = x0 + vx * t
y(t) = y0 + vy * t

其中vx、vy通过最小二乘法从历史位置估计得到。

2. 概率射击模型

射击存在散布半径。AI计算准星与预测落点的距离,若距离小于命中半径则开火。同时引入置信度阈值,只有当预测置信度足够高时才射击,避免浪费弹药。

3. 反应时间优化

模拟人类玩家的反应延迟,AI设置最小反应间隔。但利用轨迹预测提前量,AI可以在鸭子到达预测位置前提前瞄准,补偿反应延迟。

Java完整实现

import java.util.*;

/**
 * 打鸭子AI射击系统
 * 实现目标追踪、轨迹预测与概率射击优化
 */
public class DuckHuntAI {
    // 游戏场景尺寸
    public static final int WIDTH = 800;
    public static final int HEIGHT = 600;
    // 射击参数
    public static final double HIT_RADIUS = 30.0;      // 命中半径
    public static final double AIM_SPEED = 8.0;        // 准星移动速度
    public static final int REACTION_DELAY = 5;        // 反应延迟帧数
    public static final double CONFIDENCE_THRESHOLD = 0.7; // 置信度阈值

    // AI状态
    private double aimX, aimY;          // 准星位置
    private List<Duck> ducks;           // 鸭子列表
    private int ammo = 10;              // 弹药量
    private int score = 0;              // 得分
    private Random random = new Random();

    public DuckHuntAI() {
        this.aimX = WIDTH / 2.0;
        this.aimY = HEIGHT / 2.0;
        this.ducks = new ArrayList<>();
    }

    /**
     * 鸭子类:记录位置、速度与历史轨迹
     */
    static class Duck {
        double x, y;                    // 当前位置
        double vx, vy;                  // 速度向量
        List<double[]> history;         // 历史位置 [(x,y), ...]
        boolean alive;                  // 是否存活
        int frameCount;                 // 已存活帧数

        public Duck(double x, double y, double vx, double vy) {
            this.x = x; this.y = y;
            this.vx = vx; this.vy = vy;
            this.history = new ArrayList<>();
            this.alive = true;
            this.frameCount = 0;
        }

        /**
         * 更新位置并记录历史
         */
        public void update() {
            history.add(new double[]{x, y});
            if (history.size() > 10) history.remove(0);
            x += vx;
            y += vy;
            frameCount++;
        }

        /**
         * 判断是否飞出屏幕
         */
        public boolean isOutOfBounds() {
            return x < -50 || x > WIDTH + 50 || y < -50 || y > HEIGHT + 50;
        }
    }

    /**
     * 每帧AI决策:选择目标、移动准星、判断是否射击
     */
    public void makeDecision() {
        // 生成新鸭子(模拟游戏逻辑)
        if (random.nextDouble() < 0.05 && ducks.size() < 3) {
            spawnDuck();
        }

        // 更新所有鸭子位置
        Iterator<Duck> it = ducks.iterator();
        while (it.hasNext()) {
            Duck d = it.next();
            d.update();
            if (!d.alive || d.isOutOfBounds()) {
                it.remove();
            }
        }

        // 选择最优目标
        Duck target = selectBestTarget();
        if (target == null) return;

        // 预测目标未来位置
        double[] predicted = predictPosition(target);
        double predX = predicted[0];
        double predY = predicted[1];
        double confidence = predicted[2];

        // 移动准星向预测位置
        moveAimTowards(predX, predY);

        // 判断是否满足射击条件
        if (shouldFire(target, predX, predY, confidence)) {
            fire(predX, predY);
        }
    }

    /**
     * 选择最优目标:优先选择存活帧数较长(轨迹数据充足)且距离准星较近的鸭子
     */
    private Duck selectBestTarget() {
        Duck best = null;
        double bestScore = Double.NEGATIVE_INFINITY;
        for (Duck d : ducks) {
            if (!d.alive) continue;
            // 轨迹数据至少需要3帧才能可靠预测
            if (d.history.size() < 3) continue;
            double dist = distance(aimX, aimY, d.x, d.y);
            // 得分 = 轨迹数据量 - 距离惩罚 - 飞出风险
            double timeToEscape = Math.min(
                    (d.vx > 0 ? WIDTH - d.x : d.x) / Math.abs(d.vx + 0.01),
                    (d.vy > 0 ? HEIGHT - d.y : d.y) / Math.abs(d.vy + 0.01));
            double score = d.history.size() * 10 - dist * 0.5 - timeToEscape * 2;
            if (score > bestScore) {
                bestScore = score;
                best = d;
            }
        }
        return best;
    }

    /**
     * 使用线性回归预测目标未来位置
     * 返回 [predictedX, predictedY, confidence]
     */
    private double[] predictPosition(Duck duck) {
        List<double[]> h = duck.history;
        int n = h.size();
        if (n < 2) {
            return new double[]{duck.x, duck.y, 0.3};
        }

        // 构建时间序列 t = 0, 1, 2, ...
        double sumT = 0, sumX = 0, sumY = 0;
        double sumTT = 0, sumTX = 0, sumTY = 0;
        for (int i = 0; i < n; i++) {
            double t = i;
            double x = h.get(i)[0];
            double y = h.get(i)[1];
            sumT += t; sumX += x; sumY += y;
            sumTT += t * t; sumTX += t * x; sumTY += t * y;
        }

        // 最小二乘法估计速度
        double denom = n * sumTT - sumT * sumT;
        double vx = (n * sumTX - sumT * sumX) / denom;
        double vy = (n * sumTY - sumT * sumY) / denom;

        // 计算拟合优度(R²近似)作为置信度
        double xMean = sumX / n, yMean = sumY / n;
        double ssResX = 0, ssTotX = 0;
        double ssResY = 0, ssTotY = 0;
        for (int i = 0; i < n; i++) {
            double t = i;
            double predX = sumX / n + vx * (t - sumT / n);
            double predY = sumY / n + vy * (t - sumT / n);
            ssResX += Math.pow(h.get(i)[0] - predX, 2);
            ssTotX += Math.pow(h.get(i)[0] - xMean, 2);
            ssResY += Math.pow(h.get(i)[1] - predY, 2);
            ssTotY += Math.pow(h.get(i)[1] - yMean, 2);
        }
        double r2 = 1 - (ssResX + ssResY) / (ssTotX + ssTotY + 0.001);
        double confidence = Math.max(0.1, Math.min(1.0, r2));

        // 预测REACTION_DELAY帧后的位置
        double futureX = duck.x + vx * REACTION_DELAY;
        double futureY = duck.y + vy * REACTION_DELAY;

        // 边界约束
        futureX = Math.max(0, Math.min(WIDTH, futureX));
        futureY = Math.max(0, Math.min(HEIGHT, futureY));

        return new double[]{futureX, futureY, confidence};
    }

    /**
     * 移动准星向目标位置,受限于最大移动速度
     */
    private void moveAimTowards(double targetX, double targetY) {
        double dx = targetX - aimX;
        double dy = targetY - aimY;
        double dist = Math.sqrt(dx * dx + dy * dy);
        if (dist <= AIM_SPEED) {
            aimX = targetX;
            aimY = targetY;
        } else {
            aimX += dx / dist * AIM_SPEED;
            aimY += dy / dist * AIM_SPEED;
        }
    }

    /**
     * 判断是否应射击
     */
    private boolean shouldFire(Duck duck, double predX, double predY, double confidence) {
        if (ammo <= 0) return false;
        double distToPred = distance(aimX, aimY, predX, predY);
        double distToActual = distance(aimX, aimY, duck.x, duck.y);
        // 条件:准星接近预测位置,且置信度足够,且鸭子实际位置也在射程内
        return distToPred < HIT_RADIUS * 0.5
                && confidence >= CONFIDENCE_THRESHOLD
                && distToActual < HIT_RADIUS * 2;
    }

    /**
     * 执行射击
     */
    private void fire(double targetX, double targetY) {
        ammo--;
        System.out.printf("射击!准星(%.1f, %.1f) -> 目标(%.1f, %.1f) 弹药剩余:%d%n",
                aimX, aimY, targetX, targetY, ammo);
        // 检查命中
        for (Duck d : ducks) {
            if (!d.alive) continue;
            if (distance(targetX, targetY, d.x, d.y) < HIT_RADIUS) {
                d.alive = false;
                score += 100;
                System.out.println("命中!得分: " + score);
                break;
            }
        }
    }

    private void spawnDuck() {
        double x = random.nextDouble() * WIDTH;
        double y = HEIGHT - 20;
        double vx = (random.nextDouble() - 0.5) * 6;
        double vy = -(2 + random.nextDouble() * 4);
        ducks.add(new Duck(x, y, vx, vy));
    }

    private double distance(double x1, double y1, double x2, double y2) {
        return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }

    public void printStatus() {
        System.out.printf("准星(%.1f, %.1f) 得分:%d 弹药:%d 场上鸭子:%d%n",
                aimX, aimY, score, ammo, ducks.size());
    }

    public static void main(String[] args) throws InterruptedException {
        DuckHuntAI ai = new DuckHuntAI();
        System.out.println("=== 打鸭子AI模拟开始 ===");
        for (int frame = 0; frame < 200; frame++) {
            ai.makeDecision();
            if (frame % 20 == 0) {
                ai.printStatus();
            }
            Thread.sleep(50); // 模拟帧率
        }
        System.out.println("=== 模拟结束 最终得分: " + ai.score + " ===");
    }
}

算法亮点解析

轨迹预测的鲁棒性

通过最小二乘线性回归估计速度向量,并用R²系数评估预测可靠性。只有当历史轨迹足够线性(高R²)时,AI才会依赖预测结果射击,避免在鸭子做机动飞行时浪费弹药。

目标优先级动态评估

选择目标时不仅考虑距离,还综合了轨迹数据量和飞出风险。对于即将飞出屏幕的鸭子给予更高优先级,确保不错失得分机会。

提前量补偿

预测位置考虑了AI的反应延迟(REACTION_DELAY帧),让准星提前到达鸭子将要出现的位置,而非追逐当前位置,大幅提升命中率。

复杂度分析

  • 时间复杂度:每帧O(D),D为场上鸭子数量。轨迹预测涉及固定长度(≤10)的线性回归,为O(1)。
  • 空间复杂度:O(D),存储每只鸭子的历史轨迹。

扩展方向

  1. 卡尔曼滤波:用卡尔曼滤波替代线性回归,处理带噪声的非匀速运动
  2. 多目标跟踪:引入匈牙利算法进行帧间目标关联,处理遮挡和交叉飞行
  3. 弹道模拟:考虑子弹飞行时间,计算更精确的前置量
  4. 机器学习:用神经网络直接从图像像素预测最佳射击时机

总结

打鸭子AI展示了运动目标追踪的经典方法。线性轨迹预测配合置信度评估,在简单场景下已能实现高命中率;而反应延迟补偿则让AI的行为更接近人类高手玩家的预判射击技巧。