每日算法 — 使用java实现赛车:最优Racing Line与曲线速度规划

引言

赛车运动是速度与技术的极致结合。顶尖车手不仅要全力加速,更要在弯道中找到最优行驶路线(Racing Line),以最短时间和最高平均速度通过赛道。本文将用Java实现一个赛车模拟系统,讲解如何基于曲率半径计算最优过弯路线,以及如何根据向心力限制进行曲线速度规划,实现最快的单圈时间。

核心算法原理

1. 最优Racing Line算法

Racing Line是指在弯道中能使车辆以最高平均速度通过的行驶轨迹。其核心思想是:

  1. 外内外原则:入弯前靠外侧,弯心处切内侧,出弯后回外侧
  2. 最大曲率半径: Racing Line 本质上是使过弯轨迹的曲率半径最大化
  3. 几何构造:对于标准弯道,最优路线是一段与内外侧边界相切的圆弧

数学上,若弯道内径为 $R_{in}$,外径为 $R_{out}$,则最优路线半径为:

R_optimal = (R_in + R_out) / 2

2. 曲线速度规划

车辆过弯时,轮胎提供的最大侧向摩擦力限制了最大速度。向心力公式:

F_friction >= m * v^2 / R

最大过弯速度为:

v_max = sqrt(mu * g * R)

其中 $mu$ 为轮胎摩擦系数,$g$ 为重力加速度,$R$ 为曲率半径。

3. 赛道分段速度优化

将赛道离散为若干段,每段根据曲率计算理论最高速度。然后通过前后向遍历进行速度平滑:

  1. 前向遍历:根据当前速度和最大加速度,计算下一段可达速度
  2. 后向遍历:根据刹车减速度和弯道限速,反向约束速度
  3. 取最小值:每段最终速度取前向和后向约束的较小值

Java完整实现

import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import java.util.List;

/**
 * 赛车模拟 - 最优Racing Line与曲线速度规划
 * 
 * 核心算法:
 * 1. 基于曲率最大化的最优Racing Line计算
 * 2. 向心力约束的弯道速度限制
 * 3. 前向后向遍历的速度平滑优化
 * 4. 最小转弯半径路径跟随
 */
public class RacingGame extends JPanel implements Runnable {

    // 车辆物理参数
    private static final double MAX_ACCEL = 8.0;      // 最大加速度 (m/s²)
    private static final double MAX_BRAKE = 15.0;     // 最大刹车减速度 (m/s²)
    private static final double MAX_SPEED = 120.0;    // 最高速度 (m/s)
    private static final double FRICTION_COEF = 1.2;  // 轮胎摩擦系数
    private static final double CAR_LENGTH = 4.5;     // 车长 (m)
    private static final double CAR_WIDTH = 2.0;      // 车宽 (m)
    private static final double GRAVITY = 9.8;        // 重力加速度

    // 赛道参数
    private static final double TRACK_SCALE = 8.0;    // 像素/米缩放
    private static final int SEGMENTS = 200;          // 赛道分段数

    private final List<TrackPoint> centerLine = new ArrayList<>();
    private final List<TrackPoint> racingLine = new ArrayList<>();
    private final List<Double> optimalSpeeds = new ArrayList<>();
    private final List<CurveInfo> curves = new ArrayList<>();

    // 车辆状态
    private double carX, carY;
    private double carSpeed;
    private double carHeading;
    private int currentSegment = 0;
    private double lapTime = 0;
    private double bestLapTime = Double.MAX_VALUE;
    private boolean running = true;
    private boolean showRacingLine = true;
    private boolean aiDriving = true;

    /**
     * 赛道点,包含位置、宽度和方向信息
     */
    static class TrackPoint {
        double x, y;           // 中心线坐标
        double width;          // 赛道半宽
        double tangent;        // 切线角度
        double curvature;      // 曲率

        TrackPoint(double x, double y, double w) {
            this.x = x;
            this.y = y;
            this.width = w;
        }
    }

    /**
     * 弯道信息
     */
    static class CurveInfo {
        int startIdx, endIdx;
        double radius;
        boolean isLeftTurn;

