引言
赛车运动是速度与技术的极致结合。顶尖车手不仅要全力加速,更要在弯道中找到最优行驶路线(Racing Line),以最短时间和最高平均速度通过赛道。本文将用Java实现一个赛车模拟系统,讲解如何基于曲率半径计算最优过弯路线,以及如何根据向心力限制进行曲线速度规划,实现最快的单圈时间。
核心算法原理
1. 最优Racing Line算法
Racing Line是指在弯道中能使车辆以最高平均速度通过的行驶轨迹。其核心思想是:
- 外内外原则:入弯前靠外侧,弯心处切内侧,出弯后回外侧
- 最大曲率半径: Racing Line 本质上是使过弯轨迹的曲率半径最大化
- 几何构造:对于标准弯道,最优路线是一段与内外侧边界相切的圆弧
数学上,若弯道内径为 $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. 赛道分段速度优化
将赛道离散为若干段,每段根据曲率计算理论最高速度。然后通过前后向遍历进行速度平滑:
- 前向遍历:根据当前速度和最大加速度,计算下一段可达速度
- 后向遍历:根据刹车减速度和弯道限速,反向约束速度
- 取最小值:每段最终速度取前向和后向约束的较小值
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) | 每帧执行 |
核心算法优化点
- 弯道识别与内切优化:自动识别弯道区间,在弯心处向内侧偏移,使实际行驶半径最大化。
- 双向速度平滑:前向遍历考虑加速能力,后向遍历考虑刹车距离,两者取交集得到物理可行的最优速度分布。
- 向心力约束公式:直接使用 $v = \sqrt{\mu g R}$ 计算弯道限速,确保车辆不会侧滑。
- 预览转向:AI看向远处5个点而非仅仅最近点,使转向更平滑,避免振荡。
扩展思考
- 非对称赛道:引入坡度、路面材质变化(干/湿),使摩擦系数动态变化,速度规划更复杂。
- 超车策略:增加多车交互,使用博弈论或强化学习决定是否超车、何时超车。
- 轮胎磨损模型:引入轮胎温度、磨损对抓地力的影响,需要在速度与轮胎保护之间做权衡。
总结
本文通过Java实现了一个赛车模拟系统,涵盖最优Racing Line计算、向心力限速公式和双向速度平滑三大核心算法。该实现展示了如何将几何路径规划与物理约束结合,找到赛道上的最优行驶策略,对理解车辆动力学和最优控制具有参考价值。