射击游戏中的AI需要精确判断何时开火、朝哪里瞄准。核心挑战在于目标在移动,子弹有飞行时间,直接瞄准当前位置会错失目标。本文用Java实现射击AI系统,涵盖射线投射碰撞检测、抛物线弹道预测与提前量计算算法。
算法核心思路
1. 射线投射(Ray Casting)
从枪口发射一条射线,检测与场景中障碍物或目标的交点。通过参数方程表示射线:P = O + t·D,其中O为起点,D为单位方向向量,t≥0。与线段求交转化为解线性方程组,判断t是否在有效范围内。
2. 提前量计算(Lead Target)
假设目标匀速直线运动,设子弹速度为v_b,目标速度为v_t,相对位置为r。需要求解子弹飞行时间t,使得子弹在时间t后的位置与目标位置重合。通过迭代法或解析法求解二次方程得到t,进而计算瞄准点。
3. 抛物线弹道预测
考虑重力影响时,子弹轨迹为抛物线。给定发射角度和初速度,通过运动学方程预测落点。反向问题——给定目标位置求发射角度——通过求解抛物线方程的根得到可行发射角。
4. 可见性检测
在瞄准前需确认目标是否被障碍物遮挡。对场景中所有障碍物进行射线投射测试,若最近交点距离小于目标距离,则目标不可见,AI不应开火。
Java实现
import java.util.ArrayList;
import java.util.List;
public class ShootingAI {
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 dot(Vec2 v) { return x * v.x + y * v.y; }
double len() { return Math.hypot(x, y); }
Vec2 normalize() { double l = len(); return l == 0 ? new Vec2(0,0) : new Vec2(x/l, y/l); }
}
static class Segment {
Vec2 a, b;
Segment(Vec2 a, Vec2 b) { this.a = a; this.b = b; }
}
static class Target {
Vec2 pos;
Vec2 vel;
double radius;
Target(Vec2 p, Vec2 v, double r) { pos = p; vel = v; radius = r; }
}
private List<Segment> obstacles = new ArrayList<>();
private double bulletSpeed = 15.0;
private double gravity = 0.3;
public void addObstacle(Segment s) { obstacles.add(s); }
/**
* 射线与线段求交
* 返回交点参数t(沿射线距离),若无交点返回-1
*/
public double raySegmentIntersect(Vec2 origin, Vec2 dir, Segment seg) {
Vec2 r = dir;
Vec2 s = seg.b.sub(seg.a);
Vec2 diff = origin.sub(seg.a);
double rxs = r.x * s.y - r.y * s.x;
if (Math.abs(rxs) < 1e-9) return -1; // 平行
double t = (seg.a.sub(origin).x * s.y - seg.a.sub(origin).y * s.x) / (-rxs);
double u = (origin.sub(seg.a).x * r.y - origin.sub(seg.a).y * r.x) / rxs;
if (t >= 0 && u >= 0 && u <= 1) return t;
return -1;
}
/**
* 计算射线最近障碍物交点距离
*/
public double raycastDistance(Vec2 origin, Vec2 dir) {
double minT = Double.MAX_VALUE;
for (Segment seg : obstacles) {
double t = raySegmentIntersect(origin, dir, seg);
if (t >= 0 && t < minT) minT = t;
}
return minT == Double.MAX_VALUE ? -1 : minT;
}
/**
* 判断目标是否可见(无遮挡)
*/
public boolean isVisible(Vec2 shooterPos, Target target) {
Vec2 toTarget = target.pos.sub(shooterPos);
double dist = toTarget.len();
if (dist < 1e-9) return true;
Vec2 dir = toTarget.normalize();
double obstacleDist = raycastDistance(shooterPos, dir);
return obstacleDist < 0 || obstacleDist > dist - target.radius;
}
/**
* 计算提前量瞄准点(直线弹道,忽略重力)
* 解方程 |targetPos + targetVel * t - shooterPos| = bulletSpeed * t
*/
public Vec2 calculateLead(Vec2 shooterPos, Target target) {
Vec2 r = target.pos.sub(shooterPos);
double a = target.vel.dot(target.vel) - bulletSpeed * bulletSpeed;
double b = 2 * r.dot(target.vel);
double c = r.dot(r);
double discriminant = b * b - 4 * a * c;
if (discriminant < 0) return target.pos; // 无解,直接瞄准当前位置
double sqrtD = Math.sqrt(discriminant);
double t1 = (-b - sqrtD) / (2 * a);
double t2 = (-b + sqrtD) / (2 * a);
double t = (t1 > 0) ? t1 : (t2 > 0 ? t2 : -1);
if (t < 0) return target.pos;
return target.pos.add(target.vel.mul(t));
}
/**
* 计算抛物线弹道的发射角度
* 给定 shooterPos, targetPos, speed, gravity
* 返回可行角度(弧度),若无解返回Double.NaN
*/
public double calculateLaunchAngle(Vec2 shooter, Vec2 target, double speed) {
double dx = target.x - shooter.x;
double dy = target.y - shooter.y;
double dist = Math.hypot(dx, dy);
double v2 = speed * speed;
double v4 = v2 * v2;
double gd = gravity * dist;
double discriminant = v4 - gravity * (gravity * dist * dist + 2 * dy * v2);
if (discriminant < 0) return Double.NaN;
return Math.atan((v2 - Math.sqrt(discriminant)) / gd);
}
/**
* AI决策:是否开火及瞄准方向
*/
public void aiDecision(Vec2 shooterPos, Target target) {
if (!isVisible(shooterPos, target)) {
System.out.println("目标被遮挡,无法射击");
return;
}
Vec2 aimPoint = calculateLead(shooterPos, target);
Vec2 aimDir = aimPoint.sub(shooterPos).normalize();
double dist = aimPoint.sub(shooterPos).len();
// 抛物线角度(可选,若需要重力补偿)
double angle = calculateLaunchAngle(shooterPos, aimPoint, bulletSpeed);
System.out.printf("目标位置=(%.1f, %.1f) 速度=(%.1f, %.1f)%n",
target.pos.x, target.pos.y, target.vel.x, target.vel.y);
System.out.printf("提前量瞄准点=(%.1f, %.1f) 距离=%.1f%n",
aimPoint.x, aimPoint.y, dist);
System.out.printf("射击方向=(%.3f, %.3f) 抛物线角度=%.2f°%n",
aimDir.x, aimDir.y, Math.toDegrees(angle));
System.out.println(">>> AI决策:开火!\n");
}
public static void main(String[] args) {
ShootingAI ai = new ShootingAI();
// 添加障碍物
ai.addObstacle(new Segment(new Vec2(200, 0), new Vec2(200, 300)));
ai.addObstacle(new Segment(new Vec2(500, 200), new Vec2(500, 600)));
Vec2 shooter = new Vec2(50, 300);
Target target = new Target(new Vec2(600, 300), new Vec2(-2, 1), 20);
System.out.println("=== 射击AI演示 ===\n");
for (int i = 0; i < 5; i++) {
ai.aiDecision(shooter, target);
target.pos = target.pos.add(target.vel);
}
}
}
复杂度分析
- 时间复杂度:射线投射为O(W),W为障碍物线段数;提前量计算为O(1)解析解;每帧AI决策为O(W)。
- 空间复杂度:O(W),仅需存储障碍物列表。
扩展方向
- 弹道下坠补偿:结合抛物线方程,实时调整瞄准点高度以补偿重力下坠。
- 多目标优先级评估:引入威胁值、命中率预测,选择最优射击目标。
- 掩体利用:AI不仅射击,还需寻找掩护位置,结合路径规划算法实现战术移动。