        CurveInfo(int s, int e, double r, boolean left) {
            this.startIdx = s;
            this.endIdx = e;
            this.radius = r;
            this.isLeftTurn = left;
        }
    }

    /**
     * 车辆轨迹历史
     */
    private final List<Point2D> carTrail = new ArrayList<>();

    public RacingGame() {
        setPreferredSize(new Dimension(1000, 700));
        setBackground(new Color(40, 40, 40));

        generateTrack();
        calculateRacingLine();
        calculateOptimalSpeeds();

        // 初始车辆位置
        TrackPoint start = racingLine.get(0);
        carX = start.x * TRACK_SCALE + getWidth() / 2.0;
        carY = start.y * TRACK_SCALE + getHeight() / 2.0;
        carSpeed = 0;
        carHeading = start.tangent;
    }

    /**
     * 生成赛道中心线
     * 
     * 构造一个包含直道、S弯、发夹弯、高速弯的复合赛道
     */
    private void generateTrack() {
        centerLine.clear();

        double[] segments = {
            // 格式: 长度(m), 曲率(1/m), 半宽(m)
            80, 0, 12,      // 主直道
            30, 0.02, 10,   // 右弯入弯
            40, 0.05, 10,   // 右弯
            30, 0.02, 10,   // 右弯出弯
            60, 0, 12,      // 短直道
            25, -0.03, 9,   // 左弯入弯
            35, -0.06, 9,   // 左弯
            25, -0.03, 9,   // 左弯出弯
            50, 0, 11,      // 直道
            20, 0.04, 8,    // 发夹弯入弯
            30, 0.12, 8,    // 发夹弯(急弯)
            20, 0.04, 8,    // 发夹弯出弯
            70, 0, 12,      // 长直道
            40, -0.025, 10, // 高速左弯
            40, 0.025, 10,  // 高速右弯
            50, 0, 11,      // 直道回起点
        };

        double x = 0, y = 0, heading = 0;
        double totalLen = 0;

        for (int i = 0; i < segments.length; i += 3) {
            double len = segments[i];
            double curvature = segments[i + 1];
            double halfWidth = segments[i + 2];

            int steps = (int) (len / 2);  // 每2米一个点
            for (int s = 0; s < steps; s++) {
                double stepLen = len / steps;

                // 曲率变化使弯道更平滑
                double effectiveCurve = curvature * 
                    Math.sin((s + 0.5) / steps * Math.PI);

                heading += effectiveCurve * stepLen;
                x += Math.cos(heading) * stepLen;
                y += Math.sin(heading) * stepLen;
                totalLen += stepLen;

                TrackPoint tp = new TrackPoint(x, y, halfWidth);
                tp.tangent = heading;
                tp.curvature = effectiveCurve;
                centerLine.add(tp);
            }
        }

        // 计算精确曲率(基于相邻点)
        for (int i = 0; i < centerLine.size(); i++) {
            int prev = (i - 1 + centerLine.size()) % centerLine.size();
            int next = (i + 1) % centerLine.size();

            TrackPoint p = centerLine.get(prev);
            TrackPoint c = centerLine.get(i);
            TrackPoint n = centerLine.get(next);

            // 用三点法估算曲率
            double dx1 = c.x - p.x;
            double dy1 = c.y - p.y;
            double dx2 = n.x - c.x;
            double dy2 = n.y - c.y;

            double cross = dx1 * dy2 - dy1 * dx2;
            double dist1 = Math.hypot(dx1, dy1);
            double dist2 = Math.hypot(dx2, dy2);

            if (dist1 > 0 && dist2 > 0) {
                c.curvature = 2 * cross / (dist1 * dist2 * (dist1 + dist2));
            }
        }
    }

