引言
打鸭子(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),存储每只鸭子的历史轨迹。
扩展方向
- 卡尔曼滤波:用卡尔曼滤波替代线性回归,处理带噪声的非匀速运动
- 多目标跟踪:引入匈牙利算法进行帧间目标关联,处理遮挡和交叉飞行
- 弹道模拟:考虑子弹飞行时间,计算更精确的前置量
- 机器学习:用神经网络直接从图像像素预测最佳射击时机
总结
打鸭子AI展示了运动目标追踪的经典方法。线性轨迹预测配合置信度评估,在简单场景下已能实现高命中率;而反应延迟补偿则让AI的行为更接近人类高手玩家的预判射击技巧。