每日算法 — 使用java实现射击游戏:弹道预测与贪心最优瞄准策略

游戏简介

射击游戏(Shooting Game)中,玩家控制角色或炮台发射 projectile 命中移动目标。与直接瞄准目标当前位置不同,有经验的玩家会预判目标未来的位置——这就是弹道预测。本文用Java实现射击游戏的核心瞄准算法,重点讲解基于运动学方程的弹道预测与贪心最优瞄准策略。

核心算法思路

运动学模型

假设目标以恒定速度做直线运动:
– 目标位置:P_t = P_0 + V × t
– 炮弹位置:P_b = B_0 + S × d × t

其中P_0为目标初始位置,V为目标速度向量,B_0为炮口位置,S为炮弹速度,d为发射方向的单位向量。

预测射击算法

要命中目标,需要找到发射方向d和飞行时间t,使得:
– P_0 + V × t = B_0 + S × d × t
– |d| = 1(单位向量)

通过向量运算可解出d。如果目标做匀速直线运动,这是一个确定性问题;如果目标速度变化或做曲线运动,则需要迭代逼近。

贪心最优瞄准

当存在多个目标时,采用贪心策略:
1. 计算命中每个目标所需的发射方向和时间
2. 选择”最容易命中”的目标(如飞行时间最短、角度偏差最小)
3. 如果多个目标在炮弹飞行路径上形成直线,尝试”一箭双雕”

Java实现

import java.util.*;

/**
 * 射击游戏弹道预测与最优瞄准
 */
public class ShootingGameAI {