    /**
     * 计算最优Racing Line
     * 
     * 算法:
     * 1. 识别所有弯道区间
     * 2. 对每个弯道,计算使曲率半径最大的内切圆弧
     * 3. 在弯道间用直线过渡
     * 4. 整体平滑处理
     */
    private void calculateRacingLine() {
        racingLine.clear();

        // 复制中心线作为基础
        for (TrackPoint tp : centerLine) {
            racingLine.add(new TrackPoint(tp.x, tp.y, tp.width));
        }

        // 识别弯道(曲率绝对值大于阈值)
        double curveThreshold = 0.005;
        int i = 0;
        while (i < centerLine.size()) {
            if (Math.abs(centerLine.get(i).curvature) > curveThreshold) {
                int start = i;
                boolean isLeft = centerLine.get(i).curvature < 0;

                while (i < centerLine.size() && 
                       ((isLeft && centerLine.get(i).curvature < -curveThreshold) ||
                        (!isLeft && centerLine.get(i).curvature > curveThreshold))) {
                    i++;
                }
                int end = i;

                // 计算弯道最优路线
                optimizeCurveSection(start, end, isLeft);

                curves.add(new CurveInfo(start, end, 
                    estimateCurveRadius(start, end), isLeft));
            } else {
                i++;
            }
        }

        // 平滑处理
        smoothRacingLine(3);

        // 重新计算切线方向
        for (int j = 0; j < racingLine.size(); j++) {
            int next = (j + 1) % racingLine.size();
            racingLine.get(j).tangent = Math.atan2(
                racingLine.get(next).y - racingLine.get(j).y,
                racingLine.get(next).x - racingLine.get(j).x);
        }
    }

    /**
     * 优化弯道段的Racing Line
     * 
     * 核心思想:在弯道中向内侧偏移,使实际行驶半径最大化
     */
    private void optimizeCurveSection(int start, int end, boolean isLeft) {
        int n = end - start;
        if (n <= 0) n += racingLine.size();

        for (int k = 0; k < n; k++) {
            int idx = (start + k) % racingLine.size();
            TrackPoint center = centerLine.get(idx);

            // 偏移量:弯心处偏移最大,入弯出弯处逐渐回到中心
            double progress = Math.sin((k / (double) n) * Math.PI);
            double offset = progress * center.width * 0.7;

            // 向弯道内侧偏移
            double perpX = -Math.sin(center.tangent);
            double perpY = Math.cos(center.tangent);

            if (isLeft) {
                perpX = -perpX;
                perpY = -perpY;
            }

            racingLine.get(idx).x = center.x + perpX * offset;
            racingLine.get(idx).y = center.y + perpY * offset;
        }
    }

    /**
     * 估算弯道半径
     */
    private double estimateCurveRadius(int start, int end) {
        double totalCurve = 0;
        int count = 0;

        for (int k = start; k != end; k = (k + 1) % centerLine.size()) {
            totalCurve += Math.abs(centerLine.get(k).curvature);
            count++;
            if (count > 500) break;
        }

        double avgCurve = count > 0 ? totalCurve / count : 0;
        return avgCurve > 0.001 ? 1.0 / avgCurve : 1000;
    }

    /**
     * 平滑Racing Line
     */
    private void smoothRacingLine(int iterations) {
        int size = racingLine.size();
        for (int iter = 0; iter < iterations; iter++) {
            double[] newX = new double[size];
            double[] newY = new double[size];

            for (int i = 0; i < size; i++) {
                int prev = (i - 1 + size) % size;
                int next = (i + 1) % size;

                newX[i] = (racingLine.get(prev).x + racingLine.get(i).x * 2 + 
                          racingLine.get(next).x) / 4.0;
                newY[i] = (racingLine.get(prev).y + racingLine.get(i).y * 2 + 
                          racingLine.get(next).y) / 4.0;
            }

            for (int i = 0; i < size; i++) {
                racingLine.get(i).x = newX[i];
                racingLine.get(i).y = newY[i];
            }
        }
    }

