3. 真实飞行物理
2026/4/14大约 5 分钟
真实飞行物理
飞行模拟器和空战游戏最大的区别在于物理系统的深度。这里我们不只是"升力=常数×速度²",而是要考虑迎角(AoA)、翼型特性、发动机扭矩等更接近真实的因素。但不用担心——我们会用工程师风格的简化,让物理感觉真实又不让代码复杂到无法维护。
本章你将学到
- 迎角(AoA):决定升力的关键因素
- 更完整的飞机气动力模型
- 螺旋桨飞机的发动机扭矩效应
- 失速:速度过低时飞机失去控制
- 地效:降落时接近地面的特殊升力效应
什么是迎角?
迎角(Angle of Attack,AoA)是机翼弦线(机翼前后缘连线)与来流方向之间的夹角。
来流方向 →→→
/
/ ← 迎角(这个角度)
/─────────────── 机翼弦线- 迎角增大 → 升力增大(但阻力也增大)
- 迎角超过临界值(约15°) → 气流分离,失速,升力突然骤降
- 迎角为0° → 仍有少量升力(翼型不对称)
完整气动力控制器
GDScript
extends RigidBody3D
## 真实感飞行物理控制器(简化版)
class_name RealisticFlightController
# === 飞机参数(在编辑器调整)===
@export_group("发动机")
@export var max_engine_power: float = 134000.0 ## 最大功率(瓦特,约180马力)
@export var propeller_efficiency: float = 0.85 ## 螺旋桨效率
@export_group("翼型参数")
@export var wing_area: float = 16.2 ## 机翼面积(平方米)
@export var aspect_ratio: float = 7.3 ## 展弦比(翼展²/机翼面积)
@export var max_cl: float = 1.5 ## 最大升力系数(超过则失速)
@export var stall_angle: float = 15.0 ## 失速迎角(度)
@export_group("操控面")
@export var elevator_effectiveness: float = 1.0 ## 升降舵效果
@export var aileron_effectiveness: float = 0.8 ## 副翼效果
@export var rudder_effectiveness: float = 0.5 ## 方向舵效果
# === 内部状态 ===
var throttle: float = 0.0 ## 油门(0~1)
var airspeed: float = 0.0 ## 真空速(米/秒)
var altitude: float = 0.0 ## 高度(米)
var aoa: float = 0.0 ## 当前迎角(弧度)
var is_stalled: bool = false ## 是否失速
## 空气密度(随高度变化,海平面约1.225 kg/m³)
func _get_air_density() -> float:
var density_sl := 1.225
# 简化的标准大气模型:每1000米密度降低约12%
return density_sl * pow(1.0 - altitude / 44300.0, 4.256)
func _physics_process(delta: float) -> void:
_update_flight_state()
_apply_engine_thrust()
_apply_aerodynamics()
_apply_control_surfaces(delta)
func _update_flight_state() -> void:
airspeed = linear_velocity.length()
altitude = global_position.y
# 迎角:速度向量与机翼弦线(飞机前向)之间的夹角
if airspeed > 1.0:
var forward := -global_transform.basis.z
var vel_normalized := linear_velocity.normalized()
# 取垂直平面内的角度(俯仰方向的迎角)
aoa = forward.angle_to(vel_normalized)
# 判断是仰头(正迎角)还是俯头(负迎角)
var up := global_transform.basis.y
if vel_normalized.dot(up) < 0:
aoa = -aoa
else:
aoa = 0.0
is_stalled = abs(rad_to_deg(aoa)) > stall_angle
func _apply_engine_thrust() -> void:
if airspeed < 0.1:
return
# 推进功率 = 发动机功率 × 螺旋桨效率
# 推力 = 功率 / 速度
var thrust := max_engine_power * throttle * propeller_efficiency / maxf(airspeed, 10.0)
var forward := -global_transform.basis.z
apply_central_force(forward * thrust)
func _apply_aerodynamics() -> void:
if airspeed < 1.0:
return
var air_density := _get_air_density()
var dynamic_pressure := 0.5 * air_density * airspeed * airspeed
# 升力系数:随迎角线性增加直到失速
var aoa_deg := rad_to_deg(aoa)
var cl: float
if is_stalled:
# 失速后升力系数骤降
cl = max_cl * 0.3 * sign(aoa_deg)
else:
cl = clampf(aoa_deg / stall_angle * max_cl, -max_cl, max_cl)
# 诱导阻力系数(升力的副产品)
var cd_induced := cl * cl / (PI * aspect_ratio * 0.9)
# 寄生阻力(与升力无关的摩擦阻力)
var cd_parasitic := 0.025
var cd_total := cd_induced + cd_parasitic
# 升力:垂直于速度方向,偏向机翼上方
var lift_dir := global_transform.basis.y
var lift := dynamic_pressure * wing_area * cl
apply_central_force(lift_dir * lift)
# 阻力:与速度方向相反
var drag := dynamic_pressure * wing_area * cd_total
apply_central_force(-linear_velocity.normalized() * drag)
func _apply_control_surfaces(delta: float) -> void:
var pitch := Input.get_action_strength("pitch_up") - Input.get_action_strength("pitch_down")
var roll := Input.get_action_strength("roll_left") - Input.get_action_strength("roll_right")
var yaw := Input.get_action_strength("yaw_left") - Input.get_action_strength("yaw_right")
# 操控效果随速度增加(速度越快舵面越有效)
var control_authority := clampf(airspeed / 50.0, 0.1, 1.0)
if is_stalled:
control_authority *= 0.3 ## 失速时操控大幅降低
apply_torque(global_transform.basis * Vector3(
pitch * elevator_effectiveness * control_authority,
yaw * rudder_effectiveness * control_authority,
roll * aileron_effectiveness * control_authority
))C
using Godot;
public partial class RealisticFlightController : RigidBody3D
{
[ExportGroup("发动机")]
[Export] public float MaxEnginePower = 134000f;
[Export] public float PropellerEfficiency = 0.85f;
[ExportGroup("翼型参数")]
[Export] public float WingArea = 16.2f;
[Export] public float AspectRatio = 7.3f;
[Export] public float MaxCl = 1.5f;
[Export] public float StallAngle = 15f;
[ExportGroup("操控面")]
[Export] public float ElevatorEffectiveness = 1f;
[Export] public float AileronEffectiveness = 0.8f;
[Export] public float RudderEffectiveness = 0.5f;
public float Throttle { get; set; } = 0f;
public float Airspeed { get; private set; } = 0f;
public float Altitude { get; private set; } = 0f;
public float Aoa { get; private set; } = 0f;
public bool IsStalled { get; private set; } = false;
private float GetAirDensity()
{
return 1.225f * Mathf.Pow(1f - Altitude / 44300f, 4.256f);
}
public override void _PhysicsProcess(double delta)
{
UpdateFlightState();
ApplyEngineThrust();
ApplyAerodynamics();
ApplyControlSurfaces((float)delta);
}
private void UpdateFlightState()
{
Airspeed = LinearVelocity.Length();
Altitude = GlobalPosition.Y;
if (Airspeed > 1f)
{
var forward = -GlobalTransform.Basis.Z;
var velNorm = LinearVelocity.Normalized();
Aoa = forward.AngleTo(velNorm);
if (velNorm.Dot(GlobalTransform.Basis.Y) < 0) Aoa = -Aoa;
}
else Aoa = 0f;
IsStalled = Mathf.Abs(Mathf.RadToDeg(Aoa)) > StallAngle;
}
private void ApplyEngineThrust()
{
if (Airspeed < 0.1f) return;
float thrust = MaxEnginePower * Throttle * PropellerEfficiency / Mathf.Max(Airspeed, 10f);
ApplyCentralForce(-GlobalTransform.Basis.Z * thrust);
}
private void ApplyAerodynamics()
{
if (Airspeed < 1f) return;
float airDensity = GetAirDensity();
float dynamicPressure = 0.5f * airDensity * Airspeed * Airspeed;
float aoaDeg = Mathf.RadToDeg(Aoa);
float cl = IsStalled
? MaxCl * 0.3f * Mathf.Sign(aoaDeg)
: Mathf.Clamp(aoaDeg / StallAngle * MaxCl, -MaxCl, MaxCl);
float cdInduced = cl * cl / (Mathf.Pi * AspectRatio * 0.9f);
float cdTotal = cdInduced + 0.025f;
ApplyCentralForce(GlobalTransform.Basis.Y * dynamicPressure * WingArea * cl);
ApplyCentralForce(-LinearVelocity.Normalized() * dynamicPressure * WingArea * cdTotal);
}
private void ApplyControlSurfaces(float delta)
{
float pitch = Input.GetActionStrength("pitch_up") - Input.GetActionStrength("pitch_down");
float roll = Input.GetActionStrength("roll_left") - Input.GetActionStrength("roll_right");
float yaw = Input.GetActionStrength("yaw_left") - Input.GetActionStrength("yaw_right");
float authority = Mathf.Clamp(Airspeed / 50f, 0.1f, 1f);
if (IsStalled) authority *= 0.3f;
ApplyTorque(GlobalTransform.Basis * new Vector3(
pitch * ElevatorEffectiveness * authority,
yaw * RudderEffectiveness * authority,
roll * AileronEffectiveness * authority
));
}
}失速警告系统
失速是飞行中最危险的状态,游戏里需要明确提示:
GDScript
extends Node
## 失速警告系统
class_name StallWarning
@onready var aircraft: RealisticFlightController = get_parent()
@onready var warning_audio: AudioStreamPlayer = $StallWarningBeep
var is_warning_active: bool = false
func _process(_delta: float) -> void:
## 在接近失速迎角(80%)时触发警告
var aoa_ratio := abs(rad_to_deg(aircraft.aoa)) / aircraft.stall_angle
if aoa_ratio > 0.8 and not is_warning_active:
is_warning_active = true
warning_audio.play() ## 播放"叮叮叮"的失速警告音
elif aoa_ratio <= 0.7 and is_warning_active:
is_warning_active = false
warning_audio.stop()C
using Godot;
public partial class StallWarning : Node
{
private RealisticFlightController _aircraft;
private AudioStreamPlayer _warningAudio;
private bool _isWarningActive = false;
public override void _Ready()
{
_aircraft = GetParent<RealisticFlightController>();
_warningAudio = GetNode<AudioStreamPlayer>("StallWarningBeep");
}
public override void _Process(double delta)
{
float aoaRatio = Mathf.Abs(Mathf.RadToDeg(_aircraft.Aoa)) / _aircraft.StallAngle;
if (aoaRatio > 0.8f && !_isWarningActive)
{
_isWarningActive = true;
_warningAudio.Play();
}
else if (aoaRatio <= 0.7f && _isWarningActive)
{
_isWarningActive = false;
_warningAudio.Stop();
}
}
}下一步
飞行物理完成后,搭建 机场与地形系统。