    // 二维向量
    static 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 length() { return Math.sqrt(x * x + y * y); }
        Vec2 normalize() {
            double len = length();
            return len < 1e-9 ? new Vec2(0, 0) : new Vec2(x / len, y / len);
        }
        double dot(Vec2 v) { return x * v.x + y * v.y; }
    }

    // 移动目标
    static class Target {
        Vec2 position;
        Vec2 velocity;
        double radius; // 碰撞半径
        String name;

        Target(String name, Vec2 pos, Vec2 vel, double r) {
            this.name = name;
            this.position = pos;
            this.velocity = vel;
            this.radius = r;
        }

        Vec2 predictPosition(double t) {
            return position.add(velocity.mul(t));
        }
    }

    // 炮弹
    static class Projectile {
        Vec2 position;
        Vec2 direction;
        double speed;
        double radius;

        Projectile(Vec2 pos, Vec2 dir, double speed, double radius) {
            this.position = pos;
            this.direction = dir.normalize();
            this.speed = speed;
            this.radius = radius;
        }

        Vec2 positionAt(double t) {
            return position.add(direction.mul(speed * t));
        }
    }

    private Vec2 gunPosition;
    private double bulletSpeed;
    private double bulletRadius;
    private List<Target> targets;
    private Random rand = new Random(42);

    public ShootingGameAI(Vec2 gunPos, double bSpeed, double bRadius) {
        this.gunPosition = gunPos;
        this.bulletSpeed = bSpeed;
        this.bulletRadius = bRadius;
        this.targets = new ArrayList<>();
    }

    public void addTarget(Target t) {
        targets.add(t);
    }

    /**
     * 预测射击:计算命中匀速直线运动目标的发射方向
     * 解方程:|P0 + V*t - B0| = S*t
     * 即:| RelativePos + V*t |^2 = S^2 * t^2
     * @return 发射方向,null表示无法命中
     */
    public Vec2 predictAim(Target target) {
        Vec2 relativePos = target.position.sub(gunPosition);
        Vec2 v = target.velocity;
        double s = bulletSpeed;

        // 展开:|R + Vt|^2 = S^2 * t^2
        // (R.x + V.x*t)^2 + (R.y + V.y*t)^2 = S^2 * t^2
        // 整理为:at^2 + bt + c = 0
        double a = v.x * v.x + v.y * v.y - s * s;
        double b = 2 * (relativePos.x * v.x + relativePos.y * v.y);
        double c = relativePos.x * relativePos.x + relativePos.y * relativePos.y;

        // 考虑目标碰撞半径,放宽条件
        c -= target.radius * target.radius;

        double discriminant = b * b - 4 * a * c;

        if (discriminant < 0) {
            return null; // 无法命中
        }

        double sqrtDisc = Math.sqrt(discriminant);
        double t1 = (-b - sqrtDisc) / (2 * a);
        double t2 = (-b + sqrtDisc) / (2 * a);

        // 选择最小的正时间解
        double t = Double.MAX_VALUE;
        if (t1 > 0) t = t1;
        if (t2 > 0 && t2 < t) t = t2;

        if (t == Double.MAX_VALUE || t > 100) {
            return null; // 没有正解或解过大
        }

        // 计算命中位置,进而得到发射方向
        Vec2 hitPos = target.predictPosition(t);
        return hitPos.sub(gunPosition).normalize();
    }

    /**
     * 贪心最优目标选择
     * 综合考虑:命中概率、目标价值、飞行时间
     */
    public AimResult selectBestTarget() {
        AimResult best = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Target target : targets) {
            Vec2 aimDir = predictAim(target);
            if (aimDir == null) continue;

            // 计算预计飞行时间
            Vec2 relativePos = target.position.sub(gunPosition);
            double dist = relativePos.length();
            double flightTime = dist / bulletSpeed;

            // 评分:距离越近、目标越大,分数越高
            double score = 1000.0 / (flightTime + 1);
            score += target.radius * 10;

            // 如果目标正在远离,降低优先级
            Vec2 toTarget = relativePos.normalize();
            double approachSpeed = -target.velocity.dot(toTarget);
            score += approachSpeed * 5;

            if (score > bestScore) {
                bestScore = score;
                best = new AimResult(target, aimDir, flightTime);
            }
        }

        return best;
    }

    /**
     * 检测弹道上的多重目标(一箭双雕机会)
     */
    public List<Target> findMultiKillOpportunity(Vec2 direction) {
        List<Target> hitTargets = new ArrayList<>();

        for (Target t : targets) {
            // 计算目标到弹道直线的距离
            Vec2 toTarget = t.position.sub(gunPosition);
            Vec2 dir = direction.normalize();

            // 投影长度
            double proj = toTarget.dot(dir);
            if (proj < 0) continue; // 在后方

            // 垂直距离
            Vec2 projPoint = dir.mul(proj);
            Vec2 perp = toTarget.sub(projPoint);
            double perpDist = perp.length();

            if (perpDist < t.radius + bulletRadius + 5) {
                hitTargets.add(t);
            }
        }

        return hitTargets;
    }

    /**
     * 模拟发射并检测碰撞
     */
    public boolean simulateShot(Vec2 direction, double maxTime) {
        Projectile bullet = new Projectile(gunPosition, direction, bulletSpeed, bulletRadius);
        double timeStep = 0.05;

        for (double t = 0; t < maxTime; t += timeStep) {
            Vec2 bulletPos = bullet.positionAt(t);

            for (Target target : targets) {
                Vec2 targetPos = target.predictPosition(t);
                double dist = bulletPos.sub(targetPos).length();

                if (dist < bulletRadius + target.radius) {
                    System.out.printf("命中目标 [%s]!飞行时间:%.2f秒,距离:%.1f%n",
                        target.name, t, target.position.sub(gunPosition).length());
                    return true;
                }
            }
        }

        return false;
    }

    static class AimResult {
        Target target;
        Vec2 direction;
        double flightTime;

        AimResult(Target t, Vec2 d, double ft) {
            this.target = t;
            this.direction = d;
            this.flightTime = ft;
        }
    }

    public static void main(String[] args) {
        // 初始化:炮塔在原点,炮弹速度200
        ShootingGameAI ai = new ShootingGameAI(new Vec2(0, 0), 200, 2);

        // 添加多个移动目标
        ai.addTarget(new Target("敌机A", new Vec2(300, 100), new Vec2(-50, 20), 15));
        ai.addTarget(new Target("敌机B", new Vec2(200, -150), new Vec2(-30, -10), 20));
        ai.addTarget(new Target("敌机C", new Vec2(500, 200), new Vec2(-80, 0), 10));
        ai.addTarget(new Target("快速机", new Vec2(150, 50), new Vec2(-100, 50), 8));

        System.out.println("=== 射击游戏弹道预测模拟 ===\n");

        // 为每个目标计算预测瞄准
        for (Target t : ai.targets) {
            Vec2 aim = ai.predictAim(t);
            if (aim != null) {
                System.out.printf("目标 [%s]:预测方向 (%.3f, %.3f)%n",
                    t.name, aim.x, aim.y);
                ai.simulateShot(aim, 5.0);
            } else {
                System.out.printf("目标 [%s]:无法命中(速度过快或距离过远)%n", t.name);
            }
        }

        // 贪心最优选择
        System.out.println("\n=== 贪心最优目标选择 ===");
        AimResult best = ai.selectBestTarget();
        if (best != null) {
            System.out.printf("最优目标:[%s],预计飞行时间:%.2f秒%n",
                best.target.name, best.flightTime);
        }

        // 一箭双雕检测
        System.out.println("\n=== 多重目标机会检测 ===");
        Vec2 testDir = new Vec2(1, 0.3).normalize();
        List<Target> multi = ai.findMultiKillOpportunity(testDir);
        System.out.println("沿方向 (1, 0.3) 可命中目标数:" + multi.size());
        for (Target t : multi) {
            System.out.println("  - " + t.name);
        }
    }
}