    /**
     * 计算最优速度分布
     * 
     * 算法:
     * 1. 计算每段的理论最高过弯速度 v = sqrt(mu * g * R)
     * 2. 前向遍历:根据加速度约束传播速度
     * 3. 后向遍历:根据刹车约束反向限制速度
     * 4. 取两者最小值作为最终速度
     */
    private void calculateOptimalSpeeds() {
        optimalSpeeds.clear();
        int size = racingLine.size();

        // 步骤1:计算每段曲率限速
        double[] curveLimit = new double[size];
        for (int i = 0; i < size; i++) {
            TrackPoint p = racingLine.get(i);
            double curvature = Math.abs(p.curvature);

            if (curvature > 0.0001) {
                double radius = 1.0 / curvature;
                // 向心力限制:v^2 <= mu * g * R
                curveLimit[i] = Math.sqrt(FRICTION_COEF * GRAVITY * radius);
            } else {
                curveLimit[i] = MAX_SPEED;
            }
            curveLimit[i] = Math.min(curveLimit[i], MAX_SPEED);
        }

        // 步骤2:前向遍历(加速约束)
        double[] forwardSpeed = new double[size];
        forwardSpeed[0] = 0;
        for (int i = 1; i < size; i++) {
            double dist = distance(racingLine.get(i - 1), racingLine.get(i));
            // v^2 = v0^2 + 2*a*s
            double maxFromAccel = Math.sqrt(
                forwardSpeed[i - 1] * forwardSpeed[i - 1] + 2 * MAX_ACCEL * dist);
            forwardSpeed[i] = Math.min(maxFromAccel, curveLimit[i]);
        }

        // 步骤3:后向遍历(刹车约束)
        double[] backwardSpeed = new double[size];
        backwardSpeed[size - 1] = curveLimit[size - 1];
        for (int i = size - 2; i >= 0; i--) {
            double dist = distance(racingLine.get(i), racingLine.get(i + 1));
            // v^2 = v0^2 + 2*a*s (减速)
            double maxFromBrake = Math.sqrt(
                backwardSpeed[i + 1] * backwardSpeed[i + 1] + 2 * MAX_BRAKE * dist);
            backwardSpeed[i] = Math.min(maxFromBrake, curveLimit[i]);
        }

        // 步骤4:取最小值
        for (int i = 0; i < size; i++) {
            optimalSpeeds.add(Math.min(forwardSpeed[i], backwardSpeed[i]));
        }
    }

    /**
     * 计算两点间距离
     */
    private double distance(TrackPoint a, TrackPoint b) {
        return Math.hypot(a.x - b.x, a.y - b.y);
    }

    /**
     * AI驾驶决策
     * 
     * 根据最优Racing Line和速度规划自动驾驶
     */
    private void aiDrive() {
        if (racingLine.isEmpty() || optimalSpeeds.isEmpty()) return;

        // 找到最近的Racing Line点
        double minDist = Double.MAX_VALUE;
        int nearestIdx = currentSegment;

        for (int i = 0; i < racingLine.size(); i++) {
            TrackPoint p = racingLine.get(i);
            double dx = p.x * TRACK_SCALE + getWidth() / 2.0 - carX;
            double dy = p.y * TRACK_SCALE + getHeight() / 2.0 - carY;
            double dist = Math.hypot(dx, dy);

            if (dist < minDist) {
                minDist = dist;
                nearestIdx = i;
            }
        }

        currentSegment = nearestIdx;

        // 目标速度
        double targetSpeed = optimalSpeeds.get(currentSegment);

        // 看向远处(预览前方5个点)
        int lookAhead = (currentSegment + 5) % racingLine.size();
        TrackPoint target = racingLine.get(lookAhead);
        double targetX = target.x * TRACK_SCALE + getWidth() / 2.0;
        double targetY = target.y * TRACK_SCALE + getHeight() / 2.0;

        // 计算目标方向
        double desiredHeading = Math.atan2(targetY - carY, targetX - carX);

        // 平滑转向
        double headingDiff = normalizeAngle(desiredHeading - carHeading);
        carHeading += headingDiff * 0.15;

        // 速度控制(PID简化版)
        double speedError = targetSpeed - carSpeed;
        if (speedError > 0) {
            carSpeed += Math.min(speedError, MAX_ACCEL * 0.016) * 0.5;
        } else {
            carSpeed += Math.max(speedError, -MAX_BRAKE * 0.016) * 0.5;
        }

        carSpeed = Math.max(0, Math.min(carSpeed, MAX_SPEED));

        // 更新位置
        carX += Math.cos(carHeading) * carSpeed * TRACK_SCALE * 0.016;
        carY += Math.sin(carHeading) * carSpeed * TRACK_SCALE * 0.016;

        // 记录轨迹
        if (frameCount % 3 == 0) {
            carTrail.add(new Point2D.Double(carX, carY));
            if (carTrail.size() > 300) carTrail.remove(0);
        }
    }

