6. AI骑手
2026/4/14大约 8 分钟
AI骑手
一个人跑赛道多无聊,得有对手才有竞争感。AI骑手就是电脑控制的对手,它们需要像真人一样沿着赛道跑,不能穿墙、不能飞、不能完美无缺——否则玩家会觉得在跟机器比赛,没意思。
本章你将学到
- 赛道寻路AI的实现
- 速度-风险曲线设计
- 超车与防守策略
- 难度分级系统
- AI失误系统
AI骑手的设计思路
AI骑手的核心问题只有一个:接下来该往哪走?
回答了这个问题,剩下的就是:走多快?什么时候刹车?怎么超车?
赛道寻路
路点系统
我们用一系列路点(Waypoint)来定义赛道的路径。AI只需要沿着这些路点依次前进就行了。
赛道路径:
起点 ── WP1 ── WP2 ── WP3 ── WP4 ── ... ── 终点
\ /
弯道区域(路点更密)路点就是在赛道上摆放的 Marker3D 节点。直线路段可以稀一些(每隔50米放一个),弯道区域要密一些(每隔10~20米放一个),这样AI转弯才不会切到赛道外面。
寻路代码
C#
// AI骑手寻路控制器
public partial class AIRiderController : CharacterBody3D
{
[Export] public PathFollow3D TrackPath; // 赛道路径
[Export] public float MaxSpeed = 40f; // 最高速度
[Export] public float Acceleration = 15f; // 加速度
[Export] public float BrakeDeceleration = 25f; // 刹车减速度
[Export] public float SteerSpeed = 3f; // 转向速度
[Export] public float LookAheadDistance = 20f; // 前瞻距离
private float _currentSpeed;
private float _pathProgress; // 在路径上的进度
private Vector3 _targetPosition;
private int _difficulty = 1; // 难度:0=新手, 1=普通, 2=专家
// 难度影响参数
private static readonly float[] SpeedMultipliers = { 0.7f, 0.85f, 1.0f };
private static readonly float[] ErrorRates = { 0.02f, 0.01f, 0.003f };
public override void _PhysicsProcess(double delta)
{
var dt = (float)delta;
// 1. 更新路径进度
UpdatePathProgress(dt);
// 2. 计算目标点(前瞻一段距离)
CalculateTarget();
// 3. 决定速度
DecideSpeed(dt);
// 4. 决定方向
SteerTowardTarget(dt);
// 5. AI失误
ApplyMistakes(dt);
// 6. 移动
Velocity = Transform.Basis.Z * _currentSpeed;
MoveAndSlide();
}
private void UpdatePathProgress(float delta)
{
// 根据当前速度推进路径进度
float pathDelta = _currentSpeed * delta / GetPathLength();
_pathProgress = Mathf.Clamp(_pathProgress + pathDelta, 0f, 1f);
TrackPath.ProgressRatio = _pathProgress;
}
private void CalculateTarget()
{
// 看向路径前方的一个点
float targetProgress = Mathf.Min(_pathProgress + LookAheadDistance / GetPathLength(), 1f);
TrackPath.ProgressRatio = targetProgress;
_targetPosition = TrackPath.GlobalPosition;
// 恢复当前进度
TrackPath.ProgressRatio = _pathProgress;
}
private void DecideSpeed(float delta)
{
float effectiveMaxSpeed = MaxSpeed * SpeedMultipliers[_difficulty];
// 检查前方弯度来决定速度
float curvature = EstimateCurvature();
float targetSpeed = effectiveMaxSpeed * (1f - curvature * 0.6f);
if (_currentSpeed < targetSpeed)
_currentSpeed = Mathf.Min(_currentSpeed + Acceleration * delta, targetSpeed);
else
_currentSpeed = Mathf.Max(_currentSpeed - BrakeDeceleration * delta, targetSpeed);
}
private void SteerTowardTarget(float delta)
{
Vector3 direction = (_targetPosition - GlobalPosition).Normalized();
Vector3 forward = Transform.Basis.Z;
// 计算需要转向的角度
float angleDiff = Mathf.Sign(forward.Cross(direction).Y) *
Mathf.Acos(Mathf.Clamp(forward.Dot(direction), -1f, 1f));
// 平滑转向
float steerAmount = Mathf.Clamp(angleDiff * SteerSpeed * delta, -1f, 1f);
RotateY(steerAmount);
}
// 估算前方弯度(0=直线,1=急弯)
private float EstimateCurvature()
{
// 比较当前位置方向和前方目标方向
Vector3 currentDir = TrackPath.GlobalTransform.Basis.Z;
float saveProgress = _pathProgress;
TrackPath.ProgressRatio = Mathf.Min(_pathProgress + 0.05f, 1f);
Vector3 futureDir = TrackPath.GlobalTransform.Basis.Z;
TrackPath.ProgressRatio = saveProgress;
float angle = Mathf.Acos(Mathf.Clamp(currentDir.Dot(futureDir), -1f, 1f));
return Mathf.Clamp(angle / 1f, 0f, 1f); // 1弧度以内归一化
}
private float GetPathLength()
{
return TrackPath.GetParent<Path3D>().Curve.GetBakedLength();
}
// AI失误:偶尔偏移目标
private void ApplyMistakes(float delta)
{
if (GD.Randf() < ErrorRates[_difficulty])
{
// 随机偏移目标点
_targetPosition += new Vector3(
(float)GD.RandRange(-2f, 2f),
0,
(float)GD.RandRange(-2f, 2f)
);
}
}
}GDScript
# AI骑手寻路控制器
extends CharacterBody3D
@export var track_path: PathFollow3D # 赛道路径
@export var max_speed: float = 40.0 # 最高速度
@export var acceleration: float = 15.0 # 加速度
@export var brake_deceleration: float = 25.0 # 刹车减速度
@export var steer_speed: float = 3.0 # 转向速度
@export var look_ahead_distance: float = 20.0 # 前瞻距离
var _current_speed: float = 0.0
var _path_progress: float = 0.0
var _target_position: Vector3
var _difficulty: int = 1 # 0=新手, 1=普通, 2=专家
# 难度影响参数
var _speed_multipliers: Array[float] = [0.7, 0.85, 1.0]
var _error_rates: Array[float] = [0.02, 0.01, 0.003]
func _physics_process(delta):
# 1. 更新路径进度
_update_path_progress(delta)
# 2. 计算目标点
_calculate_target()
# 3. 决定速度
_decide_speed(delta)
# 4. 决定方向
_steer_toward_target(delta)
# 5. AI失误
_apply_mistakes(delta)
# 6. 移动
velocity = transform.basis.z * _current_speed
move_and_slide()
func _update_path_progress(delta: float):
var path_delta = _current_speed * delta / _get_path_length()
_path_progress = clampf(_path_progress + path_delta, 0.0, 1.0)
track_path.progress_ratio = _path_progress
func _calculate_target():
# 看向路径前方的一个点
var target_progress = minf(_path_progress + look_ahead_distance / _get_path_length(), 1.0)
track_path.progress_ratio = target_progress
_target_position = track_path.global_position
# 恢复当前进度
track_path.progress_ratio = _path_progress
func _decide_speed(delta: float):
var effective_max_speed = max_speed * _speed_multipliers[_difficulty]
# 检查前方弯度来决定速度
var curvature = _estimate_curvature()
var target_speed = effective_max_speed * (1.0 - curvature * 0.6)
if _current_speed < target_speed:
_current_speed = minf(_current_speed + acceleration * delta, target_speed)
else:
_current_speed = maxf(_current_speed - brake_deceleration * delta, target_speed)
func _steer_toward_target(delta: float):
var direction = (_target_position - global_position).normalized()
var forward = transform.basis.z
# 计算需要转向的角度
var cross = forward.cross(direction).y
var dot = clampf(forward.dot(direction), -1.0, 1.0)
var angle_diff = signf(cross) * acos(dot)
# 平滑转向
var steer_amount = clampf(angle_diff * steer_speed * delta, -1.0, 1.0)
rotate_y(steer_amount)
# 估算前方弯度(0=直线,1=急弯)
func _estimate_curvature() -> float:
var current_dir = track_path.global_transform.basis.z
var save_progress = _path_progress
track_path.progress_ratio = minf(_path_progress + 0.05, 1.0)
var future_dir = track_path.global_transform.basis.z
track_path.progress_ratio = save_progress
var angle = acos(clampf(current_dir.dot(future_dir), -1.0, 1.0))
return clampf(angle / 1.0, 0.0, 1.0)
func _get_path_length() -> float:
return track_path.get_parent().curve.get_baked_length()
# AI失误:偶尔偏移目标
func _apply_mistakes(delta: float):
if randf() < _error_rates[_difficulty]:
_target_position += Vector3(
randf_range(-2.0, 2.0),
0.0,
randf_range(-2.0, 2.0)
)速度-风险曲线
AI不应该全程以最高速度跑。在弯道前需要减速,直道上可以全力加速。这个决策逻辑就是速度-风险曲线。
简单说就是:弯道越急,AI越要减速。这个曲线的形状可以通过调整 curvature * 0.6 这个系数来改变。
超车与防守策略
当AI骑手靠近其他骑手时,需要做两件事:
- 超车:如果前面有更慢的对手,尝试绕过去
- 防守:如果后面有更快的对手,挡住超车路线
超车逻辑
C#
// 超车检测 - 检查前方是否有需要避开的对手
private void CheckOvertake()
{
var spaceState = GetWorld3D().DirectSpaceState;
Vector3 forward = GlobalTransform.Basis.Z;
// 向前方发射射线
var query = PhysicsRayQueryParameters3D.Create(
GlobalPosition,
GlobalPosition + forward * 8f // 前方8米
);
query.CollisionMask = 0b10; // 只检测骑手层
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
// 前方有对手,向侧面偏移目标点
Vector3 avoidDir = GlobalTransform.Basis.X;
// 判断哪边空间更大
_targetPosition += avoidDir * 3f; // 向右偏移3米
}
}GDScript
# 超车检测 - 检查前方是否有需要避开的对手
func _check_overtake():
var space_state = get_world_3d().direct_space_state
var forward = global_transform.basis.z
# 向前方发射射线
var query = PhysicsRayQueryParameters3D.create(
global_position,
global_position + forward * 8.0 # 前方8米
)
query.collision_mask = 0b10 # 只检测骑手层
var result = space_state.intersect_ray(query)
if result.size() > 0:
# 前方有对手,向侧面偏移目标点
var avoid_dir = global_transform.basis.x
_target_position += avoid_dir * 3.0 # 向右偏移3米难度分级系统
不同玩家水平不同,AI也需要有不同难度:
| 难度 | 最高速度比例 | 失误率 | 恢复速度 | 描述 |
|---|---|---|---|---|
| 新手 | 70% | 2% | 快 | 慢速、经常犯错、但犯错后快速恢复 |
| 普通 | 85% | 1% | 中 | 中等速度、偶尔犯错 |
| 专家 | 100% | 0.3% | 慢 | 高速、很少犯错、犯错后恢复慢 |
难度影响的不只是速度,更重要的是失误率。新手AI会偶尔冲出赛道、在弯道刹车太晚,给玩家追赶的机会。
AI失误系统
一个完美的AI是不好玩的。我们需要让AI偶尔犯错,这样比赛才有悬念。
失误类型
- 刹车太晚:进入弯道时减速不够,导致冲出赛道
- 转向过度:转弯时打多了方向,左右摇摆
- 速度判断失误:在颠簸路段没有减速
- 注意力分散:偶尔偏移路线
这些失误通过 _apply_mistakes 函数中的随机偏移来实现。难度越高,偏移越小、频率越低。
AI骑手场景结构
AIRider (CharacterBody3D)
├── MeshInstance3D ← 摩托车+骑手模型
├── CollisionShape3D ← 碰撞体
├── PathFollow3D ← 路径跟随(引用赛道Path3D)
└── AIRiderController ← AI控制脚本常见问题
Q:AI骑手总是撞墙怎么办?
检查路点是否离墙壁太近。路点应该放在赛道中央,距离两侧墙壁至少有2~3米的安全距离。同时在转向时加入平滑插值,避免急转弯。
Q:所有AI骑手都跑一样的路线怎么办?
给每个AI骑手不同的随机种子,或者在路点附近加一个随机偏移(+-1米),让它们的路线略有不同。
Q:怎么让AI骑手和玩家竞争?
用难度分级来匹配玩家的水平。可以先设定一个"基准用时",如果玩家连续赢了几局,自动调高AI难度;如果玩家连续输,降低AI难度。
下一步
AI骑手完成后,开始 游戏UI。