进阶:考虑重力的抛物线弹道

对于迫击炮或抛射武器,弹道受重力影响呈抛物线:

/**
 * 抛物线弹道预测(考虑重力)
 * 解方程组:
 *   Px + Vx*t = Bx + Sx*t
 *   Py + Vy*t = By + Sy*t - 0.5*g*t^2
 * 其中S为初速度大小,需要求解发射角度
 */
public Vec2 predictParabolicAim(Target target, double gravity) {
    Vec2 rel = target.position.sub(gunPosition);
    double dx = rel.x;
    double dy = rel.y;
    double vx = target.velocity.x;
    double vy = target.velocity.y;
    double s = bulletSpeed;

    // 通过迭代法求解
    double bestError = Double.MAX_VALUE;
    Vec2 bestDir = null;

    for (double angle = -Math.PI/2; angle < Math.PI/2; angle += 0.001) {
        Vec2 dir = new Vec2(Math.cos(angle), Math.sin(angle));

        // 数值求解碰撞时间
        double t = solveCollisionTime(target, dir, gravity);
        if (t > 0 && t < 100) {
            Vec2 actual = target.predictPosition(t);
            Vec2 bulletPos = gunPosition.add(dir.mul(s * t));
            bulletPos.y -= 0.5 * gravity * t * t;
            double error = actual.sub(bulletPos).length();

            if (error < bestError) {
                bestError = error;
                bestDir = dir;
            }
        }
    }

    return bestError < 10 ? bestDir : null;
}

复杂度分析

算法 时间复杂度 说明
直线预测射击 O(1) 解二次方程
贪心目标选择 O(n) n为目标数
抛物线迭代 O(k) k为角度采样数
多重目标检测 O(n) 每条弹道检测

总结

本文通过Java实现了射击游戏的弹道预测与最优瞄准算法,核心要点包括:

  • 运动学方程建立目标与炮弹的位置模型
  • 解二次方程预测命中匀速直线运动目标的发射方向
  • 贪心策略综合考虑距离、目标大小、接近速度选择最优目标
  • 弹道直线检测实现”一箭双雕”的多重击杀机会识别
  • 抛物线弹道通过角度采样迭代逼近最优解

读者可以扩展:加入目标加速度模型(用三次方程求解)、实现提前量HUD显示、引入风力干扰的随机扰动模型。