    /**
     * 角度归一化到[-PI, PI]
     */
    private double normalizeAngle(double angle) {
        while (angle > Math.PI) angle -= 2 * Math.PI;
        while (angle < -Math.PI) angle += 2 * Math.PI;
        return angle;
    }

    private int frameCount = 0;

    @Override
    public void run() {
        while (running) {
            frameCount++;

            if (aiDriving && !gameOver) {
                aiDrive();
            }

            lapTime += 0.016;

            // 检测是否完成一圈
            if (currentSegment < 5 && frameCount > 100) {
                if (lapTime > 10 && lapTime < bestLapTime) {
                    bestLapTime = lapTime;
                }
                if (lapTime > 10) {
                    lapTime = 0;
                    frameCount = 0;
                }
            }

            repaint();

            try {
                Thread.sleep(16);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                             RenderingHints.VALUE_ANTIALIAS_ON);

        double offsetX = getWidth() / 2.0;
        double offsetY = getHeight() / 2.0;

        // 绘制赛道边界
        drawTrackBoundary(g2d, offsetX, offsetY, true);
        drawTrackBoundary(g2d, offsetX, offsetY, false);

        // 绘制中心线
        g2d.setColor(new Color(80, 80, 80));
        g2d.setStroke(new BasicStroke(1));
        for (int i = 0; i < centerLine.size(); i++) {
            int next = (i + 1) % centerLine.size();
            g2d.drawLine(
                (int) (centerLine.get(i).x * TRACK_SCALE + offsetX),
                (int) (centerLine.get(i).y * TRACK_SCALE + offsetY),
                (int) (centerLine.get(next).x * TRACK_SCALE + offsetX),
                (int) (centerLine.get(next).y * TRACK_SCALE + offsetY));
        }

        // 绘制Racing Line
        if (showRacingLine) {
            g2d.setColor(new Color(0, 200, 255, 180));
            g2d.setStroke(new BasicStroke(3));
            for (int i = 0; i < racingLine.size(); i++) {
                int next = (i + 1) % racingLine.size();
                g2d.drawLine(
                    (int) (racingLine.get(i).x * TRACK_SCALE + offsetX),
                    (int) (racingLine.get(i).y * TRACK_SCALE + offsetY),
                    (int) (racingLine.get(next).x * TRACK_SCALE + offsetX),
                    (int) (racingLine.get(next).y * TRACK_SCALE + offsetY));
            }
        }

        // 绘制车辆轨迹
        if (carTrail.size() >= 2) {
            g2d.setColor(new Color(255, 100, 100, 150));
            g2d.setStroke(new BasicStroke(2));
            for (int i = 1; i < carTrail.size(); i++) {
                g2d.drawLine(
                    (int) carTrail.get(i - 1).getX(),
                    (int) carTrail.get(i - 1).getY(),
                    (int) carTrail.get(i).getX(),
                    (int) carTrail.get(i).getY());
            }
        }

        // 绘制车辆
        drawCar(g2d);

        // 绘制UI
        g2d.setColor(Color.WHITE);
        g2d.setFont(new Font("Arial", Font.BOLD, 16));
        g2d.drawString(String.format("速度: %.1f m/s", carSpeed), 20, 30);
        g2d.drawString(String.format("单圈时间: %.2f s", lapTime), 20, 55);
        g2d.drawString(String.format("最佳单圈: %.2f s", 
            bestLapTime == Double.MAX_VALUE ? 0 : bestLapTime), 20, 80);
        g2d.drawString("AI驾驶: " + (aiDriving ? "开启" : "关闭"), 20, 105);
        g2d.drawString("Racing Line: " + (showRacingLine ? "显示" : "隐藏"), 20, 130);
    }

    /**
     * 绘制赛道边界
     */
    private void drawTrackBoundary(Graphics2D g2d, double offsetX, double offsetY, 
                                    boolean isOuter) {
        int[] xPoints = new int[centerLine.size()];
        int[] yPoints = new int[centerLine.size()];

        for (int i = 0; i < centerLine.size(); i++) {
            TrackPoint p = centerLine.get(i);
            double perpX = -Math.sin(p.tangent);
            double perpY = Math.cos(p.tangent);
            double sign = isOuter ? 1 : -1;

            xPoints[i] = (int) ((p.x + perpX * p.width * sign) * TRACK_SCALE + offsetX);
            yPoints[i] = (int) ((p.y + perpY * p.width * sign) * TRACK_SCALE + offsetY);
        }

        g2d.setColor(isOuter ? new Color(150, 50, 50) : new Color(80, 80, 80));
        g2d.setStroke(new BasicStroke(3));
        g2d.drawPolyline(xPoints, yPoints, centerLine.size());
    }

    /**
     * 绘制赛车
     */
    private void drawCar(Graphics2D g2d) {
        Graphics2D carG = (Graphics2D) g2d.create();
        carG.translate(carX, carY);
        carG.rotate(carHeading);

        // 车身
        carG.setColor(Color.RED);
        carG.fillRoundRect((int) (-CAR_LENGTH * TRACK_SCALE / 2), 
                          (int) (-CAR_WIDTH * TRACK_SCALE / 2),
                          (int) (CAR_LENGTH * TRACK_SCALE), 
                          (int) (CAR_WIDTH * TRACK_SCALE), 5, 5);

        // 车头指示
        carG.setColor(Color.YELLOW);
        carG.fillOval((int) (CAR_LENGTH * TRACK_SCALE / 2 - 8), -4, 8, 8);

        // 速度指示
        carG.setColor(Color.CYAN);
        int speedBarLen = (int) (carSpeed / MAX_SPEED * CAR_LENGTH * TRACK_SCALE * 0.8);
        carG.fillRect((int) (-CAR_LENGTH * TRACK_SCALE / 2 + 2), 
                     (int) (-CAR_WIDTH * TRACK_SCALE / 2 - 6),
                     speedBarLen, 4);

        carG.dispose();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("赛车 - 最优Racing Line与曲线速度规划");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            RacingGame game = new RacingGame();
            frame.add(game);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
            new Thread(game).start();
        });
    }
}

