3. 摩托车物理
摩托车物理
这是整个项目最核心也最有挑战的部分。摩托车只有两个轮子,为什么不会倒?转弯时为什么要倾斜?前刹和后刹有什么区别?本章我们用 Godot 的物理引擎来回答这些问题。
本章你将学到
- 两轮平衡系统的实现原理
- 倾斜转弯(Lean Steering)机制
- 前刹与后刹的差异模拟
- 越野路面抓地力模型
- 悬挂系统基础
为什么摩托车这么难模拟?
汽车有四个轮子,天然就站得稳。摩托车只有两个轮子,静止的时候会倒,只有在行驶中才能保持平衡——靠的是陀螺效应和前轮自稳定效应。
如果完全按照真实物理来模拟,玩家连让摩托车站起来都做不到。所以我们的策略是:用简化的物理来创造"感觉像真的"效果。
方案选择:RigidBody3D vs CharacterBody3D
本教程使用 RigidBody3D 方案,因为它能更自然地模拟惯性、漂移和碰撞。
摩托车场景结构
Motorcycle (RigidBody3D) ← 物理主体
├── BodyMesh (MeshInstance3D) ← 车身模型
├── RiderMesh (MeshInstance3D) ← 骑手模型
├── CollisionShape3D ← 碰撞形状(胶囊体)
├── CenterOfMass (Marker3D) ← 重心标记(偏低设置)
├── RayCast3D_Front ← 前轮射线检测
├── RayCast3D_Rear ← 后轮射线检测
└── MotorcycleController ← 控制脚本把 CenterOfMass 的 Y 坐标设为 -0.3 左右(低于车身中心),这样重心低,不容易翻。
物理模型总览
两轮平衡系统
平衡是摩托车物理最关键的部分。我们的做法是:在每一帧都施加一个"纠偏力矩",把摩托车往垂直方向拉。
你可以想象成:摩托车上面有一根看不见的橡皮筋,永远把它往正上方拽。速度越快,橡皮筋拉力越大(因为真实摩托车高速时更稳定)。
// 摩托车控制器 - 平衡系统部分
public partial class MotorcycleController : RigidBody3D
{
[Export] public float MaxSpeed = 50f; // 最高速度 m/s (~180km/h)
[Export] public float AccelerationForce = 800f; // 加速力
[Export] public float BrakeForceFront = 1200f; // 前刹制动力
[Export] public float BrakeForceRear = 600f; // 后刹制动力
[Export] public float SteerSpeed = 2.5f; // 转向速度
[Export] public float MaxLeanAngle = 45f; // 最大倾斜角度(度)
[Export] public float BalanceStrength = 15f; // 平衡力强度
[Export] public float LeanDamping = 5f; // 倾斜阻尼
private float _steerInput;
private float _throttleInput;
private float _brakeFrontInput;
private float _brakeRearInput;
private float _currentLean;
private bool _isGrounded;
public override void _PhysicsProcess(double delta)
{
var dt = (float)delta;
// 读取输入
ReadInput();
// 检测是否在地面
_isGrounded = CheckGrounded();
if (_isGrounded)
{
// 1. 自动平衡
ApplyBalance(dt);
// 2. 处理转向和倾斜
ApplySteering(dt);
// 3. 施加驱动力
ApplyDriveForce();
// 4. 施加制动力
ApplyBrakeForce();
}
}
// 自动平衡:让摩托车趋向垂直
private void ApplyBalance(float delta)
{
float speed = LinearVelocity.Length();
// 速度越快,平衡力越强(模拟陀螺效应)
float balanceFactor = Mathf.Lerp(BalanceStrength, BalanceStrength * 3f,
Mathf.Clamp(speed / MaxSpeed, 0f, 1f));
// 获取当前的倾斜角度(绕前方向的旋转)
Vector3 localAngularVel = AngularVelocity * GlobalTransform.Basis;
float leanVelocity = localAngularVel.X; // X轴是倾斜方向
// 如果没有转向输入,自动回到垂直位置
float targetLean = 0f;
if (Mathf.Abs(_steerInput) > 0.1f)
{
targetLean = -_steerInput * MaxLeanAngle;
}
float currentLean = GetCurrentLeanAngle();
float leanDiff = Mathf.DegToRad(targetLean - currentLean);
// 施加纠偏力矩
float torqueX = leanDiff * balanceFactor - leanVelocity * LeanDamping;
// 应用到全局坐标
Vector3 torque = GlobalTransform.Basis.X * torqueX;
ApplyTorque(torque);
}
// 获取当前倾斜角度
private float GetCurrentLeanAngle()
{
Vector3 up = GlobalTransform.Basis.Y;
Vector3 worldUp = Vector3.Up;
float angle = Mathf.RadToDeg(Mathf.Acos(up.Dot(worldUp)));
// 判断倾斜方向
float side = GlobalTransform.Basis.X.Dot(worldUp);
return side < 0 ? angle : -angle;
}
// 检测是否在地面
private bool CheckGrounded()
{
// 用射线检测轮子下方是否有地面
var spaceState = GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(
GlobalPosition,
GlobalPosition + Vector3.Down * 1.2f
);
return spaceState.IntersectRay(query).Count > 0;
}
private void ReadInput()
{
_steerInput = Input.GetAxis("steer_left", "steer_right");
_throttleInput = Input.GetStrength("accelerate");
_brakeFrontInput = Input.GetStrength("brake_front");
_brakeRearInput = Input.GetStrength("brake_rear");
}
private void ApplySteering(float delta)
{
float speed = LinearVelocity.Length();
if (speed < 0.5f) return;
// 速度越快转向越灵敏(但有上限)
float steerFactor = Mathf.Clamp(speed / 10f, 0.3f, 1.5f);
float yawTorque = _steerInput * SteerSpeed * steerFactor;
// 绕摩托车上方向旋转(偏航)
Vector3 torque = GlobalTransform.Basis.Y * yawTorque;
ApplyTorque(torque);
}
private void ApplyDriveForce()
{
Vector3 forward = -GlobalTransform.Basis.Z;
Vector3 force = forward * _throttleInput * AccelerationForce;
// 限速
float forwardSpeed = LinearVelocity.Dot(forward);
if (forwardSpeed < MaxSpeed)
{
ApplyCentralForce(force);
}
}
private void ApplyBrakeForce()
{
Vector3 forward = -GlobalTransform.Basis.Z;
float forwardSpeed = LinearVelocity.Dot(forward);
// 前刹:强力减速
if (_brakeFrontInput > 0.1f && forwardSpeed > 0.5f)
{
Vector3 brakeForce = -forward * _brakeFrontInput * BrakeForceFront;
ApplyCentralForce(brakeForce);
}
// 后刹:温和减速
if (_brakeRearInput > 0.1f && forwardSpeed > 0.5f)
{
Vector3 brakeForce = -forward * _brakeRearInput * BrakeForceRear;
ApplyCentralForce(brakeForce);
}
}
}# 摩托车控制器 - 平衡系统部分
extends RigidBody3D
@export var max_speed: float = 50.0 # 最高速度 m/s (~180km/h)
@export var acceleration_force: float = 800.0 # 加速力
@export var brake_force_front: float = 1200.0 # 前刹制动力
@export var brake_force_rear: float = 600.0 # 后刹制动力
@export var steer_speed: float = 2.5 # 转向速度
@export var max_lean_angle: float = 45.0 # 最大倾斜角度(度)
@export var balance_strength: float = 15.0 # 平衡力强度
@export var lean_damping: float = 5.0 # 倾斜阻尼
var _steer_input: float
var _throttle_input: float
var _brake_front_input: float
var _brake_rear_input: float
var _current_lean: float
var _is_grounded: bool
func _physics_process(delta):
# 读取输入
_read_input()
# 检测是否在地面
_is_grounded = _check_grounded()
if _is_grounded:
# 1. 自动平衡
_apply_balance(delta)
# 2. 处理转向和倾斜
_apply_steering(delta)
# 3. 施加驱动力
_apply_drive_force()
# 4. 施加制动力
_apply_brake_force()
# 自动平衡:让摩托车趋向垂直
func _apply_balance(delta: float):
var speed = linear_velocity.length()
# 速度越快,平衡力越强(模拟陀螺效应)
var balance_factor = lerpf(balance_strength, balance_strength * 3.0,
clampf(speed / max_speed, 0.0, 1.0))
# 获取当前的倾斜角度(绕前方向的旋转)
var local_angular_vel = angular_velocity * global_transform.basis
var lean_velocity = local_angular_vel.x # X轴是倾斜方向
# 如果没有转向输入,自动回到垂直位置
var target_lean = 0.0
if absf(_steer_input) > 0.1:
target_lean = -_steer_input * max_lean_angle
var current_lean = _get_current_lean_angle()
var lean_diff = deg_to_rad(target_lean - current_lean)
# 施加纠偏力矩
var torque_x = lean_diff * balance_factor - lean_velocity * lean_damping
# 应用到全局坐标
var torque = global_transform.basis.x * torque_x
apply_torque(torque)
# 获取当前倾斜角度
func _get_current_lean_angle() -> float:
var up = global_transform.basis.y
var world_up = Vector3.UP
var angle = rad_to_deg(acos(up.dot(world_up)))
# 判断倾斜方向
var side = global_transform.basis.x.dot(world_up)
return -angle if side < 0 else angle
# 检测是否在地面
func _check_grounded() -> bool:
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
global_position,
global_position + Vector3.DOWN * 1.2
)
return space_state.intersect_ray(query).size > 0
func _read_input():
_steer_input = Input.get_axis("steer_left", "steer_right")
_throttle_input = Input.get_strength("accelerate")
_brake_front_input = Input.get_strength("brake_front")
_brake_rear_input = Input.get_strength("brake_rear")
func _apply_steering(delta: float):
var speed = linear_velocity.length()
if speed < 0.5:
return
# 速度越快转向越灵敏(但有上限)
var steer_factor = clampf(speed / 10.0, 0.3, 1.5)
var yaw_torque = _steer_input * steer_speed * steer_factor
# 绕摩托车上方向旋转(偏航)
var torque = global_transform.basis.y * yaw_torque
apply_torque(torque)
func _apply_drive_force():
var forward = -global_transform.basis.z
var force = forward * _throttle_input * acceleration_force
# 限速
var forward_speed = linear_velocity.dot(forward)
if forward_speed < max_speed:
apply_central_force(force)
func _apply_brake_force():
var forward = -global_transform.basis.z
var forward_speed = linear_velocity.dot(forward)
# 前刹:强力减速
if _brake_front_input > 0.1 and forward_speed > 0.5:
var brake_force = -forward * _brake_front_input * brake_force_front
apply_central_force(brake_force)
# 后刹:温和减速
if _brake_rear_input > 0.1 and forward_speed > 0.5:
var brake_force = -forward * _brake_rear_input * brake_force_rear
apply_central_force(brake_force)倾斜转弯(Lean Steering)
摩托车转弯和汽车完全不同。汽车转弯靠的是前轮偏转,而摩托车转弯靠的是车身倾斜——你往左倾斜,车就往左转。
这就是为什么骑摩托的人过弯时身体几乎贴到地上——倾斜角度越大,转弯半径越小。
倾斜和转向的关系
倾斜角度越大 → 转弯越急
倾斜角度越小 → 转弯越缓
不倾斜 → 直线行驶在上面的代码中,我们已经把倾斜和转向关联起来了:转向输入同时控制倾斜和偏航,让转弯看起来自然。
前刹与后刹的差异
摩托车有两个刹车,前刹和后刹的效果截然不同:
| 特性 | 前刹 | 后刹 |
|---|---|---|
| 制动力 | 强(承担约70%制动力) | 弱(承担约30%制动力) |
| 使用场景 | 需要急刹时 | 需要平稳减速时 |
| 危险性 | 急刹时前轮可能抱死→翻车 | 相对安全,最多侧滑 |
| 游戏中映射 | 空格键(Space) | S键 |
| 游戏中力度 | 1200N | 600N |
注意
在真实生活中,高速时只捏前刹非常危险!但在游戏里我们可以适当放宽,让前刹不会轻易导致翻车,否则玩家体验会很差。
越野抓地力模型
不同路面提供的抓地力不同。沙地上轮子容易打滑,柏油路上则抓得很紧。我们用一个简单的系数来模拟:
// 地面类型和对应的抓地力系数
public enum SurfaceType
{
Asphalt, // 柏油路 - 抓地力最好
Dirt, // 泥地 - 抓地力中等
Sand, // 沙地 - 抓地力差
Snow, // 雪地 - 抓地力最差
Gravel // 碎石路 - 抓地力中等偏下
}
public partial class GripSystem : Node
{
// 不同路面的摩擦系数
private static readonly Dictionary<SurfaceType, float> GripMultipliers = new()
{
{ SurfaceType.Asphalt, 1.0f },
{ SurfaceType.Dirt, 0.7f },
{ SurfaceType.Sand, 0.4f },
{ SurfaceType.Snow, 0.3f },
{ SurfaceType.Gravel, 0.6f }
};
private MotorcycleController _moto;
private SurfaceType _currentSurface = SurfaceType.Dirt;
public override void _Ready()
{
_moto = GetParent<MotorcycleController>();
}
public override void _PhysicsProcess(double delta)
{
// 检测当前路面类型(通过射线碰撞的物理材质判断)
DetectSurface();
// 根据路面调整物理参数
float grip = GripMultipliers[_currentSurface];
// 抓地力低时,增加侧滑倾向
if (grip < 0.5f)
{
// 减少侧向阻力,让摩托车更容易侧滑
_moto.SetLinearDamp(0.1f);
}
else
{
_moto.SetLinearDamp(0.5f);
}
}
private void DetectSurface()
{
// 通过射线检测地面物理材质
var spaceState = _moto.GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(
_moto.GlobalPosition,
_moto.GlobalPosition + Vector3.Down * 1.2f
);
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
// 检查碰撞体的组来判断地面类型
var collider = result["collider"].AsGodotObject() as PhysicsBody3D;
if (collider != null)
{
if (collider.IsInGroup("surface_asphalt"))
_currentSurface = SurfaceType.Asphalt;
else if (collider.IsInGroup("surface_sand"))
_currentSurface = SurfaceType.Sand;
else if (collider.IsInGroup("surface_snow"))
_currentSurface = SurfaceType.Snow;
else if (collider.IsInGroup("surface_gravel"))
_currentSurface = SurfaceType.Gravel;
else
_currentSurface = SurfaceType.Dirt;
}
}
}
public SurfaceType GetCurrentSurface() => _currentSurface;
public float GetCurrentGrip() => GripMultipliers[_currentSurface];
}# 地面类型枚举
enum SurfaceType {
ASPHALT, # 柏油路
DIRT, # 泥地
SAND, # 沙地
SNOW, # 雪地
GRAVEL # 碎石路
}
# 地面抓地力系统
extends Node
# 不同路面的摩擦系数
var _grip_multipliers: Dictionary = {
SurfaceType.ASPHALT: 1.0,
SurfaceType.DIRT: 0.7,
SurfaceType.SAND: 0.4,
SurfaceType.SNOW: 0.3,
SurfaceType.GRAVEL: 0.6
}
var _moto: RigidBody3D
var _current_surface: int = SurfaceType.DIRT
func _ready():
_moto = get_parent()
func _physics_process(delta):
# 检测当前路面类型
_detect_surface()
# 根据路面调整物理参数
var grip = _grip_multipliers[_current_surface]
# 抓地力低时,增加侧滑倾向
if grip < 0.5:
_moto.linear_damp = 0.1
else:
_moto.linear_damp = 0.5
func _detect_surface():
# 通过射线检测地面物理材质
var space_state = _moto.get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
_moto.global_position,
_moto.global_position + Vector3.DOWN * 1.2
)
var result = space_state.intersect_ray(query)
if result.size() > 0:
var collider = result["collider"]
if collider is PhysicsBody3D:
if collider.is_in_group("surface_asphalt"):
_current_surface = SurfaceType.ASPHALT
elif collider.is_in_group("surface_sand"):
_current_surface = SurfaceType.SAND
elif collider.is_in_group("surface_snow"):
_current_surface = SurfaceType.SNOW
elif collider.is_in_group("surface_gravel"):
_current_surface = SurfaceType.GRAVEL
else:
_current_surface = SurfaceType.DIRT
func get_current_surface() -> int:
return _current_surface
func get_current_grip() -> float:
return _grip_multipliers[_current_surface]悬挂系统基础
真实的摩托车有前叉和后减震器,用来吸收路面颠簸。在游戏里我们可以简化这个系统:
// 简化悬挂:通过弹簧力模拟
public partial class SimpleSuspension : Node
{
[Export] public float RestLength = 0.8f; // 弹簧自然长度
[Export] public float SpringStiffness = 500f; // 弹簧刚度
[Export] public float Damping = 30f; // 阻尼系数
[Export] public Vector3 SuspensionOffset = new(0, -0.5f, 0); // 悬挂位置偏移
private RigidBody3D _body;
public override void _Ready()
{
_body = GetParent<RigidBody3D>();
}
public override void _PhysicsProcess(double delta)
{
Vector3 suspStart = _body.GlobalPosition + _body.GlobalTransform.Basis * SuspensionOffset;
Vector3 suspEnd = suspStart + Vector3.Down * RestLength;
var spaceState = _body.GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(suspStart, suspEnd);
query.Exclude = new Godot.Collections.Array<Rid> { _body.GetRid() };
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
Vector3 hitPos = result["position"].AsVector3();
float compression = RestLength - (hitPos - suspStart).Length();
if (compression > 0)
{
// 弹簧力
float springForce = compression * SpringStiffness;
// 阻尼力(抵消速度)
float dampForce = _body.LinearVelocity.Y * Damping;
Vector3 totalForce = Vector3.Up * (springForce - dampForce);
_body.ApplyForce(totalForce, _body.GlobalTransform.Basis * SuspensionOffset);
}
}
}
}# 简化悬挂:通过弹簧力模拟
extends Node
@export var rest_length: float = 0.8 # 弹簧自然长度
@export var spring_stiffness: float = 500.0 # 弹簧刚度
@export var damping: float = 30.0 # 阻尼系数
@export var suspension_offset: Vector3 = Vector3(0, -0.5, 0) # 悬挂位置偏移
var _body: RigidBody3D
func _ready():
_body = get_parent()
func _physics_process(delta):
var susp_start = _body.global_position + _body.global_transform.basis * suspension_offset
var susp_end = susp_start + Vector3.DOWN * rest_length
var space_state = _body.get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(susp_start, susp_end)
query.exclude = [_body.get_rid()]
var result = space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos = result["position"]
var compression = rest_length - (hit_pos - susp_start).length()
if compression > 0:
# 弹簧力
var spring_force = compression * spring_stiffness
# 阻尼力(抵消速度)
var damp_force = _body.linear_velocity.y * damping
var total_force = Vector3.UP * (spring_force - damp_force)
_body.apply_force(total_force, _body.global_transform.basis * suspension_offset)物理参数调优指南
以下参数需要反复测试调整:
| 参数 | 初始值 | 调优方向 |
|---|---|---|
| BalanceStrength | 15 | 太小→容易倒;太大→不够灵活 |
| LeanDamping | 5 | 太小→晃个不停;太大→转弯迟钝 |
| AccelerationForce | 800 | 根据车重和期望加速感调整 |
| BrakeForceFront | 1200 | 应该明显大于后刹 |
| MaxLeanAngle | 45 | 超过60度看起来不自然 |
| Mass(RigidBody3D) | 200kg | 包含车身+骑手的总质量 |
调参心得
物理参数没有"正确值",只有"感觉对了的值"。建议先设一个初始值,然后反复试骑,每次只调一个参数,记录变化。就像调吉他弦一样——拧一点,弹一下,再拧,再弹。
常见问题
Q:摩托车总是倒怎么办?
检查 BalanceStrength 是否太小,同时确保 CenterOfMass 足够低。如果问题是低速时容易倒(这是正常的),可以加一个"脚撑"逻辑:速度低于某个值时自动扶正车身。
Q:转弯时感觉像在滑冰?
这说明转向不是基于物理的。确保在 ApplySteering 中使用的是 ApplyTorque(力矩)而不是直接修改旋转,让物理引擎来计算转弯效果。
Q:怎么让摩托车在空中不会乱翻?
在空中时(_isGrounded 为 false),禁用所有力和力矩的施加,只保留一个小的自稳定力矩。或者直接在空中锁定角速度。
下一步
物理系统完成后,开始 拉力赛道设计。