算法复杂度分析

操作 时间复杂度 空间复杂度 说明
赛道生成 O(S) O(S) S为分段数
Racing Line计算 O(S × I) O(S) I为平滑迭代次数
最优速度前向遍历 O(S) O(S) 单次线性扫描
最优速度后向遍历 O(S) O(S) 单次反向扫描
AI最近点搜索 O(S) O(1) 每帧执行

核心算法优化点

  1. 弯道识别与内切优化:自动识别弯道区间,在弯心处向内侧偏移,使实际行驶半径最大化。
  2. 双向速度平滑:前向遍历考虑加速能力,后向遍历考虑刹车距离,两者取交集得到物理可行的最优速度分布。
  3. 向心力约束公式:直接使用 $v = \sqrt{\mu g R}$ 计算弯道限速,确保车辆不会侧滑。
  4. 预览转向:AI看向远处5个点而非仅仅最近点,使转向更平滑,避免振荡。

扩展思考

  1. 非对称赛道:引入坡度、路面材质变化(干/湿),使摩擦系数动态变化,速度规划更复杂。
  2. 超车策略:增加多车交互,使用博弈论或强化学习决定是否超车、何时超车。
  3. 轮胎磨损模型:引入轮胎温度、磨损对抓地力的影响,需要在速度与轮胎保护之间做权衡。

总结

本文通过Java实现了一个赛车模拟系统,涵盖最优Racing Line计算、向心力限速公式和双向速度平滑三大核心算法。该实现展示了如何将几何路径规划与物理约束结合,找到赛道上的最优行驶策略,对理解车辆动力学和最优控制具有参考价值。